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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -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',
},
},
}
+111
View File
@@ -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',
},
},
}
+101
View File
@@ -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',
},
},
}
+67
View File
@@ -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',
},
},
}
+98
View File
@@ -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',
},
},
}
+24
View File
@@ -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,
}
+76
View File
@@ -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',
},
},
}
+673
View File
@@ -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> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&apos;',
}
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
}
+105
View File
@@ -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',
},
},
}
+180
View File
@@ -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
}
}
+78
View File
@@ -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',
},
},
}