chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
WorkdayAssignOnboardingParams,
|
||||
WorkdayAssignOnboardingResponse,
|
||||
} from '@/tools/workday/types'
|
||||
|
||||
export const assignOnboardingTool: ToolConfig<
|
||||
WorkdayAssignOnboardingParams,
|
||||
WorkdayAssignOnboardingResponse
|
||||
> = {
|
||||
id: 'workday_assign_onboarding',
|
||||
name: 'Assign Workday Onboarding Plan',
|
||||
description:
|
||||
'Create or update an onboarding plan assignment for a worker. Sets up onboarding stages and manages the assignment lifecycle.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
tenantUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)',
|
||||
},
|
||||
tenant: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday tenant name',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User password',
|
||||
},
|
||||
workerId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Worker ID to assign the onboarding plan to',
|
||||
},
|
||||
onboardingPlanId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Onboarding plan ID to assign',
|
||||
},
|
||||
actionEventId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Action event ID that enables the onboarding plan (e.g., the hiring event ID)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/workday/assign-onboarding',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => params,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error ?? 'Workday API request failed')
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
outputs: {
|
||||
assignmentId: {
|
||||
type: 'string',
|
||||
description: 'Onboarding plan assignment ID',
|
||||
},
|
||||
workerId: {
|
||||
type: 'string',
|
||||
description: 'Worker ID the plan was assigned to',
|
||||
},
|
||||
planId: {
|
||||
type: 'string',
|
||||
description: 'Onboarding plan ID that was assigned',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkdayChangeJobParams, WorkdayChangeJobResponse } from '@/tools/workday/types'
|
||||
|
||||
export const changeJobTool: ToolConfig<WorkdayChangeJobParams, WorkdayChangeJobResponse> = {
|
||||
id: 'workday_change_job',
|
||||
name: 'Change Workday Job',
|
||||
description:
|
||||
'Perform a job change for a worker including transfers, promotions, demotions, and lateral moves.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
tenantUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)',
|
||||
},
|
||||
tenant: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday tenant name',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User password',
|
||||
},
|
||||
workerId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Worker ID for the job change',
|
||||
},
|
||||
effectiveDate: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Effective date for the job change in ISO 8601 format (e.g., 2025-06-01)',
|
||||
},
|
||||
newPositionId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New position ID (for transfers)',
|
||||
},
|
||||
newJobProfileId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New job profile ID (for role changes)',
|
||||
},
|
||||
newLocationId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New work location ID (for relocations)',
|
||||
},
|
||||
newSupervisoryOrgId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Target supervisory organization ID (for org transfers)',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Reason for the job change (e.g., Promotion, Transfer, Reorganization)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/workday/change-job',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => params,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error ?? 'Workday API request failed')
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
outputs: {
|
||||
eventId: {
|
||||
type: 'string',
|
||||
description: 'Job change event ID',
|
||||
},
|
||||
workerId: {
|
||||
type: 'string',
|
||||
description: 'Worker ID the job change was applied to',
|
||||
},
|
||||
effectiveDate: {
|
||||
type: 'string',
|
||||
description: 'Effective date of the job change',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
WorkdayCreatePrehireParams,
|
||||
WorkdayCreatePrehireResponse,
|
||||
} from '@/tools/workday/types'
|
||||
|
||||
export const createPrehireTool: ToolConfig<
|
||||
WorkdayCreatePrehireParams,
|
||||
WorkdayCreatePrehireResponse
|
||||
> = {
|
||||
id: 'workday_create_prehire',
|
||||
name: 'Create Workday Pre-Hire',
|
||||
description:
|
||||
'Create a new pre-hire (applicant) record in Workday. This is typically the first step before hiring an employee.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
tenantUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)',
|
||||
},
|
||||
tenant: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday tenant name',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User password',
|
||||
},
|
||||
legalName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Full legal name of the pre-hire (e.g., "Jane Doe")',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Email address of the pre-hire',
|
||||
},
|
||||
phoneNumber: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Phone number of the pre-hire',
|
||||
},
|
||||
address: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Address of the pre-hire',
|
||||
},
|
||||
countryCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 Alpha-2 country code (defaults to US)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/workday/create-prehire',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => params,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error ?? 'Workday API request failed')
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
outputs: {
|
||||
preHireId: {
|
||||
type: 'string',
|
||||
description: 'ID of the created pre-hire record',
|
||||
},
|
||||
descriptor: {
|
||||
type: 'string',
|
||||
description: 'Display name of the pre-hire',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
WorkdayGetCompensationParams,
|
||||
WorkdayGetCompensationResponse,
|
||||
} from '@/tools/workday/types'
|
||||
|
||||
export const getCompensationTool: ToolConfig<
|
||||
WorkdayGetCompensationParams,
|
||||
WorkdayGetCompensationResponse
|
||||
> = {
|
||||
id: 'workday_get_compensation',
|
||||
name: 'Get Workday Compensation',
|
||||
description: 'Retrieve compensation plan details for a specific worker.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
tenantUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)',
|
||||
},
|
||||
tenant: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday tenant name',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User password',
|
||||
},
|
||||
workerId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Worker ID to retrieve compensation data for',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/workday/get-compensation',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => params,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error ?? 'Workday API request failed')
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
outputs: {
|
||||
compensationPlans: {
|
||||
type: 'array',
|
||||
description: 'Array of compensation plan details',
|
||||
items: {
|
||||
type: 'json',
|
||||
description: 'Compensation plan with amount, currency, and frequency',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Compensation plan ID' },
|
||||
planName: { type: 'string', description: 'Name of the compensation plan' },
|
||||
amount: { type: 'number', description: 'Compensation amount' },
|
||||
currency: { type: 'string', description: 'Currency code' },
|
||||
frequency: { type: 'string', description: 'Pay frequency' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
WorkdayGetOrganizationsParams,
|
||||
WorkdayGetOrganizationsResponse,
|
||||
} from '@/tools/workday/types'
|
||||
|
||||
export const getOrganizationsTool: ToolConfig<
|
||||
WorkdayGetOrganizationsParams,
|
||||
WorkdayGetOrganizationsResponse
|
||||
> = {
|
||||
id: 'workday_get_organizations',
|
||||
name: 'Get Workday Organizations',
|
||||
description: 'Retrieve organizations, departments, and cost centers from Workday.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
tenantUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)',
|
||||
},
|
||||
tenant: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday tenant name',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User password',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Organization type filter (e.g., Supervisory, Cost_Center, Company, Region)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of organizations to return (default: 20)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of records to skip for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/workday/get-organizations',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => params,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error ?? 'Workday API request failed')
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
outputs: {
|
||||
organizations: {
|
||||
type: 'array',
|
||||
description: 'Array of organization records',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of matching organizations',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkdayGetWorkerParams, WorkdayGetWorkerResponse } from '@/tools/workday/types'
|
||||
|
||||
export const getWorkerTool: ToolConfig<WorkdayGetWorkerParams, WorkdayGetWorkerResponse> = {
|
||||
id: 'workday_get_worker',
|
||||
name: 'Get Workday Worker',
|
||||
description:
|
||||
'Retrieve a specific worker profile including personal, employment, and organization data.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
tenantUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)',
|
||||
},
|
||||
tenant: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday tenant name',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User password',
|
||||
},
|
||||
workerId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Worker ID to retrieve (e.g., 3aa5550b7fe348b98d7b5741afc65534)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/workday/get-worker',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => params,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error ?? 'Workday API request failed')
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
outputs: {
|
||||
worker: {
|
||||
type: 'json',
|
||||
description: 'Worker profile with personal, employment, and organization data',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkdayHireEmployeeParams, WorkdayHireEmployeeResponse } from '@/tools/workday/types'
|
||||
|
||||
export const hireEmployeeTool: ToolConfig<WorkdayHireEmployeeParams, WorkdayHireEmployeeResponse> =
|
||||
{
|
||||
id: 'workday_hire_employee',
|
||||
name: 'Hire Workday Employee',
|
||||
description:
|
||||
'Hire a pre-hire into an employee position. Converts an applicant into an active employee record with position, start date, and manager assignment.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
tenantUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)',
|
||||
},
|
||||
tenant: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday tenant name',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User password',
|
||||
},
|
||||
preHireId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pre-hire (applicant) ID to convert into an employee',
|
||||
},
|
||||
positionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Position ID to assign the new hire to',
|
||||
},
|
||||
hireDate: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Hire date in ISO 8601 format (e.g., 2025-06-01)',
|
||||
},
|
||||
employeeType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Employee type (e.g., Regular, Temporary, Contractor)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/workday/hire',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => params,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error ?? 'Workday API request failed')
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
outputs: {
|
||||
workerId: {
|
||||
type: 'string',
|
||||
description: 'Worker ID of the newly hired employee',
|
||||
},
|
||||
employeeId: {
|
||||
type: 'string',
|
||||
description: 'Employee ID assigned to the new hire',
|
||||
},
|
||||
eventId: {
|
||||
type: 'string',
|
||||
description: 'Event ID of the hire business process',
|
||||
},
|
||||
hireDate: {
|
||||
type: 'string',
|
||||
description: 'Effective hire date',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { assignOnboardingTool } from '@/tools/workday/assign_onboarding'
|
||||
import { changeJobTool } from '@/tools/workday/change_job'
|
||||
import { createPrehireTool } from '@/tools/workday/create_prehire'
|
||||
import { getCompensationTool } from '@/tools/workday/get_compensation'
|
||||
import { getOrganizationsTool } from '@/tools/workday/get_organizations'
|
||||
import { getWorkerTool } from '@/tools/workday/get_worker'
|
||||
import { hireEmployeeTool } from '@/tools/workday/hire_employee'
|
||||
import { listWorkersTool } from '@/tools/workday/list_workers'
|
||||
import { terminateWorkerTool } from '@/tools/workday/terminate_worker'
|
||||
import { updateWorkerTool } from '@/tools/workday/update_worker'
|
||||
|
||||
export * from './types'
|
||||
export {
|
||||
assignOnboardingTool as workdayAssignOnboardingTool,
|
||||
changeJobTool as workdayChangeJobTool,
|
||||
createPrehireTool as workdayCreatePrehireTool,
|
||||
getCompensationTool as workdayGetCompensationTool,
|
||||
getOrganizationsTool as workdayGetOrganizationsTool,
|
||||
getWorkerTool as workdayGetWorkerTool,
|
||||
hireEmployeeTool as workdayHireEmployeeTool,
|
||||
listWorkersTool as workdayListWorkersTool,
|
||||
terminateWorkerTool as workdayTerminateWorkerTool,
|
||||
updateWorkerTool as workdayUpdateWorkerTool,
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkdayListWorkersParams, WorkdayListWorkersResponse } from '@/tools/workday/types'
|
||||
|
||||
export const listWorkersTool: ToolConfig<WorkdayListWorkersParams, WorkdayListWorkersResponse> = {
|
||||
id: 'workday_list_workers',
|
||||
name: 'List Workday Workers',
|
||||
description: 'List or search workers with optional filtering and pagination.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
tenantUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)',
|
||||
},
|
||||
tenant: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday tenant name',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User password',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of workers to return (default: 20)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of records to skip for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/workday/list-workers',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => params,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error ?? 'Workday API request failed')
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
outputs: {
|
||||
workers: {
|
||||
type: 'array',
|
||||
description: 'Array of worker profiles',
|
||||
},
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of matching workers',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { validateWorkdayTenantUrl } from '@/lib/core/security/input-validation'
|
||||
|
||||
const logger = createLogger('WorkdaySoapClient')
|
||||
|
||||
const WORKDAY_SERVICES = {
|
||||
staffing: { name: 'Staffing', version: 'v45.1' },
|
||||
humanResources: { name: 'Human_Resources', version: 'v45.2' },
|
||||
compensation: { name: 'Compensation', version: 'v45.0' },
|
||||
recruiting: { name: 'Recruiting', version: 'v45.0' },
|
||||
} as const
|
||||
|
||||
export type WorkdayServiceKey = keyof typeof WORKDAY_SERVICES
|
||||
|
||||
interface WorkdaySoapResult {
|
||||
Response_Data?: Record<string, unknown>
|
||||
Response_Results?: {
|
||||
Total_Results?: number | string
|
||||
Total_Pages?: number | string
|
||||
Page_Results?: number | string
|
||||
Page?: number | string
|
||||
}
|
||||
Event_Reference?: WorkdayReference
|
||||
Employee_Reference?: WorkdayReference
|
||||
Position_Reference?: WorkdayReference
|
||||
Applicant_Reference?: WorkdayReference & { attributes?: { Descriptor?: string } }
|
||||
Onboarding_Plan_Assignment_Reference?: WorkdayReference
|
||||
Personal_Information_Change_Event_Reference?: WorkdayReference
|
||||
Exceptions_Response_Data?: unknown
|
||||
}
|
||||
|
||||
export interface WorkdayReference {
|
||||
ID?: WorkdayIdEntry[] | WorkdayIdEntry
|
||||
attributes?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface WorkdayIdEntry {
|
||||
$value?: string
|
||||
_?: string
|
||||
attributes?: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw SOAP response shape for a single Worker returned by Get_Workers.
|
||||
* Fields are optional since the Response_Group controls what gets included.
|
||||
*/
|
||||
export interface WorkdayWorkerSoap {
|
||||
Worker_Reference?: WorkdayReference
|
||||
Worker_Descriptor?: string
|
||||
Worker_Data?: WorkdayWorkerDataSoap
|
||||
}
|
||||
|
||||
interface WorkdayWorkerDataSoap {
|
||||
Personal_Data?: Record<string, unknown>
|
||||
Employment_Data?: Record<string, unknown>
|
||||
Compensation_Data?: WorkdayCompensationDataSoap
|
||||
Organization_Data?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface WorkdayCompensationDataSoap {
|
||||
Employee_Base_Pay_Plan_Assignment_Data?:
|
||||
| WorkdayCompensationPlanSoap
|
||||
| WorkdayCompensationPlanSoap[]
|
||||
Employee_Salary_Unit_Plan_Assignment_Data?:
|
||||
| WorkdayCompensationPlanSoap
|
||||
| WorkdayCompensationPlanSoap[]
|
||||
Employee_Bonus_Plan_Assignment_Data?: WorkdayCompensationPlanSoap | WorkdayCompensationPlanSoap[]
|
||||
Employee_Allowance_Plan_Assignment_Data?:
|
||||
| WorkdayCompensationPlanSoap
|
||||
| WorkdayCompensationPlanSoap[]
|
||||
Employee_Commission_Plan_Assignment_Data?:
|
||||
| WorkdayCompensationPlanSoap
|
||||
| WorkdayCompensationPlanSoap[]
|
||||
Employee_Stock_Plan_Assignment_Data?: WorkdayCompensationPlanSoap | WorkdayCompensationPlanSoap[]
|
||||
Employee_Period_Salary_Plan_Assignment_Data?:
|
||||
| WorkdayCompensationPlanSoap
|
||||
| WorkdayCompensationPlanSoap[]
|
||||
}
|
||||
|
||||
export interface WorkdayCompensationPlanSoap {
|
||||
Compensation_Plan_Reference?: WorkdayReference
|
||||
Amount?: number | string
|
||||
Per_Unit_Amount?: number | string
|
||||
Individual_Target_Amount?: number | string
|
||||
Currency_Reference?: WorkdayReference
|
||||
Frequency_Reference?: WorkdayReference
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw SOAP response shape for a single Organization returned by Get_Organizations.
|
||||
*/
|
||||
export interface WorkdayOrganizationSoap {
|
||||
Organization_Reference?: WorkdayReference
|
||||
Organization_Descriptor?: string
|
||||
Organization_Data?: WorkdayOrganizationDataSoap
|
||||
}
|
||||
|
||||
interface WorkdayOrganizationDataSoap {
|
||||
Organization_Type_Reference?: WorkdayReference
|
||||
Organization_Subtype_Reference?: WorkdayReference
|
||||
Inactive?: boolean | string
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a SOAP response field that may be a single object, an array, or undefined
|
||||
* into a consistently typed array.
|
||||
*/
|
||||
export function normalizeSoapArray<T>(value: T | T[] | undefined): T[] {
|
||||
if (!value) return []
|
||||
return Array.isArray(value) ? value : [value]
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces a SOAP scalar to a boolean. The XML parser returns leaf text as strings,
|
||||
* so `"true"`/`"false"` must be normalized before boolean operations like negation.
|
||||
* Returns null when the value is null/undefined or unrecognized.
|
||||
*/
|
||||
export function parseSoapBoolean(value: unknown): boolean | null {
|
||||
if (value == null) return null
|
||||
if (typeof value === 'boolean') return value
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim().toLowerCase()
|
||||
if (trimmed === 'true' || trimmed === '1') return true
|
||||
if (trimmed === 'false' || trimmed === '0') return false
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces a SOAP scalar to a number. The XML parser returns leaf text as strings,
|
||||
* so numeric fields like `Total_Results` must be normalized before arithmetic.
|
||||
* Returns null when the value is null/undefined or not a finite number.
|
||||
*/
|
||||
export function parseSoapNumber(value: unknown): number | null {
|
||||
if (value == null) return null
|
||||
if (typeof value === 'number') return Number.isFinite(value) ? value : null
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
if (trimmed === '') return null
|
||||
const n = Number(trimmed)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const WD_OPERATIONS = [
|
||||
'Get_Workers',
|
||||
'Get_Organizations',
|
||||
'Put_Applicant',
|
||||
'Hire_Employee',
|
||||
'Change_Job',
|
||||
'Terminate_Employee',
|
||||
'Change_Personal_Information',
|
||||
'Put_Onboarding_Plan_Assignment',
|
||||
] as const
|
||||
|
||||
type WorkdayOperation = (typeof WD_OPERATIONS)[number]
|
||||
|
||||
type SoapOperationFn = (
|
||||
args: Record<string, unknown>
|
||||
) => Promise<[WorkdaySoapResult, string, Record<string, unknown>, string]>
|
||||
|
||||
export interface WorkdayClient {
|
||||
Get_WorkersAsync: SoapOperationFn
|
||||
Get_OrganizationsAsync: SoapOperationFn
|
||||
Put_ApplicantAsync: SoapOperationFn
|
||||
Hire_EmployeeAsync: SoapOperationFn
|
||||
Change_JobAsync: SoapOperationFn
|
||||
Terminate_EmployeeAsync: SoapOperationFn
|
||||
Change_Personal_InformationAsync: SoapOperationFn
|
||||
Put_Onboarding_Plan_AssignmentAsync: SoapOperationFn
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the service endpoint URL for a Workday SOAP service.
|
||||
* Pattern: {tenantUrl}/ccx/service/{tenant}/{serviceName}/{version}
|
||||
*
|
||||
* @throws Error if tenantUrl is not a trusted Workday-hosted URL (SSRF guard)
|
||||
*/
|
||||
export function buildServiceUrl(
|
||||
tenantUrl: string,
|
||||
tenant: string,
|
||||
service: WorkdayServiceKey
|
||||
): string {
|
||||
const validation = validateWorkdayTenantUrl(tenantUrl)
|
||||
if (!validation.isValid) {
|
||||
throw new Error(validation.error ?? 'Invalid tenantUrl')
|
||||
}
|
||||
const svc = WORKDAY_SERVICES[service]
|
||||
const baseUrl = (validation.sanitized ?? tenantUrl).replace(/\/$/, '')
|
||||
return `${baseUrl}/ccx/service/${tenant}/${svc.name}/${svc.version}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the WSDL URL for a Workday SOAP service. Retained for backwards compatibility
|
||||
* with any external consumers; the runtime no longer fetches the WSDL.
|
||||
*/
|
||||
export function buildWsdlUrl(
|
||||
tenantUrl: string,
|
||||
tenant: string,
|
||||
service: WorkdayServiceKey
|
||||
): string {
|
||||
return `${buildServiceUrl(tenantUrl, tenant, service)}?wsdl`
|
||||
}
|
||||
|
||||
const XML_ENTITIES: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
}
|
||||
|
||||
function escapeXml(value: string): string {
|
||||
return value.replace(/[&<>"']/g, (c) => XML_ENTITIES[c] ?? c)
|
||||
}
|
||||
|
||||
function serializeAttributes(attrs?: Record<string, string>): string {
|
||||
if (!attrs) return ''
|
||||
let out = ''
|
||||
for (const [k, v] of Object.entries(attrs)) {
|
||||
if (v === undefined || v === null) continue
|
||||
out += ` ${k}="${escapeXml(String(v))}"`
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals a JS value into XML under the `wd:` namespace.
|
||||
* Conventions:
|
||||
* - Plain objects become elements with named children
|
||||
* - `attributes` becomes element attributes
|
||||
* - `$value` (or `_`) provides the element text content
|
||||
* - Arrays produce repeated elements with the same name
|
||||
* - Booleans render as "true"/"false", numbers via String()
|
||||
*/
|
||||
function marshal(name: string, value: unknown): string {
|
||||
if (value === undefined || value === null) return ''
|
||||
const tag = `wd:${name}`
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
let out = ''
|
||||
for (const item of value) {
|
||||
out += marshal(name, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return `<${tag}>${value.toISOString()}</${tag}>`
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
const obj = value as Record<string, unknown>
|
||||
const attrs = obj.attributes as Record<string, string> | undefined
|
||||
const text = (obj.$value ?? obj._) as string | number | boolean | undefined
|
||||
|
||||
if (text !== undefined) {
|
||||
const childKeys = Object.keys(obj).filter(
|
||||
(k) => k !== 'attributes' && k !== '$value' && k !== '_'
|
||||
)
|
||||
if (childKeys.length === 0) {
|
||||
return `<${tag}${serializeAttributes(attrs)}>${escapeXml(String(text))}</${tag}>`
|
||||
}
|
||||
}
|
||||
|
||||
let inner = ''
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (k === 'attributes' || k === '$value' || k === '_') continue
|
||||
inner += marshal(k, v)
|
||||
}
|
||||
if (text !== undefined) inner = escapeXml(String(text)) + inner
|
||||
return `<${tag}${serializeAttributes(attrs)}>${inner}</${tag}>`
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return `<${tag}>${value ? 'true' : 'false'}</${tag}>`
|
||||
}
|
||||
|
||||
return `<${tag}>${escapeXml(String(value))}</${tag}>`
|
||||
}
|
||||
|
||||
function buildEnvelope(
|
||||
operation: string,
|
||||
args: Record<string, unknown>,
|
||||
username: string,
|
||||
password: string
|
||||
): string {
|
||||
let body = ''
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
body += marshal(k, v)
|
||||
}
|
||||
|
||||
const wsseNs = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
|
||||
const wssePwdType =
|
||||
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText'
|
||||
|
||||
return (
|
||||
`<?xml version="1.0" encoding="UTF-8"?>` +
|
||||
`<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wd="urn:com.workday/bsvc">` +
|
||||
`<env:Header>` +
|
||||
`<wsse:Security xmlns:wsse="${wsseNs}" env:mustUnderstand="1">` +
|
||||
`<wsse:UsernameToken>` +
|
||||
`<wsse:Username>${escapeXml(username)}</wsse:Username>` +
|
||||
`<wsse:Password Type="${wssePwdType}">${escapeXml(password)}</wsse:Password>` +
|
||||
`</wsse:UsernameToken>` +
|
||||
`</wsse:Security>` +
|
||||
`</env:Header>` +
|
||||
`<env:Body>` +
|
||||
`<wd:${operation}_Request>` +
|
||||
body +
|
||||
`</wd:${operation}_Request>` +
|
||||
`</env:Body>` +
|
||||
`</env:Envelope>`
|
||||
)
|
||||
}
|
||||
|
||||
interface XmlNode {
|
||||
name: string
|
||||
localName: string
|
||||
attributes: Record<string, string>
|
||||
children: XmlNode[]
|
||||
text: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal XML parser tuned for Workday SOAP responses: namespaced tags,
|
||||
* attributes, mixed text, self-closing tags, and CDATA sections.
|
||||
* Not a general-purpose parser — it does not expand entities beyond the
|
||||
* standard five and ignores processing instructions and DOCTYPE.
|
||||
*/
|
||||
function parseXml(xml: string): XmlNode {
|
||||
let i = 0
|
||||
const len = xml.length
|
||||
|
||||
function skipWhitespace() {
|
||||
while (i < len && xml.charCodeAt(i) <= 32) i++
|
||||
}
|
||||
|
||||
function readName(): string {
|
||||
const start = i
|
||||
while (i < len) {
|
||||
const c = xml[i]
|
||||
if (
|
||||
c === ' ' ||
|
||||
c === '\t' ||
|
||||
c === '\n' ||
|
||||
c === '\r' ||
|
||||
c === '>' ||
|
||||
c === '/' ||
|
||||
c === '='
|
||||
)
|
||||
break
|
||||
i++
|
||||
}
|
||||
return xml.slice(start, i)
|
||||
}
|
||||
|
||||
function readAttributes(): Record<string, string> {
|
||||
const attrs: Record<string, string> = {}
|
||||
while (i < len) {
|
||||
skipWhitespace()
|
||||
const c = xml[i]
|
||||
if (c === '>' || c === '/' || c === '?') return attrs
|
||||
const name = readName()
|
||||
skipWhitespace()
|
||||
if (xml[i] !== '=') {
|
||||
attrs[name] = ''
|
||||
continue
|
||||
}
|
||||
i++ // =
|
||||
skipWhitespace()
|
||||
const quote = xml[i]
|
||||
if (quote !== '"' && quote !== "'") {
|
||||
attrs[name] = ''
|
||||
continue
|
||||
}
|
||||
i++
|
||||
const start = i
|
||||
while (i < len && xml[i] !== quote) i++
|
||||
attrs[name] = decodeEntities(xml.slice(start, i))
|
||||
if (i < len) i++ // closing quote
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
function decodeEntities(s: string): string {
|
||||
return s.replace(/&(amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);/g, (_, ent) => {
|
||||
switch (ent) {
|
||||
case 'amp':
|
||||
return '&'
|
||||
case 'lt':
|
||||
return '<'
|
||||
case 'gt':
|
||||
return '>'
|
||||
case 'quot':
|
||||
return '"'
|
||||
case 'apos':
|
||||
return "'"
|
||||
default:
|
||||
if (ent.startsWith('#x')) return String.fromCodePoint(Number.parseInt(ent.slice(2), 16))
|
||||
if (ent.startsWith('#')) return String.fromCodePoint(Number.parseInt(ent.slice(1), 10))
|
||||
return `&${ent};`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function localOf(name: string): string {
|
||||
const idx = name.indexOf(':')
|
||||
return idx === -1 ? name : name.slice(idx + 1)
|
||||
}
|
||||
|
||||
function parseNode(): XmlNode {
|
||||
if (xml[i] !== '<') throw new Error(`Expected '<' at ${i}`)
|
||||
i++
|
||||
const name = readName()
|
||||
const attrs = readAttributes()
|
||||
skipWhitespace()
|
||||
const node: XmlNode = {
|
||||
name,
|
||||
localName: localOf(name),
|
||||
attributes: attrs,
|
||||
children: [],
|
||||
text: '',
|
||||
}
|
||||
if (xml[i] === '/') {
|
||||
i += 2 // />
|
||||
return node
|
||||
}
|
||||
if (xml[i] !== '>') throw new Error(`Expected '>' at ${i}`)
|
||||
i++
|
||||
|
||||
while (i < len) {
|
||||
if (xml[i] === '<') {
|
||||
if (xml.startsWith('<!--', i)) {
|
||||
const end = xml.indexOf('-->', i)
|
||||
i = end === -1 ? len : end + 3
|
||||
continue
|
||||
}
|
||||
if (xml.startsWith('<![CDATA[', i)) {
|
||||
const end = xml.indexOf(']]>', i + 9)
|
||||
const data = xml.slice(i + 9, end === -1 ? len : end)
|
||||
node.text += data
|
||||
i = end === -1 ? len : end + 3
|
||||
continue
|
||||
}
|
||||
if (xml[i + 1] === '/') {
|
||||
i += 2
|
||||
while (i < len && xml[i] !== '>') i++
|
||||
if (i < len) i++
|
||||
return node
|
||||
}
|
||||
node.children.push(parseNode())
|
||||
} else {
|
||||
const start = i
|
||||
while (i < len && xml[i] !== '<') i++
|
||||
node.text += decodeEntities(xml.slice(start, i))
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// Skip XML declaration and DOCTYPE
|
||||
while (i < len) {
|
||||
skipWhitespace()
|
||||
if (xml.startsWith('<?', i)) {
|
||||
const end = xml.indexOf('?>', i)
|
||||
i = end === -1 ? len : end + 2
|
||||
continue
|
||||
}
|
||||
if (xml.startsWith('<!--', i)) {
|
||||
const end = xml.indexOf('-->', i)
|
||||
i = end === -1 ? len : end + 3
|
||||
continue
|
||||
}
|
||||
if (xml.startsWith('<!', i)) {
|
||||
const end = xml.indexOf('>', i)
|
||||
i = end === -1 ? len : end + 1
|
||||
continue
|
||||
}
|
||||
if (xml[i] === '<') break
|
||||
i++
|
||||
}
|
||||
return parseNode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a parsed XML node tree into the JS object shape that the previous
|
||||
* `soap` library produced: nested objects keyed by local element name,
|
||||
* attributes under `attributes`, repeated elements collapsed into arrays,
|
||||
* and pure text nodes returned as strings.
|
||||
*/
|
||||
function nodeToValue(node: XmlNode): unknown {
|
||||
const hasChildren = node.children.length > 0
|
||||
const trimmedText = node.text.trim()
|
||||
const attrKeys = Object.keys(node.attributes).filter(
|
||||
(k) => k !== 'xmlns' && !k.startsWith('xmlns:')
|
||||
)
|
||||
|
||||
if (!hasChildren && attrKeys.length === 0) {
|
||||
return trimmedText
|
||||
}
|
||||
|
||||
const obj: Record<string, unknown> = {}
|
||||
if (attrKeys.length > 0) {
|
||||
const attrs: Record<string, string> = {}
|
||||
for (const k of attrKeys) {
|
||||
const localKey = k.includes(':') ? k.slice(k.indexOf(':') + 1) : k
|
||||
attrs[localKey] = node.attributes[k]
|
||||
}
|
||||
obj.attributes = attrs
|
||||
}
|
||||
|
||||
if (!hasChildren && trimmedText !== '') {
|
||||
obj.$value = trimmedText
|
||||
return obj
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
const key = child.localName
|
||||
const value = nodeToValue(child)
|
||||
if (key in obj) {
|
||||
const existing = obj[key]
|
||||
if (Array.isArray(existing)) {
|
||||
existing.push(value)
|
||||
} else {
|
||||
obj[key] = [existing, value]
|
||||
}
|
||||
} else {
|
||||
obj[key] = value
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
function findFirst(node: XmlNode, localName: string): XmlNode | null {
|
||||
if (node.localName === localName) return node
|
||||
for (const child of node.children) {
|
||||
const found = findFirst(child, localName)
|
||||
if (found) return found
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function extractFaultMessage(envelope: XmlNode): string | null {
|
||||
const fault = findFirst(envelope, 'Fault')
|
||||
if (!fault) return null
|
||||
const faultstring = findFirst(fault, 'faultstring')
|
||||
if (faultstring?.text.trim()) return faultstring.text.trim()
|
||||
const reason = findFirst(fault, 'Reason')
|
||||
if (reason) {
|
||||
const text = findFirst(reason, 'Text')
|
||||
if (text?.text.trim()) return text.text.trim()
|
||||
}
|
||||
const detail = findFirst(fault, 'detail') ?? findFirst(fault, 'Detail')
|
||||
if (detail) {
|
||||
const msg = findFirst(detail, 'Validation_Error') ?? findFirst(detail, 'Detail_Message')
|
||||
if (msg?.text.trim()) return msg.text.trim()
|
||||
}
|
||||
return 'SOAP fault returned by Workday'
|
||||
}
|
||||
|
||||
async function callOperation(
|
||||
operation: WorkdayOperation,
|
||||
args: Record<string, unknown>,
|
||||
endpoint: string,
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<[WorkdaySoapResult, string, Record<string, unknown>, string]> {
|
||||
const envelope = buildEnvelope(operation, args, username, password)
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
SOAPAction: `""`,
|
||||
},
|
||||
body: envelope,
|
||||
})
|
||||
|
||||
const responseText = await response.text()
|
||||
|
||||
let root: XmlNode
|
||||
try {
|
||||
root = parseXml(responseText)
|
||||
} catch (err) {
|
||||
logger.error('Failed to parse Workday SOAP response', {
|
||||
operation,
|
||||
status: response.status,
|
||||
error: getErrorMessage(err),
|
||||
})
|
||||
throw new Error(
|
||||
`Workday returned an unparseable response (HTTP ${response.status}): ${responseText.slice(0, 500)}`
|
||||
)
|
||||
}
|
||||
|
||||
const fault = extractFaultMessage(root)
|
||||
if (fault) {
|
||||
throw new Error(fault)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Workday SOAP request failed (HTTP ${response.status})`)
|
||||
}
|
||||
|
||||
const responseElement = findFirst(root, `${operation}_Response`)
|
||||
const value = (responseElement ? nodeToValue(responseElement) : {}) as WorkdaySoapResult
|
||||
|
||||
return [value, responseText, {}, envelope]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a typed SOAP client for a Workday service. The returned object
|
||||
* exposes the same `<Operation>Async` methods the previous `soap`-library
|
||||
* client did, so existing call sites do not change. Internally this issues
|
||||
* SOAP-over-HTTP requests directly with hand-built envelopes and an XML
|
||||
* response parser — no WSDL fetch.
|
||||
*/
|
||||
export async function createWorkdaySoapClient(
|
||||
tenantUrl: string,
|
||||
tenant: string,
|
||||
service: WorkdayServiceKey,
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<WorkdayClient> {
|
||||
const endpoint = buildServiceUrl(tenantUrl, tenant, service)
|
||||
logger.info('Creating Workday SOAP client', { service, endpoint })
|
||||
|
||||
function bind(operation: WorkdayOperation): SoapOperationFn {
|
||||
return (args) => callOperation(operation, args, endpoint, username, password)
|
||||
}
|
||||
|
||||
return {
|
||||
Get_WorkersAsync: bind('Get_Workers'),
|
||||
Get_OrganizationsAsync: bind('Get_Organizations'),
|
||||
Put_ApplicantAsync: bind('Put_Applicant'),
|
||||
Hire_EmployeeAsync: bind('Hire_Employee'),
|
||||
Change_JobAsync: bind('Change_Job'),
|
||||
Terminate_EmployeeAsync: bind('Terminate_Employee'),
|
||||
Change_Personal_InformationAsync: bind('Change_Personal_Information'),
|
||||
Put_Onboarding_Plan_AssignmentAsync: bind('Put_Onboarding_Plan_Assignment'),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Workday object reference in the format the SOAP API expects.
|
||||
* Generates: { ID: { attributes: { 'wd:type': idType }, $value: idValue } }
|
||||
*/
|
||||
export function wdRef(idType: string, idValue: string): { ID: WorkdayIdEntry } {
|
||||
return {
|
||||
ID: {
|
||||
attributes: { 'wd:type': idType },
|
||||
$value: idValue,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a reference ID from a SOAP response object.
|
||||
* Handles the nested ID structure that Workday returns.
|
||||
*/
|
||||
export function extractRefId(ref: WorkdayReference | undefined): string | null {
|
||||
if (!ref) return null
|
||||
const id = ref.ID
|
||||
if (Array.isArray(id)) {
|
||||
return id[0]?.$value ?? id[0]?._ ?? null
|
||||
}
|
||||
if (id && typeof id === 'object') {
|
||||
return id.$value ?? id._ ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
WorkdayTerminateWorkerParams,
|
||||
WorkdayTerminateWorkerResponse,
|
||||
} from '@/tools/workday/types'
|
||||
|
||||
export const terminateWorkerTool: ToolConfig<
|
||||
WorkdayTerminateWorkerParams,
|
||||
WorkdayTerminateWorkerResponse
|
||||
> = {
|
||||
id: 'workday_terminate_worker',
|
||||
name: 'Terminate Workday Worker',
|
||||
description:
|
||||
'Initiate a worker termination in Workday. Triggers the Terminate Employee business process.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
tenantUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)',
|
||||
},
|
||||
tenant: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday tenant name',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User password',
|
||||
},
|
||||
workerId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Worker ID to terminate',
|
||||
},
|
||||
terminationDate: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Termination date in ISO 8601 format (e.g., 2025-06-01)',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Termination reason (e.g., Resignation, End_of_Contract, Retirement)',
|
||||
},
|
||||
notificationDate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Date the termination was communicated in ISO 8601 format',
|
||||
},
|
||||
lastDayOfWork: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Last day of work in ISO 8601 format (defaults to termination date)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/workday/terminate',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => params,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error ?? 'Workday API request failed')
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
outputs: {
|
||||
eventId: {
|
||||
type: 'string',
|
||||
description: 'Termination event ID',
|
||||
},
|
||||
workerId: {
|
||||
type: 'string',
|
||||
description: 'Worker ID that was terminated',
|
||||
},
|
||||
terminationDate: {
|
||||
type: 'string',
|
||||
description: 'Effective termination date',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
interface WorkdayBaseParams {
|
||||
tenantUrl: string
|
||||
tenant: string
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
interface WorkdayWorker {
|
||||
id: string | null
|
||||
descriptor: string | null
|
||||
personalData?: unknown
|
||||
employmentData?: unknown
|
||||
compensationData?: unknown
|
||||
organizationData?: unknown
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface WorkdayOrganization {
|
||||
id: string
|
||||
descriptor: string
|
||||
type?: string
|
||||
subtype?: string
|
||||
isActive?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** Get Worker */
|
||||
export interface WorkdayGetWorkerParams extends WorkdayBaseParams {
|
||||
workerId: string
|
||||
}
|
||||
|
||||
export interface WorkdayGetWorkerResponse extends ToolResponse {
|
||||
output: {
|
||||
worker: WorkdayWorker
|
||||
}
|
||||
}
|
||||
|
||||
/** List Workers */
|
||||
export interface WorkdayListWorkersParams extends WorkdayBaseParams {
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
export interface WorkdayListWorkersResponse extends ToolResponse {
|
||||
output: {
|
||||
workers: WorkdayWorker[]
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
/** Create Pre-Hire */
|
||||
export interface WorkdayCreatePrehireParams extends WorkdayBaseParams {
|
||||
legalName: string
|
||||
email?: string
|
||||
phoneNumber?: string
|
||||
address?: string
|
||||
countryCode?: string
|
||||
}
|
||||
|
||||
export interface WorkdayCreatePrehireResponse extends ToolResponse {
|
||||
output: {
|
||||
preHireId: string
|
||||
descriptor: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Hire Employee */
|
||||
export interface WorkdayHireEmployeeParams extends WorkdayBaseParams {
|
||||
preHireId: string
|
||||
positionId: string
|
||||
hireDate: string
|
||||
employeeType?: string
|
||||
}
|
||||
|
||||
export interface WorkdayHireEmployeeResponse extends ToolResponse {
|
||||
output: {
|
||||
workerId: string
|
||||
employeeId: string
|
||||
eventId: string
|
||||
hireDate: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Update Worker */
|
||||
export interface WorkdayUpdateWorkerParams extends WorkdayBaseParams {
|
||||
workerId: string
|
||||
fields: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface WorkdayUpdateWorkerResponse extends ToolResponse {
|
||||
output: {
|
||||
eventId: string
|
||||
workerId: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Assign Onboarding Plan */
|
||||
export interface WorkdayAssignOnboardingParams extends WorkdayBaseParams {
|
||||
workerId: string
|
||||
onboardingPlanId: string
|
||||
actionEventId: string
|
||||
}
|
||||
|
||||
export interface WorkdayAssignOnboardingResponse extends ToolResponse {
|
||||
output: {
|
||||
assignmentId: string
|
||||
workerId: string
|
||||
planId: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Get Organizations */
|
||||
export interface WorkdayGetOrganizationsParams extends WorkdayBaseParams {
|
||||
type?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
export interface WorkdayGetOrganizationsResponse extends ToolResponse {
|
||||
output: {
|
||||
organizations: WorkdayOrganization[]
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
/** Change Job */
|
||||
export interface WorkdayChangeJobParams extends WorkdayBaseParams {
|
||||
workerId: string
|
||||
effectiveDate: string
|
||||
newPositionId?: string
|
||||
newJobProfileId?: string
|
||||
newLocationId?: string
|
||||
newSupervisoryOrgId?: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface WorkdayChangeJobResponse extends ToolResponse {
|
||||
output: {
|
||||
eventId: string
|
||||
workerId: string
|
||||
effectiveDate: string
|
||||
}
|
||||
}
|
||||
|
||||
/** Get Compensation */
|
||||
export interface WorkdayGetCompensationParams extends WorkdayBaseParams {
|
||||
workerId: string
|
||||
}
|
||||
|
||||
export interface WorkdayGetCompensationResponse extends ToolResponse {
|
||||
output: {
|
||||
compensationPlans: Array<{
|
||||
id: string
|
||||
planName: string
|
||||
amount: number
|
||||
currency: string
|
||||
frequency: string
|
||||
[key: string]: unknown
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
/** Terminate Worker */
|
||||
export interface WorkdayTerminateWorkerParams extends WorkdayBaseParams {
|
||||
workerId: string
|
||||
terminationDate: string
|
||||
reason: string
|
||||
notificationDate?: string
|
||||
lastDayOfWork?: string
|
||||
}
|
||||
|
||||
export interface WorkdayTerminateWorkerResponse extends ToolResponse {
|
||||
output: {
|
||||
eventId: string
|
||||
workerId: string
|
||||
terminationDate: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkdayUpdateWorkerParams, WorkdayUpdateWorkerResponse } from '@/tools/workday/types'
|
||||
|
||||
export const updateWorkerTool: ToolConfig<WorkdayUpdateWorkerParams, WorkdayUpdateWorkerResponse> =
|
||||
{
|
||||
id: 'workday_update_worker',
|
||||
name: 'Update Workday Worker',
|
||||
description: 'Update fields on an existing worker record in Workday.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
tenantUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday instance URL (e.g., https://wd5-impl-services1.workday.com)',
|
||||
},
|
||||
tenant: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Workday tenant name',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User username',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Integration System User password',
|
||||
},
|
||||
workerId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Worker ID to update',
|
||||
},
|
||||
fields: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Fields to update as JSON (e.g., {"businessTitle": "Senior Engineer", "primaryWorkEmail": "new@company.com"})',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/workday/update-worker',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => params,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error ?? 'Workday API request failed')
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
outputs: {
|
||||
eventId: {
|
||||
type: 'string',
|
||||
description: 'Event ID of the change personal information business process',
|
||||
},
|
||||
workerId: {
|
||||
type: 'string',
|
||||
description: 'Worker ID that was updated',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user