From 4a2b8b87ccc6bdc0500d1c883d6f7b2ac68f9ecb Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 18 Mar 2026 17:41:55 -0700 Subject: [PATCH 01/13] checkpoint workday block --- apps/sim/blocks/blocks/workday.ts | 439 ++++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 9 + apps/sim/tools/registry.ts | 22 + apps/sim/tools/workday/assign_onboarding.ts | 136 ++++++ apps/sim/tools/workday/change_job.ts | 147 +++++++ apps/sim/tools/workday/create_prehire.ts | 150 +++++++ apps/sim/tools/workday/get_compensation.ts | 112 +++++ apps/sim/tools/workday/get_organizations.ts | 124 ++++++ apps/sim/tools/workday/get_worker.ts | 98 +++++ apps/sim/tools/workday/hire_employee.ts | 155 +++++++ apps/sim/tools/workday/index.ts | 25 ++ apps/sim/tools/workday/list_workers.ts | 123 ++++++ apps/sim/tools/workday/terminate_worker.ts | 139 +++++++ apps/sim/tools/workday/types.ts | 186 +++++++++ apps/sim/tools/workday/update_worker.ts | 112 +++++ apps/sim/tools/workday/utils.ts | 21 + 17 files changed, 2000 insertions(+) create mode 100644 apps/sim/blocks/blocks/workday.ts create mode 100644 apps/sim/tools/workday/assign_onboarding.ts create mode 100644 apps/sim/tools/workday/change_job.ts create mode 100644 apps/sim/tools/workday/create_prehire.ts create mode 100644 apps/sim/tools/workday/get_compensation.ts create mode 100644 apps/sim/tools/workday/get_organizations.ts create mode 100644 apps/sim/tools/workday/get_worker.ts create mode 100644 apps/sim/tools/workday/hire_employee.ts create mode 100644 apps/sim/tools/workday/index.ts create mode 100644 apps/sim/tools/workday/list_workers.ts create mode 100644 apps/sim/tools/workday/terminate_worker.ts create mode 100644 apps/sim/tools/workday/types.ts create mode 100644 apps/sim/tools/workday/update_worker.ts create mode 100644 apps/sim/tools/workday/utils.ts diff --git a/apps/sim/blocks/blocks/workday.ts b/apps/sim/blocks/blocks/workday.ts new file mode 100644 index 00000000000..27a49de65bf --- /dev/null +++ b/apps/sim/blocks/blocks/workday.ts @@ -0,0 +1,439 @@ +import { WorkdayIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' + +export const WorkdayBlock: BlockConfig = { + type: 'workday', + name: 'Workday', + description: 'Manage workers, hiring, onboarding, and HR operations in Workday', + longDescription: + 'Integrate Workday HRIS into your workflow. Create pre-hires, hire employees, manage worker profiles, assign onboarding plans, handle job changes, retrieve compensation data, and process terminations.', + docsLink: 'https://docs.sim.ai/tools/workday', + category: 'tools', + bgColor: '#0875E1', + icon: WorkdayIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Get Worker', id: 'get_worker' }, + { label: 'List Workers', id: 'list_workers' }, + { label: 'Create Pre-Hire', id: 'create_prehire' }, + { label: 'Hire Employee', id: 'hire_employee' }, + { label: 'Update Worker', id: 'update_worker' }, + { label: 'Assign Onboarding Plan', id: 'assign_onboarding' }, + { label: 'Get Organizations', id: 'get_organizations' }, + { label: 'Change Job', id: 'change_job' }, + { label: 'Get Compensation', id: 'get_compensation' }, + { label: 'Terminate Worker', id: 'terminate_worker' }, + ], + value: () => 'get_worker', + }, + { + id: 'tenantUrl', + title: 'Tenant URL', + type: 'short-input', + placeholder: 'https://wd5-impl-services1.workday.com', + required: true, + description: 'Your Workday instance URL', + }, + { + id: 'tenant', + title: 'Tenant Name', + type: 'short-input', + placeholder: 'mycompany', + required: true, + description: 'Workday tenant identifier', + }, + { + id: 'username', + title: 'Username', + type: 'short-input', + placeholder: 'ISU username', + required: true, + description: 'Integration System User username', + }, + { + id: 'password', + title: 'Password', + type: 'short-input', + placeholder: 'ISU password', + password: true, + required: true, + description: 'Integration System User password', + }, + + // Get Worker + { + id: 'workerId', + title: 'Worker ID', + type: 'short-input', + placeholder: 'e.g., 3aa5550b7fe348b98d7b5741afc65534', + condition: { + field: 'operation', + value: [ + 'get_worker', + 'update_worker', + 'assign_onboarding', + 'change_job', + 'get_compensation', + 'terminate_worker', + ], + }, + required: { + field: 'operation', + value: [ + 'get_worker', + 'update_worker', + 'assign_onboarding', + 'change_job', + 'get_compensation', + 'terminate_worker', + ], + }, + }, + + // List Workers + { + id: 'search', + title: 'Search', + type: 'short-input', + placeholder: 'Search by name or ID', + condition: { field: 'operation', value: 'list_workers' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '20', + condition: { field: 'operation', value: ['list_workers', 'get_organizations'] }, + mode: 'advanced', + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: ['list_workers', 'get_organizations'] }, + mode: 'advanced', + }, + + // Create Pre-Hire + { + id: 'legalName', + title: 'Legal Name', + type: 'short-input', + placeholder: 'e.g., Jane Doe', + condition: { field: 'operation', value: 'create_prehire' }, + required: { field: 'operation', value: 'create_prehire' }, + }, + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'jane.doe@company.com', + condition: { field: 'operation', value: 'create_prehire' }, + }, + { + id: 'phoneNumber', + title: 'Phone Number', + type: 'short-input', + placeholder: '+1-555-0100', + condition: { field: 'operation', value: 'create_prehire' }, + mode: 'advanced', + }, + { + id: 'address', + title: 'Address', + type: 'short-input', + placeholder: '123 Main St, City, State', + condition: { field: 'operation', value: 'create_prehire' }, + mode: 'advanced', + }, + { + id: 'sourceId', + title: 'Recruiting Source', + type: 'short-input', + placeholder: 'Source ID (e.g., referral, job board)', + condition: { field: 'operation', value: 'create_prehire' }, + mode: 'advanced', + }, + + // Hire Employee + { + id: 'preHireId', + title: 'Pre-Hire ID', + type: 'short-input', + placeholder: 'Pre-hire record ID', + condition: { field: 'operation', value: 'hire_employee' }, + required: { field: 'operation', value: 'hire_employee' }, + }, + { + id: 'positionId', + title: 'Position ID', + type: 'short-input', + placeholder: 'Position to assign', + condition: { field: 'operation', value: ['hire_employee', 'change_job'] }, + required: { field: 'operation', value: 'hire_employee' }, + }, + { + id: 'hireDate', + title: 'Hire Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'hire_employee' }, + required: { field: 'operation', value: 'hire_employee' }, + wandConfig: { + enabled: true, + prompt: 'Generate an ISO 8601 date (YYYY-MM-DD). Return ONLY the date string.', + generationType: 'timestamp', + }, + }, + { + id: 'jobProfileId', + title: 'Job Profile ID', + type: 'short-input', + placeholder: 'Job profile ID', + condition: { field: 'operation', value: ['hire_employee', 'change_job'] }, + mode: 'advanced', + }, + { + id: 'locationId', + title: 'Location ID', + type: 'short-input', + placeholder: 'Work location ID', + condition: { field: 'operation', value: ['hire_employee', 'change_job'] }, + mode: 'advanced', + }, + { + id: 'managerId', + title: 'Manager ID', + type: 'short-input', + placeholder: 'Manager worker ID', + condition: { field: 'operation', value: ['hire_employee', 'change_job'] }, + mode: 'advanced', + }, + { + id: 'employeeType', + title: 'Employee Type', + type: 'dropdown', + options: [ + { label: 'Regular', id: 'Regular' }, + { label: 'Temporary', id: 'Temporary' }, + { label: 'Contractor', id: 'Contractor' }, + ], + value: () => 'Regular', + condition: { field: 'operation', value: 'hire_employee' }, + mode: 'advanced', + }, + + // Update Worker + { + id: 'fields', + title: 'Fields (JSON)', + type: 'code', + language: 'json', + placeholder: + '{\n "businessTitle": "Senior Engineer",\n "primaryWorkEmail": "new@company.com"\n}', + condition: { field: 'operation', value: 'update_worker' }, + required: { field: 'operation', value: 'update_worker' }, + wandConfig: { + enabled: true, + maintainHistory: true, + prompt: `Generate a Workday worker update payload as JSON. + +### COMMON FIELDS +- businessTitle: Job title string +- primaryWorkEmail: Work email address +- primaryWorkPhone: Work phone number +- managerReference: Manager worker ID + +### RULES +- Output ONLY valid JSON starting with { and ending with } +- Include only fields that need updating + +### EXAMPLE +User: "Update title to Senior Engineer" +Output: {"businessTitle": "Senior Engineer"}`, + generationType: 'json-object', + }, + }, + + // Assign Onboarding + { + id: 'onboardingPlanId', + title: 'Onboarding Plan ID', + type: 'short-input', + placeholder: 'Plan ID to assign', + condition: { field: 'operation', value: 'assign_onboarding' }, + required: { field: 'operation', value: 'assign_onboarding' }, + }, + { + id: 'stages', + title: 'Stage IDs (JSON)', + type: 'short-input', + placeholder: '["stage1", "stage2"]', + condition: { field: 'operation', value: 'assign_onboarding' }, + mode: 'advanced', + }, + + // Get Organizations + { + id: 'orgType', + title: 'Organization Type', + type: 'dropdown', + options: [ + { label: 'All Types', id: '' }, + { label: 'Supervisory', id: 'Supervisory' }, + { label: 'Cost Center', id: 'Cost_Center' }, + { label: 'Company', id: 'Company' }, + { label: 'Region', id: 'Region' }, + ], + value: () => '', + condition: { field: 'operation', value: 'get_organizations' }, + }, + + // Change Job + { + id: 'effectiveDate', + title: 'Effective Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'change_job' }, + required: { field: 'operation', value: 'change_job' }, + wandConfig: { + enabled: true, + prompt: 'Generate an ISO 8601 date (YYYY-MM-DD). Return ONLY the date string.', + generationType: 'timestamp', + }, + }, + { + id: 'reason', + title: 'Reason', + type: 'short-input', + placeholder: 'e.g., Promotion, Transfer', + condition: { field: 'operation', value: ['change_job', 'terminate_worker'] }, + required: { field: 'operation', value: 'terminate_worker' }, + }, + + // Terminate Worker + { + id: 'terminationDate', + title: 'Termination Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'terminate_worker' }, + required: { field: 'operation', value: 'terminate_worker' }, + wandConfig: { + enabled: true, + prompt: 'Generate an ISO 8601 date (YYYY-MM-DD). Return ONLY the date string.', + generationType: 'timestamp', + }, + }, + { + id: 'notificationDate', + title: 'Notification Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'terminate_worker' }, + mode: 'advanced', + }, + { + id: 'lastDayOfWork', + title: 'Last Day of Work', + type: 'short-input', + placeholder: 'YYYY-MM-DD (defaults to termination date)', + condition: { field: 'operation', value: 'terminate_worker' }, + mode: 'advanced', + }, + ], + tools: { + access: [ + 'workday_get_worker', + 'workday_list_workers', + 'workday_create_prehire', + 'workday_hire_employee', + 'workday_update_worker', + 'workday_assign_onboarding', + 'workday_get_organizations', + 'workday_change_job', + 'workday_get_compensation', + 'workday_terminate_worker', + ], + config: { + tool: (params) => `workday_${params.operation}`, + params: (params) => { + const { operation, orgType, fields, ...rest } = params + + if (rest.limit != null && rest.limit !== '') rest.limit = Number(rest.limit) + if (rest.offset != null && rest.offset !== '') rest.offset = Number(rest.offset) + + if (orgType) rest.type = orgType + + if (operation === 'change_job') { + if (rest.positionId) rest.newPositionId = rest.positionId + if (rest.jobProfileId) rest.newJobProfileId = rest.jobProfileId + if (rest.locationId) rest.newLocationId = rest.locationId + if (rest.managerId) rest.newManagerId = rest.managerId + } + + if (fields && operation === 'update_worker') { + const parsedFields = typeof fields === 'string' ? JSON.parse(fields) : fields + return { ...rest, fields: parsedFields } + } + + return rest + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Workday operation to perform' }, + tenantUrl: { type: 'string', description: 'Workday instance URL' }, + tenant: { type: 'string', description: 'Workday tenant name' }, + username: { type: 'string', description: 'ISU username' }, + password: { type: 'string', description: 'ISU password' }, + workerId: { type: 'string', description: 'Worker ID' }, + search: { type: 'string', description: 'Search term' }, + limit: { type: 'number', description: 'Result limit' }, + offset: { type: 'number', description: 'Pagination offset' }, + legalName: { type: 'string', description: 'Legal name for pre-hire' }, + email: { type: 'string', description: 'Email address' }, + phoneNumber: { type: 'string', description: 'Phone number' }, + address: { type: 'string', description: 'Address' }, + sourceId: { type: 'string', description: 'Recruiting source ID' }, + preHireId: { type: 'string', description: 'Pre-hire record ID' }, + positionId: { type: 'string', description: 'Position ID' }, + hireDate: { type: 'string', description: 'Hire date (YYYY-MM-DD)' }, + jobProfileId: { type: 'string', description: 'Job profile ID' }, + locationId: { type: 'string', description: 'Location ID' }, + managerId: { type: 'string', description: 'Manager worker ID' }, + employeeType: { type: 'string', description: 'Employee type' }, + fields: { type: 'json', description: 'Fields to update' }, + onboardingPlanId: { type: 'string', description: 'Onboarding plan ID' }, + stages: { type: 'string', description: 'Onboarding stage IDs' }, + orgType: { type: 'string', description: 'Organization type filter' }, + effectiveDate: { type: 'string', description: 'Effective date (YYYY-MM-DD)' }, + reason: { type: 'string', description: 'Reason for change or termination' }, + terminationDate: { type: 'string', description: 'Termination date (YYYY-MM-DD)' }, + notificationDate: { type: 'string', description: 'Notification date' }, + lastDayOfWork: { type: 'string', description: 'Last day of work' }, + }, + outputs: { + worker: { type: 'json', description: 'Worker profile data' }, + workers: { type: 'json', description: 'Array of worker profiles' }, + total: { type: 'number', description: 'Total count of results' }, + preHireId: { type: 'string', description: 'Created pre-hire ID' }, + descriptor: { type: 'string', description: 'Display name' }, + workerId: { type: 'string', description: 'Worker ID' }, + employeeId: { type: 'string', description: 'Employee ID' }, + hireDate: { type: 'string', description: 'Hire date' }, + assignmentId: { type: 'string', description: 'Onboarding assignment ID' }, + planId: { type: 'string', description: 'Onboarding plan ID' }, + organizations: { type: 'json', description: 'Array of organizations' }, + eventId: { type: 'string', description: 'Event ID for staffing changes' }, + effectiveDate: { type: 'string', description: 'Effective date of change' }, + compensationPlans: { type: 'json', description: 'Compensation plan details' }, + terminationDate: { type: 'string', description: 'Termination date' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index ee75b893544..9f9f23289ea 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -188,6 +188,7 @@ import { WebhookRequestBlock } from '@/blocks/blocks/webhook_request' import { WhatsAppBlock } from '@/blocks/blocks/whatsapp' import { WikipediaBlock } from '@/blocks/blocks/wikipedia' import { WordPressBlock } from '@/blocks/blocks/wordpress' +import { WorkdayBlock } from '@/blocks/blocks/workday' import { WorkflowBlock } from '@/blocks/blocks/workflow' import { WorkflowInputBlock } from '@/blocks/blocks/workflow_input' import { XBlock } from '@/blocks/blocks/x' @@ -408,6 +409,7 @@ export const registry: Record = { whatsapp: WhatsAppBlock, wikipedia: WikipediaBlock, wordpress: WordPressBlock, + workday: WorkdayBlock, workflow: WorkflowBlock, workflow_input: WorkflowInputBlock, x: XBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index a1c6beb37fd..2cf04cf3422 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -124,6 +124,15 @@ export function NoteIcon(props: SVGProps) { ) } +export function WorkdayIcon(props: SVGProps) { + return ( + + + + + ) +} + export function WorkflowIcon(props: SVGProps) { return ( = { wordpress_list_users: wordpressListUsersTool, wordpress_get_user: wordpressGetUserTool, wordpress_search_content: wordpressSearchContentTool, + workday_get_worker: workdayGetWorkerTool, + workday_list_workers: workdayListWorkersTool, + workday_create_prehire: workdayCreatePrehireTool, + workday_hire_employee: workdayHireEmployeeTool, + workday_update_worker: workdayUpdateWorkerTool, + workday_assign_onboarding: workdayAssignOnboardingTool, + workday_get_organizations: workdayGetOrganizationsTool, + workday_change_job: workdayChangeJobTool, + workday_get_compensation: workdayGetCompensationTool, + workday_terminate_worker: workdayTerminateWorkerTool, google_ads_list_customers: googleAdsListCustomersTool, google_ads_search: googleAdsSearchTool, google_ads_list_campaigns: googleAdsListCampaignsTool, diff --git a/apps/sim/tools/workday/assign_onboarding.ts b/apps/sim/tools/workday/assign_onboarding.ts new file mode 100644 index 00000000000..96fb69d8952 --- /dev/null +++ b/apps/sim/tools/workday/assign_onboarding.ts @@ -0,0 +1,136 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { + WorkdayAssignOnboardingParams, + WorkdayAssignOnboardingResponse, +} from '@/tools/workday/types' +import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' + +const logger = createLogger('WorkdayAssignOnboardingTool') + +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', + }, + stages: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'JSON array of onboarding stage IDs to include (optional, defaults to all stages)', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) + return `${baseUrl}/workers/${params.workerId}/onboardingPlanAssignments` + }, + method: 'POST', + headers: (params) => ({ + Authorization: createWorkdayAuthHeader(params.username, params.password), + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = { + onboardingPlan: { id: params.onboardingPlanId }, + } + + if (params.stages) { + try { + const parsedStages = + typeof params.stages === 'string' ? JSON.parse(params.stages) : params.stages + body.stages = Array.isArray(parsedStages) + ? parsedStages.map((s: string) => ({ id: s })) + : [] + } catch { + body.stages = [] + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error ?? data.errors?.[0]?.error ?? data + throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) + } + + return { + success: true, + output: { + assignmentId: data.id ?? null, + workerId: data.worker?.id ?? null, + planId: data.onboardingPlan?.id ?? null, + }, + } + } catch (error) { + logger.error('Workday assign onboarding - Error processing response:', { error }) + throw error + } + }, + + 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', + }, + }, +} diff --git a/apps/sim/tools/workday/change_job.ts b/apps/sim/tools/workday/change_job.ts new file mode 100644 index 00000000000..609e2e37492 --- /dev/null +++ b/apps/sim/tools/workday/change_job.ts @@ -0,0 +1,147 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { WorkdayChangeJobParams, WorkdayChangeJobResponse } from '@/tools/workday/types' +import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' + +const logger = createLogger('WorkdayChangeJobTool') + +export const changeJobTool: ToolConfig = { + 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)', + }, + newManagerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New manager worker ID (for reporting changes)', + }, + reason: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reason for the job change (e.g., Promotion, Transfer, Reorganization)', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) + return `${baseUrl}/workers/${params.workerId}/jobChanges` + }, + method: 'POST', + headers: (params) => ({ + Authorization: createWorkdayAuthHeader(params.username, params.password), + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = { + effectiveDate: params.effectiveDate, + } + + if (params.newPositionId) body.position = { id: params.newPositionId } + if (params.newJobProfileId) body.jobProfile = { id: params.newJobProfileId } + if (params.newLocationId) body.location = { id: params.newLocationId } + if (params.newManagerId) body.manager = { id: params.newManagerId } + if (params.reason) body.reason = params.reason + + return body + }, + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error ?? data.errors?.[0]?.error ?? data + throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) + } + + return { + success: true, + output: { + eventId: data.id ?? null, + workerId: data.worker?.id ?? null, + effectiveDate: data.effectiveDate ?? null, + }, + } + } catch (error) { + logger.error('Workday change job - Error processing response:', { error }) + throw error + } + }, + + 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', + }, + }, +} diff --git a/apps/sim/tools/workday/create_prehire.ts b/apps/sim/tools/workday/create_prehire.ts new file mode 100644 index 00000000000..ee36f0653d5 --- /dev/null +++ b/apps/sim/tools/workday/create_prehire.ts @@ -0,0 +1,150 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { + WorkdayCreatePrehireParams, + WorkdayCreatePrehireResponse, +} from '@/tools/workday/types' +import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' + +const logger = createLogger('WorkdayCreatePrehireTool') + +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', + }, + sourceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Recruiting source ID (e.g., referral, job board)', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) + return `${baseUrl}/preHires` + }, + method: 'POST', + headers: (params) => ({ + Authorization: createWorkdayAuthHeader(params.username, params.password), + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const nameParts = params.legalName.trim().split(/\s+/) + const firstName = nameParts[0] + const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : '' + + const body: Record = { + name: { + firstName, + lastName, + }, + } + + if (params.email) { + body.email = { emailAddress: params.email, usageType: 'WORK' } + } + if (params.phoneNumber) { + body.phone = { phoneNumber: params.phoneNumber, usageType: 'WORK' } + } + if (params.address) { + body.address = { formattedAddress: params.address } + } + if (params.sourceId) { + body.source = { id: params.sourceId } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error ?? data.errors?.[0]?.error ?? data + throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) + } + + return { + success: true, + output: { + preHireId: data.id ?? null, + descriptor: data.descriptor ?? null, + }, + } + } catch (error) { + logger.error('Workday create pre-hire - Error processing response:', { error }) + throw error + } + }, + + outputs: { + preHireId: { + type: 'string', + description: 'ID of the created pre-hire record', + }, + descriptor: { + type: 'string', + description: 'Display name of the pre-hire', + }, + }, +} diff --git a/apps/sim/tools/workday/get_compensation.ts b/apps/sim/tools/workday/get_compensation.ts new file mode 100644 index 00000000000..7750c735334 --- /dev/null +++ b/apps/sim/tools/workday/get_compensation.ts @@ -0,0 +1,112 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { + WorkdayGetCompensationParams, + WorkdayGetCompensationResponse, +} from '@/tools/workday/types' +import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' + +const logger = createLogger('WorkdayGetCompensationTool') + +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: (params) => { + const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) + return `${baseUrl}/workers/${params.workerId}/compensationPlans` + }, + method: 'GET', + headers: (params) => ({ + Authorization: createWorkdayAuthHeader(params.username, params.password), + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error ?? data.errors?.[0]?.error ?? data + throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) + } + + const plans = Array.isArray(data.data) ? data.data : (data.compensationPlans ?? []) + + return { + success: true, + output: { + compensationPlans: plans.map((p: Record) => ({ + id: p.id ?? null, + planName: + (p.compensationPlan as Record)?.descriptor ?? p.planName ?? null, + amount: p.amount ?? p.compensationPlanAmount ?? null, + currency: (p.currency as Record)?.descriptor ?? p.currency ?? null, + frequency: (p.frequency as Record)?.descriptor ?? p.frequency ?? null, + })), + }, + } + } catch (error) { + logger.error('Workday get compensation - Error processing response:', { error }) + throw error + } + }, + + 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' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/workday/get_organizations.ts b/apps/sim/tools/workday/get_organizations.ts new file mode 100644 index 00000000000..a2caffb8264 --- /dev/null +++ b/apps/sim/tools/workday/get_organizations.ts @@ -0,0 +1,124 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { + WorkdayGetOrganizationsParams, + WorkdayGetOrganizationsResponse, +} from '@/tools/workday/types' +import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' + +const logger = createLogger('WorkdayGetOrganizationsTool') + +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: (params) => { + const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) + const queryParams = new URLSearchParams() + + if (params.type) queryParams.append('type', params.type) + if (params.limit) queryParams.append('limit', params.limit.toString()) + if (params.offset) queryParams.append('offset', params.offset.toString()) + + const queryString = queryParams.toString() + return queryString ? `${baseUrl}/organizations?${queryString}` : `${baseUrl}/organizations` + }, + method: 'GET', + headers: (params) => ({ + Authorization: createWorkdayAuthHeader(params.username, params.password), + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error ?? data.errors?.[0]?.error ?? data + throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) + } + + const organizations = Array.isArray(data.data) ? data.data : (data.organizations ?? []) + + return { + success: true, + output: { + organizations: organizations.map((o: Record) => ({ + id: o.id ?? null, + descriptor: o.descriptor ?? null, + type: (o.type as Record)?.descriptor ?? o.type ?? null, + subtype: (o.subtype as Record)?.descriptor ?? o.subtype ?? null, + isActive: o.isActive ?? null, + })), + total: data.total ?? organizations.length, + }, + } + } catch (error) { + logger.error('Workday get organizations - Error processing response:', { error }) + throw error + } + }, + + outputs: { + organizations: { + type: 'array', + description: 'Array of organization records', + }, + total: { + type: 'number', + description: 'Total number of matching organizations', + }, + }, +} diff --git a/apps/sim/tools/workday/get_worker.ts b/apps/sim/tools/workday/get_worker.ts new file mode 100644 index 00000000000..795cb5f62a4 --- /dev/null +++ b/apps/sim/tools/workday/get_worker.ts @@ -0,0 +1,98 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { WorkdayGetWorkerParams, WorkdayGetWorkerResponse } from '@/tools/workday/types' +import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' + +const logger = createLogger('WorkdayGetWorkerTool') + +export const getWorkerTool: ToolConfig = { + 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: (params) => { + const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) + return `${baseUrl}/workers/${params.workerId}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: createWorkdayAuthHeader(params.username, params.password), + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error ?? data.errors?.[0]?.error ?? data + throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) + } + + return { + success: true, + output: { + worker: { + id: data.id ?? null, + descriptor: data.descriptor ?? null, + primaryWorkEmail: data.primaryWorkEmail ?? null, + primaryWorkPhone: data.primaryWorkPhone ?? null, + businessTitle: data.businessTitle ?? null, + supervisoryOrganization: data.supervisoryOrganization?.descriptor ?? null, + hireDate: data.hireDate ?? null, + workerType: data.workerType?.descriptor ?? null, + isActive: data.isActive ?? null, + ...data, + }, + }, + } + } catch (error) { + logger.error('Workday get worker - Error processing response:', { error }) + throw error + } + }, + + outputs: { + worker: { + type: 'json', + description: 'Worker profile with personal, employment, and organization data', + }, + }, +} diff --git a/apps/sim/tools/workday/hire_employee.ts b/apps/sim/tools/workday/hire_employee.ts new file mode 100644 index 00000000000..4cdc7b0dbeb --- /dev/null +++ b/apps/sim/tools/workday/hire_employee.ts @@ -0,0 +1,155 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { WorkdayHireEmployeeParams, WorkdayHireEmployeeResponse } from '@/tools/workday/types' +import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' + +const logger = createLogger('WorkdayHireEmployeeTool') + +export const hireEmployeeTool: ToolConfig = + { + 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)', + }, + jobProfileId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Job profile ID for the position', + }, + locationId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Work location ID', + }, + managerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Manager worker ID for the reporting relationship', + }, + employeeType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Employee type (e.g., Regular, Temporary, Contractor)', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) + return `${baseUrl}/staffingEvents` + }, + method: 'POST', + headers: (params) => ({ + Authorization: createWorkdayAuthHeader(params.username, params.password), + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = { + type: 'HIRE', + preHire: { id: params.preHireId }, + position: { id: params.positionId }, + hireDate: params.hireDate, + } + + if (params.jobProfileId) body.jobProfile = { id: params.jobProfileId } + if (params.locationId) body.location = { id: params.locationId } + if (params.managerId) body.manager = { id: params.managerId } + if (params.employeeType) body.employeeType = params.employeeType + + return body + }, + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error ?? data.errors?.[0]?.error ?? data + throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) + } + + return { + success: true, + output: { + workerId: data.worker?.id ?? data.workerId ?? null, + employeeId: data.employeeId ?? data.id ?? null, + descriptor: data.descriptor ?? data.worker?.descriptor ?? null, + hireDate: data.hireDate ?? null, + }, + } + } catch (error) { + logger.error('Workday hire employee - Error processing response:', { error }) + throw error + } + }, + + outputs: { + workerId: { + type: 'string', + description: 'Worker ID of the newly hired employee', + }, + employeeId: { + type: 'string', + description: 'Employee ID assigned to the new hire', + }, + descriptor: { + type: 'string', + description: 'Display name of the hired employee', + }, + hireDate: { + type: 'string', + description: 'Effective hire date', + }, + }, + } diff --git a/apps/sim/tools/workday/index.ts b/apps/sim/tools/workday/index.ts new file mode 100644 index 00000000000..6a93351db52 --- /dev/null +++ b/apps/sim/tools/workday/index.ts @@ -0,0 +1,25 @@ +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 { + 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, +} + +export * from './types' diff --git a/apps/sim/tools/workday/list_workers.ts b/apps/sim/tools/workday/list_workers.ts new file mode 100644 index 00000000000..58d238cbeef --- /dev/null +++ b/apps/sim/tools/workday/list_workers.ts @@ -0,0 +1,123 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { WorkdayListWorkersParams, WorkdayListWorkersResponse } from '@/tools/workday/types' +import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' + +const logger = createLogger('WorkdayListWorkersTool') + +export const listWorkersTool: ToolConfig = { + 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', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term to filter workers by name or ID', + }, + 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: (params) => { + const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) + const queryParams = new URLSearchParams() + + if (params.search) queryParams.append('search', params.search) + if (params.limit) queryParams.append('limit', params.limit.toString()) + if (params.offset) queryParams.append('offset', params.offset.toString()) + + const queryString = queryParams.toString() + return queryString ? `${baseUrl}/workers?${queryString}` : `${baseUrl}/workers` + }, + method: 'GET', + headers: (params) => ({ + Authorization: createWorkdayAuthHeader(params.username, params.password), + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error ?? data.errors?.[0]?.error ?? data + throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) + } + + const workers = Array.isArray(data.data) ? data.data : (data.workers ?? []) + + return { + success: true, + output: { + workers: workers.map((w: Record) => ({ + id: w.id ?? null, + descriptor: w.descriptor ?? null, + primaryWorkEmail: w.primaryWorkEmail ?? null, + primaryWorkPhone: w.primaryWorkPhone ?? null, + businessTitle: w.businessTitle ?? null, + supervisoryOrganization: + (w.supervisoryOrganization as Record)?.descriptor ?? null, + hireDate: w.hireDate ?? null, + workerType: (w.workerType as Record)?.descriptor ?? null, + isActive: w.isActive ?? null, + })), + total: data.total ?? workers.length, + }, + } + } catch (error) { + logger.error('Workday list workers - Error processing response:', { error }) + throw error + } + }, + + outputs: { + workers: { + type: 'array', + description: 'Array of worker profiles', + }, + total: { + type: 'number', + description: 'Total number of matching workers', + }, + }, +} diff --git a/apps/sim/tools/workday/terminate_worker.ts b/apps/sim/tools/workday/terminate_worker.ts new file mode 100644 index 00000000000..939eac62111 --- /dev/null +++ b/apps/sim/tools/workday/terminate_worker.ts @@ -0,0 +1,139 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { + WorkdayTerminateWorkerParams, + WorkdayTerminateWorkerResponse, +} from '@/tools/workday/types' +import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' + +const logger = createLogger('WorkdayTerminateWorkerTool') + +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: (params) => { + const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) + return `${baseUrl}/workers/${params.workerId}/terminations` + }, + method: 'POST', + headers: (params) => ({ + Authorization: createWorkdayAuthHeader(params.username, params.password), + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const body: Record = { + terminationDate: params.terminationDate, + reason: params.reason, + } + + if (params.notificationDate) body.notificationDate = params.notificationDate + if (params.lastDayOfWork) body.lastDayOfWork = params.lastDayOfWork + + return body + }, + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error ?? data.errors?.[0]?.error ?? data + throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) + } + + return { + success: true, + output: { + eventId: data.id ?? null, + workerId: data.worker?.id ?? null, + terminationDate: data.terminationDate ?? null, + }, + } + } catch (error) { + logger.error('Workday terminate worker - Error processing response:', { error }) + throw error + } + }, + + outputs: { + eventId: { + type: 'string', + description: 'Termination event ID', + }, + workerId: { + type: 'string', + description: 'Worker ID that was terminated', + }, + terminationDate: { + type: 'string', + description: 'Effective termination date', + }, + }, +} diff --git a/apps/sim/tools/workday/types.ts b/apps/sim/tools/workday/types.ts new file mode 100644 index 00000000000..42aba56147b --- /dev/null +++ b/apps/sim/tools/workday/types.ts @@ -0,0 +1,186 @@ +import type { ToolResponse } from '@/tools/types' + +export interface WorkdayBaseParams { + tenantUrl: string + tenant: string + username: string + password: string +} + +export interface WorkdayWorker { + id: string + descriptor: string + primaryWorkEmail?: string + primaryWorkPhone?: string + businessTitle?: string + supervisoryOrganization?: string + hireDate?: string + workerType?: string + isActive?: boolean + [key: string]: unknown +} + +export 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 { + search?: string + 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 + sourceId?: string +} + +export interface WorkdayCreatePrehireResponse extends ToolResponse { + output: { + preHireId: string + descriptor: string + } +} + +/** Hire Employee */ +export interface WorkdayHireEmployeeParams extends WorkdayBaseParams { + preHireId: string + positionId: string + hireDate: string + jobProfileId?: string + locationId?: string + managerId?: string + employeeType?: string +} + +export interface WorkdayHireEmployeeResponse extends ToolResponse { + output: { + workerId: string + employeeId: string + descriptor: string + hireDate: string + } +} + +/** Update Worker */ +export interface WorkdayUpdateWorkerParams extends WorkdayBaseParams { + workerId: string + fields: Record +} + +export interface WorkdayUpdateWorkerResponse extends ToolResponse { + output: { + worker: WorkdayWorker + } +} + +/** Assign Onboarding Plan */ +export interface WorkdayAssignOnboardingParams extends WorkdayBaseParams { + workerId: string + onboardingPlanId: string + stages?: 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 + newManagerId?: 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 + } +} diff --git a/apps/sim/tools/workday/update_worker.ts b/apps/sim/tools/workday/update_worker.ts new file mode 100644 index 00000000000..9fe7ed929d5 --- /dev/null +++ b/apps/sim/tools/workday/update_worker.ts @@ -0,0 +1,112 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' +import type { WorkdayUpdateWorkerParams, WorkdayUpdateWorkerResponse } from '@/tools/workday/types' +import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' + +const logger = createLogger('WorkdayUpdateWorkerTool') + +export const updateWorkerTool: ToolConfig = + { + 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: (params) => { + const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) + return `${baseUrl}/workers/${params.workerId}` + }, + method: 'PATCH', + headers: (params) => ({ + Authorization: createWorkdayAuthHeader(params.username, params.password), + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + if (!params.fields || typeof params.fields !== 'object') { + throw new Error('Fields must be a JSON object') + } + return params.fields + }, + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error ?? data.errors?.[0]?.error ?? data + throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) + } + + return { + success: true, + output: { + worker: { + id: data.id ?? null, + descriptor: data.descriptor ?? null, + primaryWorkEmail: data.primaryWorkEmail ?? null, + primaryWorkPhone: data.primaryWorkPhone ?? null, + businessTitle: data.businessTitle ?? null, + supervisoryOrganization: data.supervisoryOrganization?.descriptor ?? null, + hireDate: data.hireDate ?? null, + workerType: data.workerType?.descriptor ?? null, + isActive: data.isActive ?? null, + ...data, + }, + }, + } + } catch (error) { + logger.error('Workday update worker - Error processing response:', { error }) + throw error + } + }, + + outputs: { + worker: { + type: 'json', + description: 'Updated worker record', + }, + }, + } diff --git a/apps/sim/tools/workday/utils.ts b/apps/sim/tools/workday/utils.ts new file mode 100644 index 00000000000..33a0f10c95f --- /dev/null +++ b/apps/sim/tools/workday/utils.ts @@ -0,0 +1,21 @@ +/** + * Creates a Basic Authentication header for Workday ISU credentials. + * @param username Integration System User username + * @param password Integration System User password + * @returns Base64-encoded Basic Auth header value + */ +export function createWorkdayAuthHeader(username: string, password: string): string { + const credentials = Buffer.from(`${username}:${password}`).toString('base64') + return `Basic ${credentials}` +} + +/** + * Builds a Workday REST API base URL from tenant URL and tenant name. + * @param tenantUrl The Workday instance URL (e.g., https://wd5-impl-services1.workday.com) + * @param tenant The tenant name + * @returns Formatted base URL for API calls + */ +export function buildWorkdayBaseUrl(tenantUrl: string, tenant: string): string { + const baseUrl = tenantUrl.replace(/\/$/, '') + return `${baseUrl}/ccx/api/v1/${tenant}` +} From 734f4ee4bddbd341dd1c3df31b993f13024abb3f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 18 Mar 2026 19:07:34 -0700 Subject: [PATCH 02/13] add icon svg --- apps/sim/blocks/blocks/workday.ts | 2 +- apps/sim/components/icons.tsx | 23 ++++++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/sim/blocks/blocks/workday.ts b/apps/sim/blocks/blocks/workday.ts index 27a49de65bf..8e14f527eb9 100644 --- a/apps/sim/blocks/blocks/workday.ts +++ b/apps/sim/blocks/blocks/workday.ts @@ -9,7 +9,7 @@ export const WorkdayBlock: BlockConfig = { 'Integrate Workday HRIS into your workflow. Create pre-hires, hire employees, manage worker profiles, assign onboarding plans, handle job changes, retrieve compensation data, and process terminations.', docsLink: 'https://docs.sim.ai/tools/workday', category: 'tools', - bgColor: '#0875E1', + bgColor: '#F5F0EB', icon: WorkdayIcon, subBlocks: [ { diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 2cf04cf3422..86885a81571 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -126,9 +126,26 @@ export function NoteIcon(props: SVGProps) { export function WorkdayIcon(props: SVGProps) { return ( - - - + + + + + + + + + + ) } From 70ff0ee3d8810ac78cf0296e81c1c4cb1e85cc0c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 18 Mar 2026 20:34:08 -0700 Subject: [PATCH 03/13] fix workday to use soap api --- .../tools/workday/assign-onboarding/route.ts | 68 ++++++++++ .../app/api/tools/workday/change-job/route.ts | 94 +++++++++++++ .../api/tools/workday/create-prehire/route.ts | 127 ++++++++++++++++++ .../tools/workday/get-compensation/route.ts | 81 +++++++++++ .../tools/workday/get-organizations/route.ts | 92 +++++++++++++ .../app/api/tools/workday/get-worker/route.ts | 84 ++++++++++++ apps/sim/app/api/tools/workday/hire/route.ts | 81 +++++++++++ .../api/tools/workday/list-workers/route.ts | 81 +++++++++++ .../app/api/tools/workday/terminate/route.ts | 77 +++++++++++ .../api/tools/workday/update-worker/route.ts | 68 ++++++++++ apps/sim/blocks/blocks/workday.ts | 15 ++- apps/sim/package.json | 1 + apps/sim/tools/workday/assign_onboarding.ts | 62 ++------- apps/sim/tools/workday/change_job.ts | 52 ++----- apps/sim/tools/workday/create_prehire.ts | 63 +-------- apps/sim/tools/workday/get_compensation.ts | 59 ++------ apps/sim/tools/workday/get_organizations.ts | 66 ++------- apps/sim/tools/workday/get_worker.ts | 49 ++----- apps/sim/tools/workday/hire_employee.ts | 53 +------- apps/sim/tools/workday/list_workers.ts | 59 ++------ apps/sim/tools/workday/soap.ts | 117 ++++++++++++++++ apps/sim/tools/workday/terminate_worker.ts | 60 ++------- apps/sim/tools/workday/types.ts | 3 +- apps/sim/tools/workday/update_worker.ts | 54 ++------ apps/sim/tools/workday/utils.ts | 29 +++- bun.lock | 34 ++++- 26 files changed, 1135 insertions(+), 494 deletions(-) create mode 100644 apps/sim/app/api/tools/workday/assign-onboarding/route.ts create mode 100644 apps/sim/app/api/tools/workday/change-job/route.ts create mode 100644 apps/sim/app/api/tools/workday/create-prehire/route.ts create mode 100644 apps/sim/app/api/tools/workday/get-compensation/route.ts create mode 100644 apps/sim/app/api/tools/workday/get-organizations/route.ts create mode 100644 apps/sim/app/api/tools/workday/get-worker/route.ts create mode 100644 apps/sim/app/api/tools/workday/hire/route.ts create mode 100644 apps/sim/app/api/tools/workday/list-workers/route.ts create mode 100644 apps/sim/app/api/tools/workday/terminate/route.ts create mode 100644 apps/sim/app/api/tools/workday/update-worker/route.ts create mode 100644 apps/sim/tools/workday/soap.ts diff --git a/apps/sim/app/api/tools/workday/assign-onboarding/route.ts b/apps/sim/app/api/tools/workday/assign-onboarding/route.ts new file mode 100644 index 00000000000..ed0643aabb1 --- /dev/null +++ b/apps/sim/app/api/tools/workday/assign-onboarding/route.ts @@ -0,0 +1,68 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayAssignOnboardingAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), + onboardingPlanId: z.string().min(1), + actionEventId: z.string().min(1), + stages: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const [result] = await client.Put_Onboarding_Plan_AssignmentAsync({ + Onboarding_Plan_Assignment_Data: { + Onboarding_Plan_Reference: wdRef('Onboarding_Plan_ID', data.onboardingPlanId), + Person_Reference: wdRef('Employee_ID', data.workerId), + Action_Event_Reference: wdRef('Background_Check_ID', data.actionEventId), + Assignment_Effective_Moment: new Date().toISOString(), + Active: true, + }, + }) + + return NextResponse.json({ + success: true, + output: { + assignmentId: extractRefId(result?.Onboarding_Plan_Assignment_Reference), + workerId: data.workerId, + planId: data.onboardingPlanId, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday assign onboarding failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/change-job/route.ts b/apps/sim/app/api/tools/workday/change-job/route.ts new file mode 100644 index 00000000000..615db2fb21e --- /dev/null +++ b/apps/sim/app/api/tools/workday/change-job/route.ts @@ -0,0 +1,94 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayChangeJobAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), + effectiveDate: z.string().min(1), + newPositionId: z.string().optional(), + newJobProfileId: z.string().optional(), + newLocationId: z.string().optional(), + newManagerId: z.string().optional(), + reason: z.string().min(1, 'Reason is required for job changes'), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const changeJobDetailData: Record = { + Reason_Reference: wdRef('Change_Job_Subcategory_ID', data.reason), + } + if (data.newPositionId) { + changeJobDetailData.Position_Reference = wdRef('Position_ID', data.newPositionId) + } + if (data.newJobProfileId) { + changeJobDetailData.Job_Profile_Reference = wdRef('Job_Profile_ID', data.newJobProfileId) + } + if (data.newLocationId) { + changeJobDetailData.Location_Reference = wdRef('Location_ID', data.newLocationId) + } + if (data.newManagerId) { + changeJobDetailData.Supervisory_Organization_Reference = wdRef( + 'Supervisory_Organization_ID', + data.newManagerId + ) + } + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'staffing', + data.username, + data.password + ) + + const [result] = await client.Change_JobAsync({ + Business_Process_Parameters: { + Auto_Complete: true, + Run_Now: true, + }, + Change_Job_Data: { + Worker_Reference: wdRef('Employee_ID', data.workerId), + Effective_Date: data.effectiveDate, + Change_Job_Detail_Data: changeJobDetailData, + }, + }) + + const eventRef = result?.Event_Reference + + return NextResponse.json({ + success: true, + output: { + eventId: extractRefId(eventRef), + workerId: data.workerId, + effectiveDate: data.effectiveDate, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday change job failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/create-prehire/route.ts b/apps/sim/app/api/tools/workday/create-prehire/route.ts new file mode 100644 index 00000000000..bc98e28786c --- /dev/null +++ b/apps/sim/app/api/tools/workday/create-prehire/route.ts @@ -0,0 +1,127 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayCreatePrehireAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + legalName: z.string().min(1), + email: z.string().optional(), + phoneNumber: z.string().optional(), + address: z.string().optional(), + sourceId: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const parts = data.legalName.trim().split(/\s+/) + const firstName = parts[0] ?? '' + const lastName = parts.length > 1 ? parts.slice(1).join(' ') : (parts[0] ?? '') + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'staffing', + data.username, + data.password + ) + + if (!data.email && !data.phoneNumber && !data.address) { + return NextResponse.json( + { + success: false, + error: 'At least one contact method (email, phone, or address) is required', + }, + { status: 400 } + ) + } + + const contactData: Record = {} + if (data.email) { + contactData.Email_Address_Data = [ + { + Email_Address: data.email, + Usage_Data: { + Type_Data: { Type_Reference: wdRef('Communication_Usage_Type_ID', 'WORK') }, + Public: true, + }, + }, + ] + } + if (data.phoneNumber) { + contactData.Phone_Data = [ + { + Phone_Number: data.phoneNumber, + Phone_Device_Type_Reference: wdRef('Phone_Device_Type_ID', 'Landline'), + Usage_Data: { + Type_Data: { Type_Reference: wdRef('Communication_Usage_Type_ID', 'WORK') }, + Public: true, + }, + }, + ] + } + if (data.address) { + contactData.Address_Data = [ + { + Formatted_Address: data.address, + Usage_Data: { + Type_Data: { Type_Reference: wdRef('Communication_Usage_Type_ID', 'WORK') }, + Public: true, + }, + }, + ] + } + + const [result] = await client.Put_ApplicantAsync({ + Applicant_Data: { + Personal_Data: { + Name_Data: { + Legal_Name_Data: { + Name_Detail_Data: { + Country_Reference: wdRef('ISO_3166-1_Alpha-2_Code', 'US'), + First_Name: firstName, + Last_Name: lastName, + }, + }, + }, + Contact_Information_Data: contactData, + }, + }, + }) + + const applicantRef = result?.Applicant_Reference + + return NextResponse.json({ + success: true, + output: { + preHireId: extractRefId(applicantRef), + descriptor: applicantRef?.attributes?.Descriptor ?? null, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday create prehire failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/get-compensation/route.ts b/apps/sim/app/api/tools/workday/get-compensation/route.ts new file mode 100644 index 00000000000..cacea304c01 --- /dev/null +++ b/apps/sim/app/api/tools/workday/get-compensation/route.ts @@ -0,0 +1,81 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayGetCompensationAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const [result] = await client.Get_WorkersAsync({ + Request_References: { + Worker_Reference: { + ID: { attributes: { 'wd:type': 'Employee_ID' }, $value: data.workerId }, + }, + }, + Response_Group: { + Include_Reference: true, + Include_Compensation: true, + }, + }) + + const rawWorker = result?.Response_Data?.Worker + const workerData = (Array.isArray(rawWorker) ? rawWorker[0] : (rawWorker ?? null)) as Record< + string, + unknown + > | null + const workerInner = workerData?.Worker_Data as Record | undefined + const compensationData = workerInner?.Compensation_Data as Record | undefined + + const rawPlans = compensationData?.Compensation_Plan_Assignment + const plansArray = (Array.isArray(rawPlans) ? rawPlans : rawPlans ? [rawPlans] : []) as Record< + string, + unknown + >[] + + const compensationPlans = plansArray.map((p) => ({ + ...p, + })) + + return NextResponse.json({ + success: true, + output: { compensationPlans }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday get compensation failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/get-organizations/route.ts b/apps/sim/app/api/tools/workday/get-organizations/route.ts new file mode 100644 index 00000000000..70488285e8d --- /dev/null +++ b/apps/sim/app/api/tools/workday/get-organizations/route.ts @@ -0,0 +1,92 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayGetOrganizationsAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + type: z.string().optional(), + limit: z.number().optional(), + offset: z.number().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const limit = data.limit ?? 20 + const offset = data.offset ?? 0 + const page = offset > 0 ? Math.floor(offset / limit) + 1 : 1 + + const [result] = await client.Get_OrganizationsAsync({ + Response_Filter: { Page: page, Count: limit }, + Request_Criteria: data.type + ? { + Organization_Type_Reference: { + ID: { + attributes: { 'wd:type': 'Organization_Type_ID' }, + $value: data.type, + }, + }, + } + : undefined, + Response_Group: { Include_Hierarchy_Data: true }, + }) + + const rawOrgs = result?.Response_Data?.Organization + const orgsArray = Array.isArray(rawOrgs) ? rawOrgs : rawOrgs ? [rawOrgs] : [] + + const organizations = orgsArray.map((o: Record) => { + const orgData = o.Organization_Data as Record | undefined + return { + id: extractRefId(o.Organization_Reference as Record) ?? null, + descriptor: o.Organization_Descriptor ?? null, + type: orgData?.Organization_Type_Reference + ? (extractRefId(orgData.Organization_Type_Reference) ?? null) + : null, + subtype: orgData?.Organization_Subtype_Reference + ? (extractRefId(orgData.Organization_Subtype_Reference) ?? null) + : null, + isActive: orgData?.Inactive != null ? !orgData.Inactive : null, + } + }) + + const total = result?.Response_Results?.Total_Results ?? organizations.length + + return NextResponse.json({ + success: true, + output: { organizations, total }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday get organizations failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/get-worker/route.ts b/apps/sim/app/api/tools/workday/get-worker/route.ts new file mode 100644 index 00000000000..b99c9f7d8eb --- /dev/null +++ b/apps/sim/app/api/tools/workday/get-worker/route.ts @@ -0,0 +1,84 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, type WorkdayReference } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayGetWorkerAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const [result] = await client.Get_WorkersAsync({ + Request_References: { + Worker_Reference: { + ID: { attributes: { 'wd:type': 'Employee_ID' }, $value: data.workerId }, + }, + }, + Response_Group: { + Include_Reference: true, + Include_Personal_Information: true, + Include_Employment_Information: true, + Include_Compensation: true, + Include_Organizations: true, + }, + }) + + const rawWorker = result?.Response_Data?.Worker + const workerData = (Array.isArray(rawWorker) ? rawWorker[0] : (rawWorker ?? null)) as Record< + string, + unknown + > | null + const workerInner = (workerData?.Worker_Data ?? null) as Record | null + + return NextResponse.json({ + success: true, + output: { + worker: workerData + ? { + id: extractRefId(workerData.Worker_Reference as WorkdayReference | undefined) ?? null, + descriptor: (workerData.Worker_Descriptor as string) ?? null, + personalData: workerInner?.Personal_Data ?? null, + employmentData: workerInner?.Employment_Data ?? null, + compensationData: workerInner?.Compensation_Data ?? null, + organizationData: workerInner?.Organization_Data ?? null, + } + : null, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday get worker failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/hire/route.ts b/apps/sim/app/api/tools/workday/hire/route.ts new file mode 100644 index 00000000000..e0a89861363 --- /dev/null +++ b/apps/sim/app/api/tools/workday/hire/route.ts @@ -0,0 +1,81 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayHireAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + preHireId: z.string().min(1), + positionId: z.string().min(1), + hireDate: z.string().min(1), + jobProfileId: z.string().optional(), + locationId: z.string().optional(), + managerId: z.string().optional(), + employeeType: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'staffing', + data.username, + data.password + ) + + const [result] = await client.Hire_EmployeeAsync({ + Business_Process_Parameters: { + Auto_Complete: true, + Run_Now: true, + }, + Hire_Employee_Data: { + Applicant_Reference: wdRef('Applicant_ID', data.preHireId), + Position_Reference: wdRef('Position_ID', data.positionId), + Hire_Date: data.hireDate, + Hire_Employee_Event_Data: { + Employee_Type_Reference: wdRef('Employee_Type_ID', data.employeeType ?? 'Regular'), + First_Day_of_Work: data.hireDate, + }, + }, + }) + + const employeeRef = result?.Employee_Reference + const eventRef = result?.Event_Reference + + return NextResponse.json({ + success: true, + output: { + workerId: extractRefId(employeeRef), + employeeId: extractRefId(employeeRef), + eventId: extractRefId(eventRef), + hireDate: data.hireDate, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday hire employee failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/list-workers/route.ts b/apps/sim/app/api/tools/workday/list-workers/route.ts new file mode 100644 index 00000000000..aab3e1a1af8 --- /dev/null +++ b/apps/sim/app/api/tools/workday/list-workers/route.ts @@ -0,0 +1,81 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayListWorkersAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + search: z.string().optional(), + limit: z.number().optional(), + offset: z.number().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const limit = data.limit ?? 20 + const offset = data.offset ?? 0 + const page = offset > 0 ? Math.floor(offset / limit) + 1 : 1 + + const [result] = await client.Get_WorkersAsync({ + Response_Filter: { Page: page, Count: limit }, + Response_Group: { + Include_Reference: true, + Include_Personal_Information: true, + Include_Employment_Information: true, + }, + }) + + const rawWorkers = result?.Response_Data?.Worker + const workersArray = Array.isArray(rawWorkers) ? rawWorkers : rawWorkers ? [rawWorkers] : [] + + const workers = workersArray.map((w: Record) => { + const workerData = w.Worker_Data as Record | undefined + return { + id: extractRefId(w.Worker_Reference as Record) ?? null, + descriptor: w.Worker_Descriptor ?? null, + personalData: workerData?.Personal_Data ?? null, + employmentData: workerData?.Employment_Data ?? null, + } + }) + + const total = result?.Response_Results?.Total_Results ?? workers.length + + return NextResponse.json({ + success: true, + output: { workers, total }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday list workers failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/terminate/route.ts b/apps/sim/app/api/tools/workday/terminate/route.ts new file mode 100644 index 00000000000..8484d781a03 --- /dev/null +++ b/apps/sim/app/api/tools/workday/terminate/route.ts @@ -0,0 +1,77 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayTerminateAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), + terminationDate: z.string().min(1), + reason: z.string().min(1), + notificationDate: z.string().optional(), + lastDayOfWork: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'staffing', + data.username, + data.password + ) + + const [result] = await client.Terminate_EmployeeAsync({ + Business_Process_Parameters: { + Auto_Complete: true, + Run_Now: true, + }, + Terminate_Employee_Data: { + Employee_Reference: wdRef('Employee_ID', data.workerId), + Termination_Date: data.terminationDate, + Terminate_Event_Data: { + Primary_Reason_Reference: wdRef('Termination_Subcategory_ID', data.reason), + Last_Day_of_Work: data.lastDayOfWork ?? data.terminationDate, + Notification_Date: data.notificationDate ?? data.terminationDate, + }, + }, + }) + + const eventRef = result?.Event_Reference + + return NextResponse.json({ + success: true, + output: { + eventId: extractRefId(eventRef), + workerId: data.workerId, + terminationDate: data.terminationDate, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday terminate employee failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/workday/update-worker/route.ts b/apps/sim/app/api/tools/workday/update-worker/route.ts new file mode 100644 index 00000000000..54677e59847 --- /dev/null +++ b/apps/sim/app/api/tools/workday/update-worker/route.ts @@ -0,0 +1,68 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { createWorkdaySoapClient, extractRefId, wdRef } from '@/tools/workday/soap' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkdayUpdateWorkerAPI') + +const RequestSchema = z.object({ + tenantUrl: z.string().min(1), + tenant: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + workerId: z.string().min(1), + fields: z.record(z.unknown()), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const data = RequestSchema.parse(body) + + const client = await createWorkdaySoapClient( + data.tenantUrl, + data.tenant, + 'humanResources', + data.username, + data.password + ) + + const [result] = await client.Change_Personal_InformationAsync({ + Business_Process_Parameters: { + Auto_Complete: true, + Run_Now: true, + }, + Change_Personal_Information_Data: { + Worker_Reference: wdRef('Employee_ID', data.workerId), + Personal_Information_Data: data.fields, + }, + }) + + const eventRef = result?.Event_Reference + + return NextResponse.json({ + success: true, + output: { + eventId: extractRefId(eventRef), + workerId: data.workerId, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Workday update worker failed`, { error }) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/blocks/blocks/workday.ts b/apps/sim/blocks/blocks/workday.ts index 8e14f527eb9..be2b09312f7 100644 --- a/apps/sim/blocks/blocks/workday.ts +++ b/apps/sim/blocks/blocks/workday.ts @@ -34,9 +34,9 @@ export const WorkdayBlock: BlockConfig = { id: 'tenantUrl', title: 'Tenant URL', type: 'short-input', - placeholder: 'https://wd5-impl-services1.workday.com', + placeholder: 'https://wd2-impl-services1.workday.com', required: true, - description: 'Your Workday instance URL', + description: 'Your Workday instance URL (e.g., https://wd2-impl-services1.workday.com)', }, { id: 'tenant', @@ -269,6 +269,14 @@ Output: {"businessTitle": "Senior Engineer"}`, condition: { field: 'operation', value: 'assign_onboarding' }, required: { field: 'operation', value: 'assign_onboarding' }, }, + { + id: 'actionEventId', + title: 'Action Event ID', + type: 'short-input', + placeholder: 'Hiring event ID that enables onboarding', + condition: { field: 'operation', value: 'assign_onboarding' }, + required: { field: 'operation', value: 'assign_onboarding' }, + }, { id: 'stages', title: 'Stage IDs (JSON)', @@ -314,7 +322,7 @@ Output: {"businessTitle": "Senior Engineer"}`, type: 'short-input', placeholder: 'e.g., Promotion, Transfer', condition: { field: 'operation', value: ['change_job', 'terminate_worker'] }, - required: { field: 'operation', value: 'terminate_worker' }, + required: { field: 'operation', value: ['change_job', 'terminate_worker'] }, }, // Terminate Worker @@ -411,6 +419,7 @@ Output: {"businessTitle": "Senior Engineer"}`, employeeType: { type: 'string', description: 'Employee type' }, fields: { type: 'json', description: 'Fields to update' }, onboardingPlanId: { type: 'string', description: 'Onboarding plan ID' }, + actionEventId: { type: 'string', description: 'Action event ID for onboarding' }, stages: { type: 'string', description: 'Onboarding stage IDs' }, orgType: { type: 'string', description: 'Organization type filter' }, effectiveDate: { type: 'string', description: 'Effective date (YYYY-MM-DD)' }, diff --git a/apps/sim/package.json b/apps/sim/package.json index 8e9fee648e5..6148884846a 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -159,6 +159,7 @@ "resend": "^4.1.2", "rss-parser": "3.13.0", "sharp": "0.34.3", + "soap": "1.8.0", "socket.io": "^4.8.1", "socket.io-client": "4.8.1", "ssh2": "^1.17.0", diff --git a/apps/sim/tools/workday/assign_onboarding.ts b/apps/sim/tools/workday/assign_onboarding.ts index 96fb69d8952..3f43f5a8885 100644 --- a/apps/sim/tools/workday/assign_onboarding.ts +++ b/apps/sim/tools/workday/assign_onboarding.ts @@ -1,12 +1,8 @@ -import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { WorkdayAssignOnboardingParams, WorkdayAssignOnboardingResponse, } from '@/tools/workday/types' -import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' - -const logger = createLogger('WorkdayAssignOnboardingTool') export const assignOnboardingTool: ToolConfig< WorkdayAssignOnboardingParams, @@ -55,6 +51,12 @@ export const assignOnboardingTool: ToolConfig< 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)', + }, stages: { type: 'string', required: false, @@ -65,58 +67,20 @@ export const assignOnboardingTool: ToolConfig< }, request: { - url: (params) => { - const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) - return `${baseUrl}/workers/${params.workerId}/onboardingPlanAssignments` - }, + url: '/api/tools/workday/assign-onboarding', method: 'POST', - headers: (params) => ({ - Authorization: createWorkdayAuthHeader(params.username, params.password), + headers: () => ({ 'Content-Type': 'application/json', - Accept: 'application/json', }), - body: (params) => { - const body: Record = { - onboardingPlan: { id: params.onboardingPlanId }, - } - - if (params.stages) { - try { - const parsedStages = - typeof params.stages === 'string' ? JSON.parse(params.stages) : params.stages - body.stages = Array.isArray(parsedStages) - ? parsedStages.map((s: string) => ({ id: s })) - : [] - } catch { - body.stages = [] - } - } - - return body - }, + body: (params) => params, }, transformResponse: async (response: Response) => { - try { - const data = await response.json() - - if (!response.ok) { - const error = data.error ?? data.errors?.[0]?.error ?? data - throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) - } - - return { - success: true, - output: { - assignmentId: data.id ?? null, - workerId: data.worker?.id ?? null, - planId: data.onboardingPlan?.id ?? null, - }, - } - } catch (error) { - logger.error('Workday assign onboarding - Error processing response:', { error }) - throw error + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') } + return data }, outputs: { diff --git a/apps/sim/tools/workday/change_job.ts b/apps/sim/tools/workday/change_job.ts index 609e2e37492..b7088831b16 100644 --- a/apps/sim/tools/workday/change_job.ts +++ b/apps/sim/tools/workday/change_job.ts @@ -1,9 +1,5 @@ -import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { WorkdayChangeJobParams, WorkdayChangeJobResponse } from '@/tools/workday/types' -import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' - -const logger = createLogger('WorkdayChangeJobTool') export const changeJobTool: ToolConfig = { id: 'workday_change_job', @@ -75,59 +71,27 @@ export const changeJobTool: ToolConfig { - const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) - return `${baseUrl}/workers/${params.workerId}/jobChanges` - }, + url: '/api/tools/workday/change-job', method: 'POST', - headers: (params) => ({ - Authorization: createWorkdayAuthHeader(params.username, params.password), + headers: () => ({ 'Content-Type': 'application/json', - Accept: 'application/json', }), - body: (params) => { - const body: Record = { - effectiveDate: params.effectiveDate, - } - - if (params.newPositionId) body.position = { id: params.newPositionId } - if (params.newJobProfileId) body.jobProfile = { id: params.newJobProfileId } - if (params.newLocationId) body.location = { id: params.newLocationId } - if (params.newManagerId) body.manager = { id: params.newManagerId } - if (params.reason) body.reason = params.reason - - return body - }, + body: (params) => params, }, transformResponse: async (response: Response) => { - try { - const data = await response.json() - - if (!response.ok) { - const error = data.error ?? data.errors?.[0]?.error ?? data - throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) - } - - return { - success: true, - output: { - eventId: data.id ?? null, - workerId: data.worker?.id ?? null, - effectiveDate: data.effectiveDate ?? null, - }, - } - } catch (error) { - logger.error('Workday change job - Error processing response:', { error }) - throw error + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') } + return data }, outputs: { diff --git a/apps/sim/tools/workday/create_prehire.ts b/apps/sim/tools/workday/create_prehire.ts index ee36f0653d5..a3e55b18718 100644 --- a/apps/sim/tools/workday/create_prehire.ts +++ b/apps/sim/tools/workday/create_prehire.ts @@ -1,12 +1,8 @@ -import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { WorkdayCreatePrehireParams, WorkdayCreatePrehireResponse, } from '@/tools/workday/types' -import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' - -const logger = createLogger('WorkdayCreatePrehireTool') export const createPrehireTool: ToolConfig< WorkdayCreatePrehireParams, @@ -76,65 +72,20 @@ export const createPrehireTool: ToolConfig< }, request: { - url: (params) => { - const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) - return `${baseUrl}/preHires` - }, + url: '/api/tools/workday/create-prehire', method: 'POST', - headers: (params) => ({ - Authorization: createWorkdayAuthHeader(params.username, params.password), + headers: () => ({ 'Content-Type': 'application/json', - Accept: 'application/json', }), - body: (params) => { - const nameParts = params.legalName.trim().split(/\s+/) - const firstName = nameParts[0] - const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : '' - - const body: Record = { - name: { - firstName, - lastName, - }, - } - - if (params.email) { - body.email = { emailAddress: params.email, usageType: 'WORK' } - } - if (params.phoneNumber) { - body.phone = { phoneNumber: params.phoneNumber, usageType: 'WORK' } - } - if (params.address) { - body.address = { formattedAddress: params.address } - } - if (params.sourceId) { - body.source = { id: params.sourceId } - } - - return body - }, + body: (params) => params, }, transformResponse: async (response: Response) => { - try { - const data = await response.json() - - if (!response.ok) { - const error = data.error ?? data.errors?.[0]?.error ?? data - throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) - } - - return { - success: true, - output: { - preHireId: data.id ?? null, - descriptor: data.descriptor ?? null, - }, - } - } catch (error) { - logger.error('Workday create pre-hire - Error processing response:', { error }) - throw error + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') } + return data }, outputs: { diff --git a/apps/sim/tools/workday/get_compensation.ts b/apps/sim/tools/workday/get_compensation.ts index 7750c735334..800d14fbf92 100644 --- a/apps/sim/tools/workday/get_compensation.ts +++ b/apps/sim/tools/workday/get_compensation.ts @@ -1,12 +1,8 @@ -import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { WorkdayGetCompensationParams, WorkdayGetCompensationResponse, } from '@/tools/workday/types' -import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' - -const logger = createLogger('WorkdayGetCompensationTool') export const getCompensationTool: ToolConfig< WorkdayGetCompensationParams, @@ -30,17 +26,11 @@ export const getCompensationTool: ToolConfig< visibility: 'user-only', description: 'Workday tenant name', }, - username: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Integration System User username', - }, - password: { + accessToken: { type: 'string', required: true, - visibility: 'user-only', - description: 'Integration System User password', + visibility: 'hidden', + description: 'OAuth 2.0 access token for Workday REST API', }, workerId: { type: 'string', @@ -51,45 +41,20 @@ export const getCompensationTool: ToolConfig< }, request: { - url: (params) => { - const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) - return `${baseUrl}/workers/${params.workerId}/compensationPlans` - }, - method: 'GET', - headers: (params) => ({ - Authorization: createWorkdayAuthHeader(params.username, params.password), - Accept: 'application/json', + url: '/api/tools/workday/get-compensation', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', }), + body: (params) => params, }, transformResponse: async (response: Response) => { - try { - const data = await response.json() - - if (!response.ok) { - const error = data.error ?? data.errors?.[0]?.error ?? data - throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) - } - - const plans = Array.isArray(data.data) ? data.data : (data.compensationPlans ?? []) - - return { - success: true, - output: { - compensationPlans: plans.map((p: Record) => ({ - id: p.id ?? null, - planName: - (p.compensationPlan as Record)?.descriptor ?? p.planName ?? null, - amount: p.amount ?? p.compensationPlanAmount ?? null, - currency: (p.currency as Record)?.descriptor ?? p.currency ?? null, - frequency: (p.frequency as Record)?.descriptor ?? p.frequency ?? null, - })), - }, - } - } catch (error) { - logger.error('Workday get compensation - Error processing response:', { error }) - throw error + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') } + return data }, outputs: { diff --git a/apps/sim/tools/workday/get_organizations.ts b/apps/sim/tools/workday/get_organizations.ts index a2caffb8264..35953883a2c 100644 --- a/apps/sim/tools/workday/get_organizations.ts +++ b/apps/sim/tools/workday/get_organizations.ts @@ -1,12 +1,8 @@ -import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { WorkdayGetOrganizationsParams, WorkdayGetOrganizationsResponse, } from '@/tools/workday/types' -import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' - -const logger = createLogger('WorkdayGetOrganizationsTool') export const getOrganizationsTool: ToolConfig< WorkdayGetOrganizationsParams, @@ -30,17 +26,11 @@ export const getOrganizationsTool: ToolConfig< visibility: 'user-only', description: 'Workday tenant name', }, - username: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Integration System User username', - }, - password: { + accessToken: { type: 'string', required: true, - visibility: 'user-only', - description: 'Integration System User password', + visibility: 'hidden', + description: 'OAuth 2.0 access token for Workday REST API', }, type: { type: 'string', @@ -63,52 +53,20 @@ export const getOrganizationsTool: ToolConfig< }, request: { - url: (params) => { - const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) - const queryParams = new URLSearchParams() - - if (params.type) queryParams.append('type', params.type) - if (params.limit) queryParams.append('limit', params.limit.toString()) - if (params.offset) queryParams.append('offset', params.offset.toString()) - - const queryString = queryParams.toString() - return queryString ? `${baseUrl}/organizations?${queryString}` : `${baseUrl}/organizations` - }, - method: 'GET', - headers: (params) => ({ - Authorization: createWorkdayAuthHeader(params.username, params.password), - Accept: 'application/json', + url: '/api/tools/workday/get-organizations', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', }), + body: (params) => params, }, transformResponse: async (response: Response) => { - try { - const data = await response.json() - - if (!response.ok) { - const error = data.error ?? data.errors?.[0]?.error ?? data - throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) - } - - const organizations = Array.isArray(data.data) ? data.data : (data.organizations ?? []) - - return { - success: true, - output: { - organizations: organizations.map((o: Record) => ({ - id: o.id ?? null, - descriptor: o.descriptor ?? null, - type: (o.type as Record)?.descriptor ?? o.type ?? null, - subtype: (o.subtype as Record)?.descriptor ?? o.subtype ?? null, - isActive: o.isActive ?? null, - })), - total: data.total ?? organizations.length, - }, - } - } catch (error) { - logger.error('Workday get organizations - Error processing response:', { error }) - throw error + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') } + return data }, outputs: { diff --git a/apps/sim/tools/workday/get_worker.ts b/apps/sim/tools/workday/get_worker.ts index 795cb5f62a4..753eff0f9b2 100644 --- a/apps/sim/tools/workday/get_worker.ts +++ b/apps/sim/tools/workday/get_worker.ts @@ -1,9 +1,5 @@ -import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { WorkdayGetWorkerParams, WorkdayGetWorkerResponse } from '@/tools/workday/types' -import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' - -const logger = createLogger('WorkdayGetWorkerTool') export const getWorkerTool: ToolConfig = { id: 'workday_get_worker', @@ -46,47 +42,20 @@ export const getWorkerTool: ToolConfig { - const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) - return `${baseUrl}/workers/${params.workerId}` - }, - method: 'GET', - headers: (params) => ({ - Authorization: createWorkdayAuthHeader(params.username, params.password), - Accept: 'application/json', + url: '/api/tools/workday/get-worker', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', }), + body: (params) => params, }, transformResponse: async (response: Response) => { - try { - const data = await response.json() - - if (!response.ok) { - const error = data.error ?? data.errors?.[0]?.error ?? data - throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) - } - - return { - success: true, - output: { - worker: { - id: data.id ?? null, - descriptor: data.descriptor ?? null, - primaryWorkEmail: data.primaryWorkEmail ?? null, - primaryWorkPhone: data.primaryWorkPhone ?? null, - businessTitle: data.businessTitle ?? null, - supervisoryOrganization: data.supervisoryOrganization?.descriptor ?? null, - hireDate: data.hireDate ?? null, - workerType: data.workerType?.descriptor ?? null, - isActive: data.isActive ?? null, - ...data, - }, - }, - } - } catch (error) { - logger.error('Workday get worker - Error processing response:', { error }) - throw error + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') } + return data }, outputs: { diff --git a/apps/sim/tools/workday/hire_employee.ts b/apps/sim/tools/workday/hire_employee.ts index 4cdc7b0dbeb..6409276ecea 100644 --- a/apps/sim/tools/workday/hire_employee.ts +++ b/apps/sim/tools/workday/hire_employee.ts @@ -1,9 +1,5 @@ -import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { WorkdayHireEmployeeParams, WorkdayHireEmployeeResponse } from '@/tools/workday/types' -import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' - -const logger = createLogger('WorkdayHireEmployeeTool') export const hireEmployeeTool: ToolConfig = { @@ -83,55 +79,20 @@ export const hireEmployeeTool: ToolConfig { - const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) - return `${baseUrl}/staffingEvents` - }, + url: '/api/tools/workday/hire', method: 'POST', - headers: (params) => ({ - Authorization: createWorkdayAuthHeader(params.username, params.password), + headers: () => ({ 'Content-Type': 'application/json', - Accept: 'application/json', }), - body: (params) => { - const body: Record = { - type: 'HIRE', - preHire: { id: params.preHireId }, - position: { id: params.positionId }, - hireDate: params.hireDate, - } - - if (params.jobProfileId) body.jobProfile = { id: params.jobProfileId } - if (params.locationId) body.location = { id: params.locationId } - if (params.managerId) body.manager = { id: params.managerId } - if (params.employeeType) body.employeeType = params.employeeType - - return body - }, + body: (params) => params, }, transformResponse: async (response: Response) => { - try { - const data = await response.json() - - if (!response.ok) { - const error = data.error ?? data.errors?.[0]?.error ?? data - throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) - } - - return { - success: true, - output: { - workerId: data.worker?.id ?? data.workerId ?? null, - employeeId: data.employeeId ?? data.id ?? null, - descriptor: data.descriptor ?? data.worker?.descriptor ?? null, - hireDate: data.hireDate ?? null, - }, - } - } catch (error) { - logger.error('Workday hire employee - Error processing response:', { error }) - throw error + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') } + return data }, outputs: { diff --git a/apps/sim/tools/workday/list_workers.ts b/apps/sim/tools/workday/list_workers.ts index 58d238cbeef..ed5712125ed 100644 --- a/apps/sim/tools/workday/list_workers.ts +++ b/apps/sim/tools/workday/list_workers.ts @@ -1,9 +1,5 @@ -import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { WorkdayListWorkersParams, WorkdayListWorkersResponse } from '@/tools/workday/types' -import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' - -const logger = createLogger('WorkdayListWorkersTool') export const listWorkersTool: ToolConfig = { id: 'workday_list_workers', @@ -57,57 +53,20 @@ export const listWorkersTool: ToolConfig { - const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) - const queryParams = new URLSearchParams() - - if (params.search) queryParams.append('search', params.search) - if (params.limit) queryParams.append('limit', params.limit.toString()) - if (params.offset) queryParams.append('offset', params.offset.toString()) - - const queryString = queryParams.toString() - return queryString ? `${baseUrl}/workers?${queryString}` : `${baseUrl}/workers` - }, - method: 'GET', - headers: (params) => ({ - Authorization: createWorkdayAuthHeader(params.username, params.password), - Accept: 'application/json', + url: '/api/tools/workday/list-workers', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', }), + body: (params) => params, }, transformResponse: async (response: Response) => { - try { - const data = await response.json() - - if (!response.ok) { - const error = data.error ?? data.errors?.[0]?.error ?? data - throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) - } - - const workers = Array.isArray(data.data) ? data.data : (data.workers ?? []) - - return { - success: true, - output: { - workers: workers.map((w: Record) => ({ - id: w.id ?? null, - descriptor: w.descriptor ?? null, - primaryWorkEmail: w.primaryWorkEmail ?? null, - primaryWorkPhone: w.primaryWorkPhone ?? null, - businessTitle: w.businessTitle ?? null, - supervisoryOrganization: - (w.supervisoryOrganization as Record)?.descriptor ?? null, - hireDate: w.hireDate ?? null, - workerType: (w.workerType as Record)?.descriptor ?? null, - isActive: w.isActive ?? null, - })), - total: data.total ?? workers.length, - }, - } - } catch (error) { - logger.error('Workday list workers - Error processing response:', { error }) - throw error + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') } + return data }, outputs: { diff --git a/apps/sim/tools/workday/soap.ts b/apps/sim/tools/workday/soap.ts new file mode 100644 index 00000000000..2effe3ebba0 --- /dev/null +++ b/apps/sim/tools/workday/soap.ts @@ -0,0 +1,117 @@ +import { createLogger } from '@sim/logger' +import * as soap from 'soap' + +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 + +export interface WorkdaySoapResult { + Response_Data?: Record + Response_Results?: { + Total_Results?: number + Total_Pages?: number + Page_Results?: number + Page?: number + } + Event_Reference?: WorkdayReference + Employee_Reference?: WorkdayReference + Position_Reference?: WorkdayReference + Applicant_Reference?: WorkdayReference & { attributes?: { Descriptor?: string } } + Onboarding_Plan_Assignment_Reference?: WorkdayReference + Exceptions_Response_Data?: unknown +} + +export interface WorkdayReference { + ID?: WorkdayIdEntry[] | WorkdayIdEntry + attributes?: Record +} + +interface WorkdayIdEntry { + $value?: string + _?: string + attributes?: Record +} + +type SoapOperationFn = ( + args: Record +) => Promise<[WorkdaySoapResult, string, Record, string]> + +export interface WorkdayClient extends soap.Client { + 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 WSDL URL for a Workday SOAP service. + * Pattern: {tenantUrl}/ccx/service/{tenant}/{serviceName}/{version}?wsdl + */ +export function buildWsdlUrl( + tenantUrl: string, + tenant: string, + service: WorkdayServiceKey +): string { + const svc = WORKDAY_SERVICES[service] + const baseUrl = tenantUrl.replace(/\/$/, '') + return `${baseUrl}/ccx/service/${tenant}/${svc.name}/${svc.version}?wsdl` +} + +/** + * Creates a typed SOAP client for a Workday service. + * Uses the `soap` npm package to parse the WSDL and auto-marshall JSON to XML. + */ +export async function createWorkdaySoapClient( + tenantUrl: string, + tenant: string, + service: WorkdayServiceKey, + username: string, + password: string +): Promise { + const wsdlUrl = buildWsdlUrl(tenantUrl, tenant, service) + logger.info('Creating Workday SOAP client', { service, wsdlUrl }) + + const client = await soap.createClientAsync(wsdlUrl) + client.setSecurity(new soap.BasicAuthSecurity(username, password)) + return client as WorkdayClient +} + +/** + * Builds a Workday object reference in the format the SOAP API expects. + * Generates: { ID: { attributes: { 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 +} diff --git a/apps/sim/tools/workday/terminate_worker.ts b/apps/sim/tools/workday/terminate_worker.ts index 939eac62111..fe841b71209 100644 --- a/apps/sim/tools/workday/terminate_worker.ts +++ b/apps/sim/tools/workday/terminate_worker.ts @@ -1,12 +1,8 @@ -import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { WorkdayTerminateWorkerParams, WorkdayTerminateWorkerResponse, } from '@/tools/workday/types' -import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' - -const logger = createLogger('WorkdayTerminateWorkerTool') export const terminateWorkerTool: ToolConfig< WorkdayTerminateWorkerParams, @@ -31,17 +27,11 @@ export const terminateWorkerTool: ToolConfig< visibility: 'user-only', description: 'Workday tenant name', }, - username: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Integration System User username', - }, - password: { + accessToken: { type: 'string', required: true, - visibility: 'user-only', - description: 'Integration System User password', + visibility: 'hidden', + description: 'OAuth 2.0 access token for Workday REST API', }, workerId: { type: 'string', @@ -76,50 +66,20 @@ export const terminateWorkerTool: ToolConfig< }, request: { - url: (params) => { - const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) - return `${baseUrl}/workers/${params.workerId}/terminations` - }, + url: '/api/tools/workday/terminate', method: 'POST', - headers: (params) => ({ - Authorization: createWorkdayAuthHeader(params.username, params.password), + headers: () => ({ 'Content-Type': 'application/json', - Accept: 'application/json', }), - body: (params) => { - const body: Record = { - terminationDate: params.terminationDate, - reason: params.reason, - } - - if (params.notificationDate) body.notificationDate = params.notificationDate - if (params.lastDayOfWork) body.lastDayOfWork = params.lastDayOfWork - - return body - }, + body: (params) => params, }, transformResponse: async (response: Response) => { - try { - const data = await response.json() - - if (!response.ok) { - const error = data.error ?? data.errors?.[0]?.error ?? data - throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) - } - - return { - success: true, - output: { - eventId: data.id ?? null, - workerId: data.worker?.id ?? null, - terminationDate: data.terminationDate ?? null, - }, - } - } catch (error) { - logger.error('Workday terminate worker - Error processing response:', { error }) - throw error + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') } + return data }, outputs: { diff --git a/apps/sim/tools/workday/types.ts b/apps/sim/tools/workday/types.ts index 42aba56147b..3df41b472ea 100644 --- a/apps/sim/tools/workday/types.ts +++ b/apps/sim/tools/workday/types.ts @@ -106,6 +106,7 @@ export interface WorkdayUpdateWorkerResponse extends ToolResponse { export interface WorkdayAssignOnboardingParams extends WorkdayBaseParams { workerId: string onboardingPlanId: string + actionEventId: string stages?: string } @@ -139,7 +140,7 @@ export interface WorkdayChangeJobParams extends WorkdayBaseParams { newJobProfileId?: string newLocationId?: string newManagerId?: string - reason?: string + reason: string } export interface WorkdayChangeJobResponse extends ToolResponse { diff --git a/apps/sim/tools/workday/update_worker.ts b/apps/sim/tools/workday/update_worker.ts index 9fe7ed929d5..f18e85b3a81 100644 --- a/apps/sim/tools/workday/update_worker.ts +++ b/apps/sim/tools/workday/update_worker.ts @@ -1,9 +1,5 @@ -import { createLogger } from '@sim/logger' import type { ToolConfig } from '@/tools/types' import type { WorkdayUpdateWorkerParams, WorkdayUpdateWorkerResponse } from '@/tools/workday/types' -import { buildWorkdayBaseUrl, createWorkdayAuthHeader } from '@/tools/workday/utils' - -const logger = createLogger('WorkdayUpdateWorkerTool') export const updateWorkerTool: ToolConfig = { @@ -53,54 +49,20 @@ export const updateWorkerTool: ToolConfig { - const baseUrl = buildWorkdayBaseUrl(params.tenantUrl, params.tenant) - return `${baseUrl}/workers/${params.workerId}` - }, - method: 'PATCH', - headers: (params) => ({ - Authorization: createWorkdayAuthHeader(params.username, params.password), + url: '/api/tools/workday/update-worker', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json', - Accept: 'application/json', }), - body: (params) => { - if (!params.fields || typeof params.fields !== 'object') { - throw new Error('Fields must be a JSON object') - } - return params.fields - }, + body: (params) => params, }, transformResponse: async (response: Response) => { - try { - const data = await response.json() - - if (!response.ok) { - const error = data.error ?? data.errors?.[0]?.error ?? data - throw new Error(typeof error === 'string' ? error : JSON.stringify(error)) - } - - return { - success: true, - output: { - worker: { - id: data.id ?? null, - descriptor: data.descriptor ?? null, - primaryWorkEmail: data.primaryWorkEmail ?? null, - primaryWorkPhone: data.primaryWorkPhone ?? null, - businessTitle: data.businessTitle ?? null, - supervisoryOrganization: data.supervisoryOrganization?.descriptor ?? null, - hireDate: data.hireDate ?? null, - workerType: data.workerType?.descriptor ?? null, - isActive: data.isActive ?? null, - ...data, - }, - }, - } - } catch (error) { - logger.error('Workday update worker - Error processing response:', { error }) - throw error + const data = await response.json() + if (!response.ok) { + throw new Error(data.error ?? 'Workday API request failed') } + return data }, outputs: { diff --git a/apps/sim/tools/workday/utils.ts b/apps/sim/tools/workday/utils.ts index 33a0f10c95f..b8c12ab9a0a 100644 --- a/apps/sim/tools/workday/utils.ts +++ b/apps/sim/tools/workday/utils.ts @@ -10,12 +10,31 @@ export function createWorkdayAuthHeader(username: string, password: string): str } /** - * Builds a Workday REST API base URL from tenant URL and tenant name. - * @param tenantUrl The Workday instance URL (e.g., https://wd5-impl-services1.workday.com) + * Builds a Workday REST API base URL. + * REST pattern: {tenantUrl}/api/v1/{tenant} + * @param tenantUrl The Workday instance URL (e.g., https://wd2-impl-services1.workday.com) * @param tenant The tenant name - * @returns Formatted base URL for API calls */ -export function buildWorkdayBaseUrl(tenantUrl: string, tenant: string): string { +export function buildWorkdayRestUrl(tenantUrl: string, tenant: string): string { const baseUrl = tenantUrl.replace(/\/$/, '') - return `${baseUrl}/ccx/api/v1/${tenant}` + return `${baseUrl}/api/v1/${tenant}` +} + +/** + * Builds a Workday SOAP/WS API base URL. + * SOAP pattern: {tenantUrl}/ccx/service/{tenant}/{serviceName}/{version} + * Used for operations not available via REST (hire, terminate, etc.). + * @param tenantUrl The Workday instance URL + * @param tenant The tenant name + * @param serviceName The WS service name (e.g., Staffing, Human_Resources) + * @param version The API version (e.g., v42.0, v45.0) + */ +export function buildWorkdaySoapUrl( + tenantUrl: string, + tenant: string, + serviceName: string, + version: string +): string { + const baseUrl = tenantUrl.replace(/\/$/, '') + return `${baseUrl}/ccx/service/${tenant}/${serviceName}/${version}` } diff --git a/bun.lock b/bun.lock index 61df0c93763..9e3efc4d447 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -185,6 +184,7 @@ "resend": "^4.1.2", "rss-parser": "3.13.0", "sharp": "0.34.3", + "soap": "1.8.0", "socket.io": "^4.8.1", "socket.io-client": "4.8.1", "ssh2": "^1.17.0", @@ -952,6 +952,8 @@ "@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="], + "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="], + "@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="], "@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="], @@ -1690,6 +1692,8 @@ "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], "asn1js": ["asn1js@3.0.7", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ=="], @@ -1712,7 +1716,9 @@ "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], - "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], + "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], + + "axios-ntlm": ["axios-ntlm@1.4.6", "", { "dependencies": { "axios": "^1.12.2", "des.js": "^1.1.0", "dev-null": "^0.1.1", "js-md4": "^0.3.2" } }, "sha512-4nR5cbVEBfPMTFkd77FEDpDuaR205JKibmrkaQyNwGcCx0szWNpRZaL0jZyMx4+mVY2PXHjRHuJafv9Oipl0Kg=="], "b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="], @@ -2020,6 +2026,8 @@ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], @@ -2028,10 +2036,14 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "dev-null": ["dev-null@0.1.1", "", {}, "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "devtools-protocol": ["devtools-protocol@0.0.1464554", "", {}, "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw=="], + "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="], + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], "dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="], @@ -2252,6 +2264,8 @@ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="], @@ -2500,6 +2514,8 @@ "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], + "js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="], "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], @@ -2786,6 +2802,8 @@ "minimal-polyfills": ["minimal-polyfills@2.2.3", "", {}, "sha512-oxdmJ9cL+xV72h0xYxp4tP2d5/fTBpP45H8DIOn9pASuF8a3IYTf+25fMGDYGiWW+MFsuog6KD6nfmhZJQ+uUw=="], + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + "minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -3242,7 +3260,7 @@ "satori": ["satori@0.12.2", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-gradient-parser": "^0.0.16", "css-to-react-native": "^3.0.0", "emoji-regex": "^10.2.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-wasm-web": "^0.3.3" } }, "sha512-3C/laIeE6UUe9A+iQ0A48ywPVCCMKCNSTU5Os101Vhgsjd3AAxGNjyq0uAA8kulMPK5n0csn8JlxPN9riXEjLA=="], - "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], @@ -3318,6 +3336,8 @@ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + "soap": ["soap@1.8.0", "", { "dependencies": { "axios": "^1.13.6", "axios-ntlm": "^1.4.6", "debug": "^4.4.3", "follow-redirects": "^1.15.11", "formidable": "^3.5.4", "sax": "^1.5.0", "whatwg-mimetype": "4.0.0", "xml-crypto": "^6.1.2" } }, "sha512-WRIzZm4M13a9j1t8yMdZZtbbkxNatXAhvtO8UXc/LvdfZ/Op1MqZS6qsAbILLsLTk3oLM/PRw0XOG0U53dAZzg=="], + "socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="], "socket.io-adapter": ["socket.io-adapter@2.5.6", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" } }, "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ=="], @@ -3914,6 +3934,8 @@ "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], + "@paralleldrive/cuid2/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@puppeteer/browsers/tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="], "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -4326,6 +4348,8 @@ "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "twilio/axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], + "twilio/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "twilio/xmlbuilder": ["xmlbuilder@13.0.2", "", {}, "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ=="], @@ -4344,6 +4368,10 @@ "xml-crypto/xpath": ["xpath@0.0.33", "", {}, "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA=="], + "xml-js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + + "xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], From 8120712eb02e6cb6eff07acda33ce15822cf4df1 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 18 Mar 2026 20:48:54 -0700 Subject: [PATCH 04/13] fix SOAP API --- .../tools/workday/assign-onboarding/route.ts | 3 +- .../api/tools/workday/create-prehire/route.ts | 1 - apps/sim/app/api/tools/workday/hire/route.ts | 3 -- .../api/tools/workday/list-workers/route.ts | 1 - .../api/tools/workday/update-worker/route.ts | 6 ++-- apps/sim/blocks/blocks/workday.ts | 36 +++---------------- apps/sim/tools/workday/assign_onboarding.ts | 7 ---- apps/sim/tools/workday/create_prehire.ts | 6 ---- apps/sim/tools/workday/get_compensation.ts | 12 +++++-- apps/sim/tools/workday/get_organizations.ts | 12 +++++-- apps/sim/tools/workday/hire_employee.ts | 22 ++---------- apps/sim/tools/workday/list_workers.ts | 6 ---- apps/sim/tools/workday/soap.ts | 1 + apps/sim/tools/workday/terminate_worker.ts | 12 +++++-- apps/sim/tools/workday/types.ts | 11 ++---- apps/sim/tools/workday/update_worker.ts | 10 ++++-- 16 files changed, 48 insertions(+), 101 deletions(-) diff --git a/apps/sim/app/api/tools/workday/assign-onboarding/route.ts b/apps/sim/app/api/tools/workday/assign-onboarding/route.ts index ed0643aabb1..c04e1c65db5 100644 --- a/apps/sim/app/api/tools/workday/assign-onboarding/route.ts +++ b/apps/sim/app/api/tools/workday/assign-onboarding/route.ts @@ -17,7 +17,6 @@ const RequestSchema = z.object({ workerId: z.string().min(1), onboardingPlanId: z.string().min(1), actionEventId: z.string().min(1), - stages: z.string().optional(), }) export async function POST(request: NextRequest) { @@ -43,7 +42,7 @@ export async function POST(request: NextRequest) { const [result] = await client.Put_Onboarding_Plan_AssignmentAsync({ Onboarding_Plan_Assignment_Data: { Onboarding_Plan_Reference: wdRef('Onboarding_Plan_ID', data.onboardingPlanId), - Person_Reference: wdRef('Employee_ID', data.workerId), + Person_Reference: wdRef('WID', data.workerId), Action_Event_Reference: wdRef('Background_Check_ID', data.actionEventId), Assignment_Effective_Moment: new Date().toISOString(), Active: true, diff --git a/apps/sim/app/api/tools/workday/create-prehire/route.ts b/apps/sim/app/api/tools/workday/create-prehire/route.ts index bc98e28786c..06ee611e124 100644 --- a/apps/sim/app/api/tools/workday/create-prehire/route.ts +++ b/apps/sim/app/api/tools/workday/create-prehire/route.ts @@ -18,7 +18,6 @@ const RequestSchema = z.object({ email: z.string().optional(), phoneNumber: z.string().optional(), address: z.string().optional(), - sourceId: z.string().optional(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/workday/hire/route.ts b/apps/sim/app/api/tools/workday/hire/route.ts index e0a89861363..1c6c8abc8b8 100644 --- a/apps/sim/app/api/tools/workday/hire/route.ts +++ b/apps/sim/app/api/tools/workday/hire/route.ts @@ -17,9 +17,6 @@ const RequestSchema = z.object({ preHireId: z.string().min(1), positionId: z.string().min(1), hireDate: z.string().min(1), - jobProfileId: z.string().optional(), - locationId: z.string().optional(), - managerId: z.string().optional(), employeeType: z.string().optional(), }) diff --git a/apps/sim/app/api/tools/workday/list-workers/route.ts b/apps/sim/app/api/tools/workday/list-workers/route.ts index aab3e1a1af8..d6a5c83e3f7 100644 --- a/apps/sim/app/api/tools/workday/list-workers/route.ts +++ b/apps/sim/app/api/tools/workday/list-workers/route.ts @@ -14,7 +14,6 @@ const RequestSchema = z.object({ tenant: z.string().min(1), username: z.string().min(1), password: z.string().min(1), - search: z.string().optional(), limit: z.number().optional(), offset: z.number().optional(), }) diff --git a/apps/sim/app/api/tools/workday/update-worker/route.ts b/apps/sim/app/api/tools/workday/update-worker/route.ts index 54677e59847..dbf2f1c5799 100644 --- a/apps/sim/app/api/tools/workday/update-worker/route.ts +++ b/apps/sim/app/api/tools/workday/update-worker/route.ts @@ -44,17 +44,15 @@ export async function POST(request: NextRequest) { Run_Now: true, }, Change_Personal_Information_Data: { - Worker_Reference: wdRef('Employee_ID', data.workerId), + Person_Reference: wdRef('Employee_ID', data.workerId), Personal_Information_Data: data.fields, }, }) - const eventRef = result?.Event_Reference - return NextResponse.json({ success: true, output: { - eventId: extractRefId(eventRef), + eventId: extractRefId(result?.Personal_Information_Change_Event_Reference), workerId: data.workerId, }, }) diff --git a/apps/sim/blocks/blocks/workday.ts b/apps/sim/blocks/blocks/workday.ts index be2b09312f7..ef251aaa8fb 100644 --- a/apps/sim/blocks/blocks/workday.ts +++ b/apps/sim/blocks/blocks/workday.ts @@ -95,13 +95,6 @@ export const WorkdayBlock: BlockConfig = { }, // List Workers - { - id: 'search', - title: 'Search', - type: 'short-input', - placeholder: 'Search by name or ID', - condition: { field: 'operation', value: 'list_workers' }, - }, { id: 'limit', title: 'Limit', @@ -151,14 +144,6 @@ export const WorkdayBlock: BlockConfig = { condition: { field: 'operation', value: 'create_prehire' }, mode: 'advanced', }, - { - id: 'sourceId', - title: 'Recruiting Source', - type: 'short-input', - placeholder: 'Source ID (e.g., referral, job board)', - condition: { field: 'operation', value: 'create_prehire' }, - mode: 'advanced', - }, // Hire Employee { @@ -175,7 +160,7 @@ export const WorkdayBlock: BlockConfig = { type: 'short-input', placeholder: 'Position to assign', condition: { field: 'operation', value: ['hire_employee', 'change_job'] }, - required: { field: 'operation', value: 'hire_employee' }, + required: { field: 'operation', value: ['hire_employee'] }, }, { id: 'hireDate', @@ -195,7 +180,7 @@ export const WorkdayBlock: BlockConfig = { title: 'Job Profile ID', type: 'short-input', placeholder: 'Job profile ID', - condition: { field: 'operation', value: ['hire_employee', 'change_job'] }, + condition: { field: 'operation', value: 'change_job' }, mode: 'advanced', }, { @@ -203,7 +188,7 @@ export const WorkdayBlock: BlockConfig = { title: 'Location ID', type: 'short-input', placeholder: 'Work location ID', - condition: { field: 'operation', value: ['hire_employee', 'change_job'] }, + condition: { field: 'operation', value: 'change_job' }, mode: 'advanced', }, { @@ -211,7 +196,7 @@ export const WorkdayBlock: BlockConfig = { title: 'Manager ID', type: 'short-input', placeholder: 'Manager worker ID', - condition: { field: 'operation', value: ['hire_employee', 'change_job'] }, + condition: { field: 'operation', value: 'change_job' }, mode: 'advanced', }, { @@ -277,14 +262,6 @@ Output: {"businessTitle": "Senior Engineer"}`, condition: { field: 'operation', value: 'assign_onboarding' }, required: { field: 'operation', value: 'assign_onboarding' }, }, - { - id: 'stages', - title: 'Stage IDs (JSON)', - type: 'short-input', - placeholder: '["stage1", "stage2"]', - condition: { field: 'operation', value: 'assign_onboarding' }, - mode: 'advanced', - }, // Get Organizations { @@ -402,14 +379,12 @@ Output: {"businessTitle": "Senior Engineer"}`, username: { type: 'string', description: 'ISU username' }, password: { type: 'string', description: 'ISU password' }, workerId: { type: 'string', description: 'Worker ID' }, - search: { type: 'string', description: 'Search term' }, limit: { type: 'number', description: 'Result limit' }, offset: { type: 'number', description: 'Pagination offset' }, legalName: { type: 'string', description: 'Legal name for pre-hire' }, email: { type: 'string', description: 'Email address' }, phoneNumber: { type: 'string', description: 'Phone number' }, address: { type: 'string', description: 'Address' }, - sourceId: { type: 'string', description: 'Recruiting source ID' }, preHireId: { type: 'string', description: 'Pre-hire record ID' }, positionId: { type: 'string', description: 'Position ID' }, hireDate: { type: 'string', description: 'Hire date (YYYY-MM-DD)' }, @@ -420,7 +395,6 @@ Output: {"businessTitle": "Senior Engineer"}`, fields: { type: 'json', description: 'Fields to update' }, onboardingPlanId: { type: 'string', description: 'Onboarding plan ID' }, actionEventId: { type: 'string', description: 'Action event ID for onboarding' }, - stages: { type: 'string', description: 'Onboarding stage IDs' }, orgType: { type: 'string', description: 'Organization type filter' }, effectiveDate: { type: 'string', description: 'Effective date (YYYY-MM-DD)' }, reason: { type: 'string', description: 'Reason for change or termination' }, @@ -433,7 +407,7 @@ Output: {"businessTitle": "Senior Engineer"}`, workers: { type: 'json', description: 'Array of worker profiles' }, total: { type: 'number', description: 'Total count of results' }, preHireId: { type: 'string', description: 'Created pre-hire ID' }, - descriptor: { type: 'string', description: 'Display name' }, + descriptor: { type: 'string', description: 'Display name of pre-hire' }, workerId: { type: 'string', description: 'Worker ID' }, employeeId: { type: 'string', description: 'Employee ID' }, hireDate: { type: 'string', description: 'Hire date' }, diff --git a/apps/sim/tools/workday/assign_onboarding.ts b/apps/sim/tools/workday/assign_onboarding.ts index 3f43f5a8885..5964a9dd566 100644 --- a/apps/sim/tools/workday/assign_onboarding.ts +++ b/apps/sim/tools/workday/assign_onboarding.ts @@ -57,13 +57,6 @@ export const assignOnboardingTool: ToolConfig< visibility: 'user-or-llm', description: 'Action event ID that enables the onboarding plan (e.g., the hiring event ID)', }, - stages: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: - 'JSON array of onboarding stage IDs to include (optional, defaults to all stages)', - }, }, request: { diff --git a/apps/sim/tools/workday/create_prehire.ts b/apps/sim/tools/workday/create_prehire.ts index a3e55b18718..95746cb9c9f 100644 --- a/apps/sim/tools/workday/create_prehire.ts +++ b/apps/sim/tools/workday/create_prehire.ts @@ -63,12 +63,6 @@ export const createPrehireTool: ToolConfig< visibility: 'user-or-llm', description: 'Address of the pre-hire', }, - sourceId: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Recruiting source ID (e.g., referral, job board)', - }, }, request: { diff --git a/apps/sim/tools/workday/get_compensation.ts b/apps/sim/tools/workday/get_compensation.ts index 800d14fbf92..9dba3db09a0 100644 --- a/apps/sim/tools/workday/get_compensation.ts +++ b/apps/sim/tools/workday/get_compensation.ts @@ -26,11 +26,17 @@ export const getCompensationTool: ToolConfig< visibility: 'user-only', description: 'Workday tenant name', }, - accessToken: { + username: { type: 'string', required: true, - visibility: 'hidden', - description: 'OAuth 2.0 access token for Workday REST API', + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', }, workerId: { type: 'string', diff --git a/apps/sim/tools/workday/get_organizations.ts b/apps/sim/tools/workday/get_organizations.ts index 35953883a2c..b2c4bb6acb7 100644 --- a/apps/sim/tools/workday/get_organizations.ts +++ b/apps/sim/tools/workday/get_organizations.ts @@ -26,11 +26,17 @@ export const getOrganizationsTool: ToolConfig< visibility: 'user-only', description: 'Workday tenant name', }, - accessToken: { + username: { type: 'string', required: true, - visibility: 'hidden', - description: 'OAuth 2.0 access token for Workday REST API', + visibility: 'user-only', + description: 'Integration System User username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Integration System User password', }, type: { type: 'string', diff --git a/apps/sim/tools/workday/hire_employee.ts b/apps/sim/tools/workday/hire_employee.ts index 6409276ecea..f0686ca88d0 100644 --- a/apps/sim/tools/workday/hire_employee.ts +++ b/apps/sim/tools/workday/hire_employee.ts @@ -52,24 +52,6 @@ export const hireEmployeeTool: ToolConfig Date: Wed, 18 Mar 2026 20:57:36 -0700 Subject: [PATCH 05/13] address comments --- .../api/tools/workday/create-prehire/route.ts | 20 +++++----- apps/sim/blocks/blocks/workday.ts | 19 ++++++--- apps/sim/tools/workday/utils.ts | 40 ------------------- 3 files changed, 24 insertions(+), 55 deletions(-) delete mode 100644 apps/sim/tools/workday/utils.ts diff --git a/apps/sim/app/api/tools/workday/create-prehire/route.ts b/apps/sim/app/api/tools/workday/create-prehire/route.ts index 06ee611e124..de8c3e584c6 100644 --- a/apps/sim/app/api/tools/workday/create-prehire/route.ts +++ b/apps/sim/app/api/tools/workday/create-prehire/route.ts @@ -32,6 +32,16 @@ export async function POST(request: NextRequest) { const body = await request.json() const data = RequestSchema.parse(body) + if (!data.email && !data.phoneNumber && !data.address) { + return NextResponse.json( + { + success: false, + error: 'At least one contact method (email, phone, or address) is required', + }, + { status: 400 } + ) + } + const parts = data.legalName.trim().split(/\s+/) const firstName = parts[0] ?? '' const lastName = parts.length > 1 ? parts.slice(1).join(' ') : (parts[0] ?? '') @@ -44,16 +54,6 @@ export async function POST(request: NextRequest) { data.password ) - if (!data.email && !data.phoneNumber && !data.address) { - return NextResponse.json( - { - success: false, - error: 'At least one contact method (email, phone, or address) is required', - }, - { status: 400 } - ) - } - const contactData: Record = {} if (data.email) { contactData.Email_Address_Data = [ diff --git a/apps/sim/blocks/blocks/workday.ts b/apps/sim/blocks/blocks/workday.ts index ef251aaa8fb..99992ad00f8 100644 --- a/apps/sim/blocks/blocks/workday.ts +++ b/apps/sim/blocks/blocks/workday.ts @@ -349,7 +349,16 @@ Output: {"businessTitle": "Senior Engineer"}`, config: { tool: (params) => `workday_${params.operation}`, params: (params) => { - const { operation, orgType, fields, ...rest } = params + const { + operation, + orgType, + fields, + positionId, + jobProfileId, + locationId, + managerId, + ...rest + } = params if (rest.limit != null && rest.limit !== '') rest.limit = Number(rest.limit) if (rest.offset != null && rest.offset !== '') rest.offset = Number(rest.offset) @@ -357,10 +366,10 @@ Output: {"businessTitle": "Senior Engineer"}`, if (orgType) rest.type = orgType if (operation === 'change_job') { - if (rest.positionId) rest.newPositionId = rest.positionId - if (rest.jobProfileId) rest.newJobProfileId = rest.jobProfileId - if (rest.locationId) rest.newLocationId = rest.locationId - if (rest.managerId) rest.newManagerId = rest.managerId + if (positionId) rest.newPositionId = positionId + if (jobProfileId) rest.newJobProfileId = jobProfileId + if (locationId) rest.newLocationId = locationId + if (managerId) rest.newManagerId = managerId } if (fields && operation === 'update_worker') { diff --git a/apps/sim/tools/workday/utils.ts b/apps/sim/tools/workday/utils.ts deleted file mode 100644 index b8c12ab9a0a..00000000000 --- a/apps/sim/tools/workday/utils.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Creates a Basic Authentication header for Workday ISU credentials. - * @param username Integration System User username - * @param password Integration System User password - * @returns Base64-encoded Basic Auth header value - */ -export function createWorkdayAuthHeader(username: string, password: string): string { - const credentials = Buffer.from(`${username}:${password}`).toString('base64') - return `Basic ${credentials}` -} - -/** - * Builds a Workday REST API base URL. - * REST pattern: {tenantUrl}/api/v1/{tenant} - * @param tenantUrl The Workday instance URL (e.g., https://wd2-impl-services1.workday.com) - * @param tenant The tenant name - */ -export function buildWorkdayRestUrl(tenantUrl: string, tenant: string): string { - const baseUrl = tenantUrl.replace(/\/$/, '') - return `${baseUrl}/api/v1/${tenant}` -} - -/** - * Builds a Workday SOAP/WS API base URL. - * SOAP pattern: {tenantUrl}/ccx/service/{tenant}/{serviceName}/{version} - * Used for operations not available via REST (hire, terminate, etc.). - * @param tenantUrl The Workday instance URL - * @param tenant The tenant name - * @param serviceName The WS service name (e.g., Staffing, Human_Resources) - * @param version The API version (e.g., v42.0, v45.0) - */ -export function buildWorkdaySoapUrl( - tenantUrl: string, - tenant: string, - serviceName: string, - version: string -): string { - const baseUrl = tenantUrl.replace(/\/$/, '') - return `${baseUrl}/ccx/service/${tenant}/${serviceName}/${version}` -} From e1e5a066322036174424a7fcb102e082927ffd0e Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 18 Mar 2026 21:02:44 -0700 Subject: [PATCH 06/13] fix --- apps/sim/blocks/blocks/workday.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/sim/blocks/blocks/workday.ts b/apps/sim/blocks/blocks/workday.ts index 99992ad00f8..4ddd419ca9f 100644 --- a/apps/sim/blocks/blocks/workday.ts +++ b/apps/sim/blocks/blocks/workday.ts @@ -349,16 +349,7 @@ Output: {"businessTitle": "Senior Engineer"}`, config: { tool: (params) => `workday_${params.operation}`, params: (params) => { - const { - operation, - orgType, - fields, - positionId, - jobProfileId, - locationId, - managerId, - ...rest - } = params + const { operation, orgType, fields, jobProfileId, locationId, managerId, ...rest } = params if (rest.limit != null && rest.limit !== '') rest.limit = Number(rest.limit) if (rest.offset != null && rest.offset !== '') rest.offset = Number(rest.offset) @@ -366,7 +357,10 @@ Output: {"businessTitle": "Senior Engineer"}`, if (orgType) rest.type = orgType if (operation === 'change_job') { - if (positionId) rest.newPositionId = positionId + if (rest.positionId) { + rest.newPositionId = rest.positionId + rest.positionId = undefined + } if (jobProfileId) rest.newJobProfileId = jobProfileId if (locationId) rest.newLocationId = locationId if (managerId) rest.newManagerId = managerId From 01a7fbce16fdc64aaf62189e370f2b8e85ecaeec Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 18 Mar 2026 21:17:11 -0700 Subject: [PATCH 07/13] more type fixes --- apps/sim/app/api/tools/workday/change-job/route.ts | 6 +++--- .../app/api/tools/workday/create-prehire/route.ts | 2 +- apps/sim/blocks/blocks/workday.ts | 13 +++++++------ apps/sim/tools/workday/change_job.ts | 4 ++-- apps/sim/tools/workday/types.ts | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/api/tools/workday/change-job/route.ts b/apps/sim/app/api/tools/workday/change-job/route.ts index 615db2fb21e..6858a49a649 100644 --- a/apps/sim/app/api/tools/workday/change-job/route.ts +++ b/apps/sim/app/api/tools/workday/change-job/route.ts @@ -19,7 +19,7 @@ const RequestSchema = z.object({ newPositionId: z.string().optional(), newJobProfileId: z.string().optional(), newLocationId: z.string().optional(), - newManagerId: z.string().optional(), + newSupervisoryOrgId: z.string().optional(), reason: z.string().min(1, 'Reason is required for job changes'), }) @@ -47,10 +47,10 @@ export async function POST(request: NextRequest) { if (data.newLocationId) { changeJobDetailData.Location_Reference = wdRef('Location_ID', data.newLocationId) } - if (data.newManagerId) { + if (data.newSupervisoryOrgId) { changeJobDetailData.Supervisory_Organization_Reference = wdRef( 'Supervisory_Organization_ID', - data.newManagerId + data.newSupervisoryOrgId ) } diff --git a/apps/sim/app/api/tools/workday/create-prehire/route.ts b/apps/sim/app/api/tools/workday/create-prehire/route.ts index de8c3e584c6..e98b1791620 100644 --- a/apps/sim/app/api/tools/workday/create-prehire/route.ts +++ b/apps/sim/app/api/tools/workday/create-prehire/route.ts @@ -44,7 +44,7 @@ export async function POST(request: NextRequest) { const parts = data.legalName.trim().split(/\s+/) const firstName = parts[0] ?? '' - const lastName = parts.length > 1 ? parts.slice(1).join(' ') : (parts[0] ?? '') + const lastName = parts.length > 1 ? parts.slice(1).join(' ') : '' const client = await createWorkdaySoapClient( data.tenantUrl, diff --git a/apps/sim/blocks/blocks/workday.ts b/apps/sim/blocks/blocks/workday.ts index 4ddd419ca9f..61c9a3012ea 100644 --- a/apps/sim/blocks/blocks/workday.ts +++ b/apps/sim/blocks/blocks/workday.ts @@ -192,10 +192,10 @@ export const WorkdayBlock: BlockConfig = { mode: 'advanced', }, { - id: 'managerId', - title: 'Manager ID', + id: 'supervisoryOrgId', + title: 'Supervisory Organization ID', type: 'short-input', - placeholder: 'Manager worker ID', + placeholder: 'Target supervisory organization ID', condition: { field: 'operation', value: 'change_job' }, mode: 'advanced', }, @@ -349,7 +349,8 @@ Output: {"businessTitle": "Senior Engineer"}`, config: { tool: (params) => `workday_${params.operation}`, params: (params) => { - const { operation, orgType, fields, jobProfileId, locationId, managerId, ...rest } = params + const { operation, orgType, fields, jobProfileId, locationId, supervisoryOrgId, ...rest } = + params if (rest.limit != null && rest.limit !== '') rest.limit = Number(rest.limit) if (rest.offset != null && rest.offset !== '') rest.offset = Number(rest.offset) @@ -363,7 +364,7 @@ Output: {"businessTitle": "Senior Engineer"}`, } if (jobProfileId) rest.newJobProfileId = jobProfileId if (locationId) rest.newLocationId = locationId - if (managerId) rest.newManagerId = managerId + if (supervisoryOrgId) rest.newSupervisoryOrgId = supervisoryOrgId } if (fields && operation === 'update_worker') { @@ -393,7 +394,7 @@ Output: {"businessTitle": "Senior Engineer"}`, hireDate: { type: 'string', description: 'Hire date (YYYY-MM-DD)' }, jobProfileId: { type: 'string', description: 'Job profile ID' }, locationId: { type: 'string', description: 'Location ID' }, - managerId: { type: 'string', description: 'Manager worker ID' }, + supervisoryOrgId: { type: 'string', description: 'Target supervisory organization ID' }, employeeType: { type: 'string', description: 'Employee type' }, fields: { type: 'json', description: 'Fields to update' }, onboardingPlanId: { type: 'string', description: 'Onboarding plan ID' }, diff --git a/apps/sim/tools/workday/change_job.ts b/apps/sim/tools/workday/change_job.ts index b7088831b16..2d986ccd873 100644 --- a/apps/sim/tools/workday/change_job.ts +++ b/apps/sim/tools/workday/change_job.ts @@ -63,11 +63,11 @@ export const changeJobTool: ToolConfig Date: Wed, 18 Mar 2026 21:38:54 -0700 Subject: [PATCH 08/13] address more comments --- .../api/tools/workday/create-prehire/route.ts | 10 ++++- .../tools/workday/get-compensation/route.ts | 38 ++++++++++++++----- apps/sim/blocks/blocks/workday.ts | 18 ++++++++- apps/sim/components/icons.tsx | 6 ++- apps/sim/tools/workday/create_prehire.ts | 6 +++ apps/sim/tools/workday/types.ts | 1 + 6 files changed, 65 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/api/tools/workday/create-prehire/route.ts b/apps/sim/app/api/tools/workday/create-prehire/route.ts index e98b1791620..d9a955b4187 100644 --- a/apps/sim/app/api/tools/workday/create-prehire/route.ts +++ b/apps/sim/app/api/tools/workday/create-prehire/route.ts @@ -18,6 +18,7 @@ const RequestSchema = z.object({ email: z.string().optional(), phoneNumber: z.string().optional(), address: z.string().optional(), + countryCode: z.string().optional(), }) export async function POST(request: NextRequest) { @@ -46,6 +47,13 @@ export async function POST(request: NextRequest) { const firstName = parts[0] ?? '' const lastName = parts.length > 1 ? parts.slice(1).join(' ') : '' + if (!lastName) { + return NextResponse.json( + { success: false, error: 'Legal name must include both a first name and last name' }, + { status: 400 } + ) + } + const client = await createWorkdaySoapClient( data.tenantUrl, data.tenant, @@ -96,7 +104,7 @@ export async function POST(request: NextRequest) { Name_Data: { Legal_Name_Data: { Name_Detail_Data: { - Country_Reference: wdRef('ISO_3166-1_Alpha-2_Code', 'US'), + Country_Reference: wdRef('ISO_3166-1_Alpha-2_Code', data.countryCode ?? 'US'), First_Name: firstName, Last_Name: lastName, }, diff --git a/apps/sim/app/api/tools/workday/get-compensation/route.ts b/apps/sim/app/api/tools/workday/get-compensation/route.ts index cacea304c01..be32cf5f0af 100644 --- a/apps/sim/app/api/tools/workday/get-compensation/route.ts +++ b/apps/sim/app/api/tools/workday/get-compensation/route.ts @@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' -import { createWorkdaySoapClient } from '@/tools/workday/soap' +import { createWorkdaySoapClient, extractRefId, type WorkdayReference } from '@/tools/workday/soap' export const dynamic = 'force-dynamic' @@ -57,15 +57,35 @@ export async function POST(request: NextRequest) { const workerInner = workerData?.Worker_Data as Record | undefined const compensationData = workerInner?.Compensation_Data as Record | undefined - const rawPlans = compensationData?.Compensation_Plan_Assignment - const plansArray = (Array.isArray(rawPlans) ? rawPlans : rawPlans ? [rawPlans] : []) as Record< - string, - unknown - >[] + const mapPlan = (p: Record) => ({ + id: extractRefId(p.Compensation_Plan_Reference as WorkdayReference | undefined) ?? null, + planName: + (p.Compensation_Plan_Reference as Record | undefined)?.attributes + ?.Descriptor ?? null, + amount: p.Amount ?? p.Per_Unit_Amount ?? p.Individual_Target_Amount ?? null, + currency: extractRefId(p.Currency_Reference as WorkdayReference | undefined) ?? null, + frequency: extractRefId(p.Frequency_Reference as WorkdayReference | undefined) ?? null, + }) - const compensationPlans = plansArray.map((p) => ({ - ...p, - })) + const planTypes = [ + 'Employee_Base_Pay_Plan_Assignment_Data', + 'Employee_Salary_Unit_Plan_Assignment_Data', + 'Employee_Bonus_Plan_Assignment_Data', + 'Employee_Allowance_Plan_Assignment_Data', + 'Employee_Commission_Plan_Assignment_Data', + 'Employee_Stock_Plan_Assignment_Data', + 'Employee_Period_Salary_Plan_Assignment_Data', + ] as const + + const compensationPlans: ReturnType[] = [] + for (const planType of planTypes) { + const raw = compensationData?.[planType] + if (!raw) continue + const arr = Array.isArray(raw) ? raw : [raw] + for (const p of arr) { + compensationPlans.push(mapPlan(p as Record)) + } + } return NextResponse.json({ success: true, diff --git a/apps/sim/blocks/blocks/workday.ts b/apps/sim/blocks/blocks/workday.ts index 61c9a3012ea..b55e4e7fc0f 100644 --- a/apps/sim/blocks/blocks/workday.ts +++ b/apps/sim/blocks/blocks/workday.ts @@ -144,6 +144,15 @@ export const WorkdayBlock: BlockConfig = { condition: { field: 'operation', value: 'create_prehire' }, mode: 'advanced', }, + { + id: 'countryCode', + title: 'Country Code', + type: 'short-input', + placeholder: 'US', + condition: { field: 'operation', value: 'create_prehire' }, + mode: 'advanced', + description: 'ISO 3166-1 Alpha-2 country code (defaults to US)', + }, // Hire Employee { @@ -368,8 +377,12 @@ Output: {"businessTitle": "Senior Engineer"}`, } if (fields && operation === 'update_worker') { - const parsedFields = typeof fields === 'string' ? JSON.parse(fields) : fields - return { ...rest, fields: parsedFields } + try { + const parsedFields = typeof fields === 'string' ? JSON.parse(fields) : fields + return { ...rest, fields: parsedFields } + } catch { + throw new Error('Invalid JSON in Fields block') + } } return rest @@ -389,6 +402,7 @@ Output: {"businessTitle": "Senior Engineer"}`, email: { type: 'string', description: 'Email address' }, phoneNumber: { type: 'string', description: 'Phone number' }, address: { type: 'string', description: 'Address' }, + countryCode: { type: 'string', description: 'ISO 3166-1 Alpha-2 country code' }, preHireId: { type: 'string', description: 'Pre-hire record ID' }, positionId: { type: 'string', description: 'Position ID' }, hireDate: { type: 'string', description: 'Hire date (YYYY-MM-DD)' }, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 86885a81571..db37ce09e17 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -125,9 +125,11 @@ export function NoteIcon(props: SVGProps) { } export function WorkdayIcon(props: SVGProps) { + const id = useId() + const clipId = `workday_clip_${id}` return ( - + ) { /> - + diff --git a/apps/sim/tools/workday/create_prehire.ts b/apps/sim/tools/workday/create_prehire.ts index 95746cb9c9f..cf787eea437 100644 --- a/apps/sim/tools/workday/create_prehire.ts +++ b/apps/sim/tools/workday/create_prehire.ts @@ -63,6 +63,12 @@ export const createPrehireTool: ToolConfig< 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: { diff --git a/apps/sim/tools/workday/types.ts b/apps/sim/tools/workday/types.ts index 448df99375d..bc77a33a0ee 100644 --- a/apps/sim/tools/workday/types.ts +++ b/apps/sim/tools/workday/types.ts @@ -59,6 +59,7 @@ export interface WorkdayCreatePrehireParams extends WorkdayBaseParams { email?: string phoneNumber?: string address?: string + countryCode?: string } export interface WorkdayCreatePrehireResponse extends ToolResponse { From 4b5e192287050c3ea231945d7793a92883a55620 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 18 Mar 2026 21:54:46 -0700 Subject: [PATCH 09/13] fix files --- apps/sim/app/api/files/upload/route.ts | 2 +- apps/sim/app/api/workspaces/[id]/files/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/files/upload/route.ts b/apps/sim/app/api/files/upload/route.ts index e97aeeb3707..48f85bf21ba 100644 --- a/apps/sim/app/api/files/upload/route.ts +++ b/apps/sim/app/api/files/upload/route.ts @@ -75,7 +75,7 @@ export async function POST(request: NextRequest) { const uploadResults = [] for (const file of files) { - const originalName = file.name || 'untitled' + const originalName = file.name || 'untitled.md' if (!validateFileExtension(originalName)) { const extension = originalName.split('.').pop()?.toLowerCase() || 'unknown' diff --git a/apps/sim/app/api/workspaces/[id]/files/route.ts b/apps/sim/app/api/workspaces/[id]/files/route.ts index b452ae47006..18f4faa2afd 100644 --- a/apps/sim/app/api/workspaces/[id]/files/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/route.ts @@ -93,7 +93,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'No file provided' }, { status: 400 }) } - const fileName = rawFile.name || 'untitled' + const fileName = rawFile.name || 'untitled.md' const maxSize = 100 * 1024 * 1024 if (rawFile.size > maxSize) { From db76e5bae05c34c729bb9212e6cd264f09739c26 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 18 Mar 2026 21:57:32 -0700 Subject: [PATCH 10/13] fix file editor useEffect --- apps/sim/app/workspace/[workspaceId]/files/files.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index ac156edf562..448a1b251d6 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -151,6 +151,8 @@ export function Files() { } const justCreatedFileIdRef = useRef(null) + const filesRef = useRef(files) + filesRef.current = files const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 }) @@ -483,11 +485,11 @@ export function Files() { if (isJustCreated) { setPreviewMode('editor') } else { - const file = selectedFileId ? files.find((f) => f.id === selectedFileId) : null + const file = selectedFileId ? filesRef.current.find((f) => f.id === selectedFileId) : null const canPreview = file ? isPreviewable(file) : false setPreviewMode(canPreview ? 'preview' : 'editor') } - }, [selectedFileId, files]) + }, [selectedFileId]) useEffect(() => { if (!selectedFile) return From 50f68fc2df0be253bdd3e9c8579ecb025936c4fb Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 18 Mar 2026 22:01:44 -0700 Subject: [PATCH 11/13] fix build issue --- .../app/api/files/serve/[...path]/route.ts | 4 +- apps/sim/app/api/files/utils.test.ts | 80 ++++++++++--------- apps/sim/app/api/files/utils.ts | 76 +++--------------- 3 files changed, 56 insertions(+), 104 deletions(-) diff --git a/apps/sim/app/api/files/serve/[...path]/route.ts b/apps/sim/app/api/files/serve/[...path]/route.ts index f09bad7100d..94d882a86e8 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.ts @@ -102,7 +102,7 @@ async function handleLocalFile(filename: string, userId: string): Promise { try { - const filePath = findLocalFile(filename) + const filePath = await findLocalFile(filename) if (!filePath) { throw new FileNotFoundError(`File not found: ${filename}`) diff --git a/apps/sim/app/api/files/utils.test.ts b/apps/sim/app/api/files/utils.test.ts index 58d1791f922..5180efd47ea 100644 --- a/apps/sim/app/api/files/utils.test.ts +++ b/apps/sim/app/api/files/utils.test.ts @@ -331,7 +331,7 @@ describe('extractFilename', () => { describe('findLocalFile - Path Traversal Security Tests', () => { describe('path traversal attack prevention', () => { - it.concurrent('should reject classic path traversal attacks', () => { + it.concurrent('should reject classic path traversal attacks', async () => { const maliciousInputs = [ '../../../etc/passwd', '..\\..\\..\\windows\\system32\\config\\sam', @@ -340,35 +340,35 @@ describe('findLocalFile - Path Traversal Security Tests', () => { '..\\config.ini', ] - maliciousInputs.forEach((input) => { - const result = findLocalFile(input) + for (const input of maliciousInputs) { + const result = await findLocalFile(input) expect(result).toBeNull() - }) + } }) - it.concurrent('should reject encoded path traversal attempts', () => { + it.concurrent('should reject encoded path traversal attempts', async () => { const encodedInputs = [ '%2e%2e%2f%2e%2e%2f%65%74%63%2f%70%61%73%73%77%64', // ../../../etc/passwd '..%2f..%2fetc%2fpasswd', '..%5c..%5cconfig.ini', ] - encodedInputs.forEach((input) => { - const result = findLocalFile(input) + for (const input of encodedInputs) { + const result = await findLocalFile(input) expect(result).toBeNull() - }) + } }) - it.concurrent('should reject mixed path separators', () => { + it.concurrent('should reject mixed path separators', async () => { const mixedInputs = ['../..\\config.txt', '..\\../secret.ini', '/..\\..\\system32'] - mixedInputs.forEach((input) => { - const result = findLocalFile(input) + for (const input of mixedInputs) { + const result = await findLocalFile(input) expect(result).toBeNull() - }) + } }) - it.concurrent('should reject filenames with dangerous characters', () => { + it.concurrent('should reject filenames with dangerous characters', async () => { const dangerousInputs = [ 'file:with:colons.txt', 'file|with|pipes.txt', @@ -376,43 +376,45 @@ describe('findLocalFile - Path Traversal Security Tests', () => { 'file*with*asterisks.txt', ] - dangerousInputs.forEach((input) => { - const result = findLocalFile(input) + for (const input of dangerousInputs) { + const result = await findLocalFile(input) expect(result).toBeNull() - }) + } }) - it.concurrent('should reject null and empty inputs', () => { - expect(findLocalFile('')).toBeNull() - expect(findLocalFile(' ')).toBeNull() - expect(findLocalFile('\t\n')).toBeNull() + it.concurrent('should reject null and empty inputs', async () => { + expect(await findLocalFile('')).toBeNull() + expect(await findLocalFile(' ')).toBeNull() + expect(await findLocalFile('\t\n')).toBeNull() }) - it.concurrent('should reject filenames that become empty after sanitization', () => { + it.concurrent('should reject filenames that become empty after sanitization', async () => { const emptyAfterSanitization = ['../..', '..\\..\\', '////', '....', '..'] - emptyAfterSanitization.forEach((input) => { - const result = findLocalFile(input) + for (const input of emptyAfterSanitization) { + const result = await findLocalFile(input) expect(result).toBeNull() - }) + } }) }) describe('security validation passes for legitimate files', () => { - it.concurrent('should accept properly formatted filenames without throwing errors', () => { - const legitimateInputs = [ - 'document.pdf', - 'image.png', - 'data.csv', - 'report-2024.doc', - 'file_with_underscores.txt', - 'file-with-dashes.json', - ] - - legitimateInputs.forEach((input) => { - // Should not throw security errors for legitimate filenames - expect(() => findLocalFile(input)).not.toThrow() - }) - }) + it.concurrent( + 'should accept properly formatted filenames without throwing errors', + async () => { + const legitimateInputs = [ + 'document.pdf', + 'image.png', + 'data.csv', + 'report-2024.doc', + 'file_with_underscores.txt', + 'file-with-dashes.json', + ] + + for (const input of legitimateInputs) { + await expect(findLocalFile(input)).resolves.not.toThrow() + } + } + ) }) }) diff --git a/apps/sim/app/api/files/utils.ts b/apps/sim/app/api/files/utils.ts index a4831cdd3b7..d290288f2da 100644 --- a/apps/sim/app/api/files/utils.ts +++ b/apps/sim/app/api/files/utils.ts @@ -1,8 +1,5 @@ -import { existsSync } from 'fs' -import path from 'path' import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' -import { UPLOAD_DIR } from '@/lib/uploads/config' import { sanitizeFileKey } from '@/lib/uploads/utils/file-utils' const logger = createLogger('FilesUtils') @@ -123,76 +120,29 @@ export function extractFilename(path: string): string { return filename } -function sanitizeFilename(filename: string): string { - if (!filename || typeof filename !== 'string') { - throw new Error('Invalid filename provided') - } - - if (!filename.includes('/')) { - throw new Error('File key must include a context prefix (e.g., kb/, workspace/, execution/)') - } - - const segments = filename.split('/') - - const sanitizedSegments = segments.map((segment) => { - if (segment === '..' || segment === '.') { - throw new Error('Path traversal detected') - } - - const sanitized = segment.replace(/\.\./g, '').replace(/[\\]/g, '').replace(/^\./g, '').trim() - - if (!sanitized) { - throw new Error('Invalid or empty path segment after sanitization') - } - - if ( - sanitized.includes(':') || - sanitized.includes('|') || - sanitized.includes('?') || - sanitized.includes('*') || - sanitized.includes('\x00') || - /[\x00-\x1F\x7F]/.test(sanitized) - ) { - throw new Error('Path segment contains invalid characters') - } - - return sanitized - }) - - return sanitizedSegments.join(path.sep) -} - -export function findLocalFile(filename: string): string | null { +export async function findLocalFile(filename: string): Promise { try { const sanitizedFilename = sanitizeFileKey(filename) - // Reject if sanitized filename is empty or only contains path separators/dots if (!sanitizedFilename || !sanitizedFilename.trim() || /^[/\\.\s]+$/.test(sanitizedFilename)) { return null } - const possiblePaths = [ - path.join(UPLOAD_DIR, sanitizedFilename), - path.join(process.cwd(), 'uploads', sanitizedFilename), - ] + const { existsSync } = await import('fs') + const path = await import('path') + const { UPLOAD_DIR_SERVER } = await import('@/lib/uploads/core/setup.server') - for (const filePath of possiblePaths) { - const resolvedPath = path.resolve(filePath) - const allowedDirs = [path.resolve(UPLOAD_DIR), path.resolve(process.cwd(), 'uploads')] + const resolvedPath = path.join(UPLOAD_DIR_SERVER, sanitizedFilename) - // Must be within allowed directory but NOT the directory itself - const isWithinAllowedDir = allowedDirs.some( - (allowedDir) => - resolvedPath.startsWith(allowedDir + path.sep) && resolvedPath !== allowedDir - ) - - if (!isWithinAllowedDir) { - continue - } + if ( + !resolvedPath.startsWith(UPLOAD_DIR_SERVER + path.sep) || + resolvedPath === UPLOAD_DIR_SERVER + ) { + return null + } - if (existsSync(resolvedPath)) { - return resolvedPath - } + if (existsSync(resolvedPath)) { + return resolvedPath } return null From e74a6686943f37f0f50775830044d371cc462ab2 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 18 Mar 2026 22:18:16 -0700 Subject: [PATCH 12/13] fix typing --- .../tools/workday/get-compensation/route.ts | 46 ++++++------ .../tools/workday/get-organizations/route.ts | 36 +++++----- .../app/api/tools/workday/get-worker/route.ts | 31 ++++---- .../api/tools/workday/list-workers/route.ts | 27 +++---- apps/sim/tools/workday/soap.ts | 72 ++++++++++++++++++- 5 files changed, 145 insertions(+), 67 deletions(-) diff --git a/apps/sim/app/api/tools/workday/get-compensation/route.ts b/apps/sim/app/api/tools/workday/get-compensation/route.ts index be32cf5f0af..a78a1619933 100644 --- a/apps/sim/app/api/tools/workday/get-compensation/route.ts +++ b/apps/sim/app/api/tools/workday/get-compensation/route.ts @@ -3,7 +3,14 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' -import { createWorkdaySoapClient, extractRefId, type WorkdayReference } from '@/tools/workday/soap' +import { + createWorkdaySoapClient, + extractRefId, + normalizeSoapArray, + type WorkdayCompensationDataSoap, + type WorkdayCompensationPlanSoap, + type WorkdayWorkerSoap, +} from '@/tools/workday/soap' export const dynamic = 'force-dynamic' @@ -49,25 +56,21 @@ export async function POST(request: NextRequest) { }, }) - const rawWorker = result?.Response_Data?.Worker - const workerData = (Array.isArray(rawWorker) ? rawWorker[0] : (rawWorker ?? null)) as Record< - string, - unknown - > | null - const workerInner = workerData?.Worker_Data as Record | undefined - const compensationData = workerInner?.Compensation_Data as Record | undefined + const worker = + normalizeSoapArray( + result?.Response_Data?.Worker as WorkdayWorkerSoap | WorkdayWorkerSoap[] | undefined + )[0] ?? null + const compensationData = worker?.Worker_Data?.Compensation_Data - const mapPlan = (p: Record) => ({ - id: extractRefId(p.Compensation_Plan_Reference as WorkdayReference | undefined) ?? null, - planName: - (p.Compensation_Plan_Reference as Record | undefined)?.attributes - ?.Descriptor ?? null, + const mapPlan = (p: WorkdayCompensationPlanSoap) => ({ + id: extractRefId(p.Compensation_Plan_Reference) ?? null, + planName: p.Compensation_Plan_Reference?.attributes?.Descriptor ?? null, amount: p.Amount ?? p.Per_Unit_Amount ?? p.Individual_Target_Amount ?? null, - currency: extractRefId(p.Currency_Reference as WorkdayReference | undefined) ?? null, - frequency: extractRefId(p.Frequency_Reference as WorkdayReference | undefined) ?? null, + currency: extractRefId(p.Currency_Reference) ?? null, + frequency: extractRefId(p.Frequency_Reference) ?? null, }) - const planTypes = [ + const planTypeKeys: (keyof WorkdayCompensationDataSoap)[] = [ 'Employee_Base_Pay_Plan_Assignment_Data', 'Employee_Salary_Unit_Plan_Assignment_Data', 'Employee_Bonus_Plan_Assignment_Data', @@ -75,15 +78,12 @@ export async function POST(request: NextRequest) { 'Employee_Commission_Plan_Assignment_Data', 'Employee_Stock_Plan_Assignment_Data', 'Employee_Period_Salary_Plan_Assignment_Data', - ] as const + ] const compensationPlans: ReturnType[] = [] - for (const planType of planTypes) { - const raw = compensationData?.[planType] - if (!raw) continue - const arr = Array.isArray(raw) ? raw : [raw] - for (const p of arr) { - compensationPlans.push(mapPlan(p as Record)) + for (const key of planTypeKeys) { + for (const plan of normalizeSoapArray(compensationData?.[key])) { + compensationPlans.push(mapPlan(plan)) } } diff --git a/apps/sim/app/api/tools/workday/get-organizations/route.ts b/apps/sim/app/api/tools/workday/get-organizations/route.ts index 70488285e8d..93adddd0b86 100644 --- a/apps/sim/app/api/tools/workday/get-organizations/route.ts +++ b/apps/sim/app/api/tools/workday/get-organizations/route.ts @@ -3,7 +3,12 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' -import { createWorkdaySoapClient, extractRefId } from '@/tools/workday/soap' +import { + createWorkdaySoapClient, + extractRefId, + normalizeSoapArray, + type WorkdayOrganizationSoap, +} from '@/tools/workday/soap' export const dynamic = 'force-dynamic' @@ -58,23 +63,20 @@ export async function POST(request: NextRequest) { Response_Group: { Include_Hierarchy_Data: true }, }) - const rawOrgs = result?.Response_Data?.Organization - const orgsArray = Array.isArray(rawOrgs) ? rawOrgs : rawOrgs ? [rawOrgs] : [] + const orgsArray = normalizeSoapArray( + result?.Response_Data?.Organization as + | WorkdayOrganizationSoap + | WorkdayOrganizationSoap[] + | undefined + ) - const organizations = orgsArray.map((o: Record) => { - const orgData = o.Organization_Data as Record | undefined - return { - id: extractRefId(o.Organization_Reference as Record) ?? null, - descriptor: o.Organization_Descriptor ?? null, - type: orgData?.Organization_Type_Reference - ? (extractRefId(orgData.Organization_Type_Reference) ?? null) - : null, - subtype: orgData?.Organization_Subtype_Reference - ? (extractRefId(orgData.Organization_Subtype_Reference) ?? null) - : null, - isActive: orgData?.Inactive != null ? !orgData.Inactive : null, - } - }) + const organizations = orgsArray.map((o) => ({ + id: extractRefId(o.Organization_Reference) ?? null, + descriptor: o.Organization_Descriptor ?? null, + type: extractRefId(o.Organization_Data?.Organization_Type_Reference) ?? null, + subtype: extractRefId(o.Organization_Data?.Organization_Subtype_Reference) ?? null, + isActive: o.Organization_Data?.Inactive != null ? !o.Organization_Data.Inactive : null, + })) const total = result?.Response_Results?.Total_Results ?? organizations.length diff --git a/apps/sim/app/api/tools/workday/get-worker/route.ts b/apps/sim/app/api/tools/workday/get-worker/route.ts index b99c9f7d8eb..904c5cf4132 100644 --- a/apps/sim/app/api/tools/workday/get-worker/route.ts +++ b/apps/sim/app/api/tools/workday/get-worker/route.ts @@ -3,7 +3,12 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' -import { createWorkdaySoapClient, extractRefId, type WorkdayReference } from '@/tools/workday/soap' +import { + createWorkdaySoapClient, + extractRefId, + normalizeSoapArray, + type WorkdayWorkerSoap, +} from '@/tools/workday/soap' export const dynamic = 'force-dynamic' @@ -52,24 +57,22 @@ export async function POST(request: NextRequest) { }, }) - const rawWorker = result?.Response_Data?.Worker - const workerData = (Array.isArray(rawWorker) ? rawWorker[0] : (rawWorker ?? null)) as Record< - string, - unknown - > | null - const workerInner = (workerData?.Worker_Data ?? null) as Record | null + const worker = + normalizeSoapArray( + result?.Response_Data?.Worker as WorkdayWorkerSoap | WorkdayWorkerSoap[] | undefined + )[0] ?? null return NextResponse.json({ success: true, output: { - worker: workerData + worker: worker ? { - id: extractRefId(workerData.Worker_Reference as WorkdayReference | undefined) ?? null, - descriptor: (workerData.Worker_Descriptor as string) ?? null, - personalData: workerInner?.Personal_Data ?? null, - employmentData: workerInner?.Employment_Data ?? null, - compensationData: workerInner?.Compensation_Data ?? null, - organizationData: workerInner?.Organization_Data ?? null, + id: extractRefId(worker.Worker_Reference) ?? null, + descriptor: worker.Worker_Descriptor ?? null, + personalData: worker.Worker_Data?.Personal_Data ?? null, + employmentData: worker.Worker_Data?.Employment_Data ?? null, + compensationData: worker.Worker_Data?.Compensation_Data ?? null, + organizationData: worker.Worker_Data?.Organization_Data ?? null, } : null, }, diff --git a/apps/sim/app/api/tools/workday/list-workers/route.ts b/apps/sim/app/api/tools/workday/list-workers/route.ts index d6a5c83e3f7..e8f31950367 100644 --- a/apps/sim/app/api/tools/workday/list-workers/route.ts +++ b/apps/sim/app/api/tools/workday/list-workers/route.ts @@ -3,7 +3,12 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' -import { createWorkdaySoapClient, extractRefId } from '@/tools/workday/soap' +import { + createWorkdaySoapClient, + extractRefId, + normalizeSoapArray, + type WorkdayWorkerSoap, +} from '@/tools/workday/soap' export const dynamic = 'force-dynamic' @@ -51,18 +56,16 @@ export async function POST(request: NextRequest) { }, }) - const rawWorkers = result?.Response_Data?.Worker - const workersArray = Array.isArray(rawWorkers) ? rawWorkers : rawWorkers ? [rawWorkers] : [] + const workersArray = normalizeSoapArray( + result?.Response_Data?.Worker as WorkdayWorkerSoap | WorkdayWorkerSoap[] | undefined + ) - const workers = workersArray.map((w: Record) => { - const workerData = w.Worker_Data as Record | undefined - return { - id: extractRefId(w.Worker_Reference as Record) ?? null, - descriptor: w.Worker_Descriptor ?? null, - personalData: workerData?.Personal_Data ?? null, - employmentData: workerData?.Employment_Data ?? null, - } - }) + const workers = workersArray.map((w) => ({ + id: extractRefId(w.Worker_Reference) ?? null, + descriptor: w.Worker_Descriptor ?? null, + personalData: w.Worker_Data?.Personal_Data ?? null, + employmentData: w.Worker_Data?.Employment_Data ?? null, + })) const total = result?.Response_Results?.Total_Results ?? workers.length diff --git a/apps/sim/tools/workday/soap.ts b/apps/sim/tools/workday/soap.ts index 725b2ac2f92..a1f269008f8 100644 --- a/apps/sim/tools/workday/soap.ts +++ b/apps/sim/tools/workday/soap.ts @@ -34,12 +34,82 @@ export interface WorkdayReference { attributes?: Record } -interface WorkdayIdEntry { +export interface WorkdayIdEntry { $value?: string _?: string attributes?: Record } +/** + * 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 +} + +export interface WorkdayWorkerDataSoap { + Personal_Data?: Record + Employment_Data?: Record + Compensation_Data?: WorkdayCompensationDataSoap + Organization_Data?: Record +} + +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 + Per_Unit_Amount?: number + Individual_Target_Amount?: number + 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 +} + +export interface WorkdayOrganizationDataSoap { + Organization_Type_Reference?: WorkdayReference + Organization_Subtype_Reference?: WorkdayReference + Inactive?: boolean +} + +/** + * Normalizes a SOAP response field that may be a single object, an array, or undefined + * into a consistently typed array. + */ +export function normalizeSoapArray(value: T | T[] | undefined): T[] { + if (!value) return [] + return Array.isArray(value) ? value : [value] +} + type SoapOperationFn = ( args: Record ) => Promise<[WorkdaySoapResult, string, Record, string]> From 3d1e33a8bde326364e6e83d0a378a4d33959c652 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 18 Mar 2026 22:23:01 -0700 Subject: [PATCH 13/13] fix test --- apps/sim/app/api/files/utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/api/files/utils.test.ts b/apps/sim/app/api/files/utils.test.ts index 5180efd47ea..ff02212a808 100644 --- a/apps/sim/app/api/files/utils.test.ts +++ b/apps/sim/app/api/files/utils.test.ts @@ -412,7 +412,7 @@ describe('findLocalFile - Path Traversal Security Tests', () => { ] for (const input of legitimateInputs) { - await expect(findLocalFile(input)).resolves.not.toThrow() + await expect(findLocalFile(input)).resolves.toBeDefined() } } )