From 252fbfcb556f6e01eee3e970a55a0ae49a9ad880 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:28:50 +0000 Subject: [PATCH 1/3] Initial plan From 19bb4d0afca4174e7ac65941c3e5e04845ed4167 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:33:30 +0000 Subject: [PATCH 2/3] Add 8 new workflow action types: SMS, Slack, Teams, HTTP, Webhook, Task, Push, Script Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../references/data/CustomScriptAction.mdx | 15 + .../docs/references/data/HttpCallAction.mdx | 17 + .../data/PushNotificationAction.mdx | 18 + .../references/data/SlackMessageAction.mdx | 15 + .../references/data/SmsNotificationAction.mdx | 15 + .../references/data/TaskCreationAction.mdx | 19 + .../references/data/TeamsMessageAction.mdx | 15 + .../references/data/WebhookTriggerAction.mdx | 17 + .../data/automation/WorkflowRule.mdx | 2 +- .../spec/json-schema/CustomScriptAction.json | 49 ++ packages/spec/json-schema/HttpCallAction.json | 81 +++ .../json-schema/PushNotificationAction.json | 59 ++ .../spec/json-schema/SlackMessageAction.json | 45 ++ .../json-schema/SmsNotificationAction.json | 50 ++ .../spec/json-schema/TaskCreationAction.json | 59 ++ .../spec/json-schema/TeamsMessageAction.json | 45 ++ .../json-schema/WebhookTriggerAction.json | 58 ++ packages/spec/json-schema/WorkflowAction.json | 389 +++++++++++- packages/spec/json-schema/WorkflowRule.json | 389 +++++++++++- packages/spec/src/data/workflow.test.ts | 597 +++++++++++++++++- packages/spec/src/data/workflow.zod.ts | 128 +++- 21 files changed, 2060 insertions(+), 22 deletions(-) create mode 100644 content/docs/references/data/CustomScriptAction.mdx create mode 100644 content/docs/references/data/HttpCallAction.mdx create mode 100644 content/docs/references/data/PushNotificationAction.mdx create mode 100644 content/docs/references/data/SlackMessageAction.mdx create mode 100644 content/docs/references/data/SmsNotificationAction.mdx create mode 100644 content/docs/references/data/TaskCreationAction.mdx create mode 100644 content/docs/references/data/TeamsMessageAction.mdx create mode 100644 content/docs/references/data/WebhookTriggerAction.mdx create mode 100644 packages/spec/json-schema/CustomScriptAction.json create mode 100644 packages/spec/json-schema/HttpCallAction.json create mode 100644 packages/spec/json-schema/PushNotificationAction.json create mode 100644 packages/spec/json-schema/SlackMessageAction.json create mode 100644 packages/spec/json-schema/SmsNotificationAction.json create mode 100644 packages/spec/json-schema/TaskCreationAction.json create mode 100644 packages/spec/json-schema/TeamsMessageAction.json create mode 100644 packages/spec/json-schema/WebhookTriggerAction.json diff --git a/content/docs/references/data/CustomScriptAction.mdx b/content/docs/references/data/CustomScriptAction.mdx new file mode 100644 index 000000000..ce65c528f --- /dev/null +++ b/content/docs/references/data/CustomScriptAction.mdx @@ -0,0 +1,15 @@ +--- +title: CustomScriptAction +description: CustomScriptAction Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Action name | +| **type** | `string` | ✅ | | +| **language** | `Enum<'javascript' \| 'typescript' \| 'python'>` | optional | Script language | +| **code** | `string` | ✅ | Script code to execute | +| **timeout** | `number` | optional | Execution timeout in milliseconds | +| **context** | `Record` | optional | Additional context variables | diff --git a/content/docs/references/data/HttpCallAction.mdx b/content/docs/references/data/HttpCallAction.mdx new file mode 100644 index 000000000..e5bc884ef --- /dev/null +++ b/content/docs/references/data/HttpCallAction.mdx @@ -0,0 +1,17 @@ +--- +title: HttpCallAction +description: HttpCallAction Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Action name | +| **type** | `string` | ✅ | | +| **url** | `string` | ✅ | Target URL | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` | ✅ | HTTP method | +| **headers** | `Record` | optional | Request headers | +| **body** | `any` | optional | Request body (object/string) | +| **authentication** | `object` | optional | Authentication configuration | +| **timeout** | `number` | optional | Request timeout in milliseconds | diff --git a/content/docs/references/data/PushNotificationAction.mdx b/content/docs/references/data/PushNotificationAction.mdx new file mode 100644 index 000000000..77add310e --- /dev/null +++ b/content/docs/references/data/PushNotificationAction.mdx @@ -0,0 +1,18 @@ +--- +title: PushNotificationAction +description: PushNotificationAction Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Action name | +| **type** | `string` | ✅ | | +| **title** | `string` | ✅ | Notification title | +| **body** | `string` | ✅ | Notification body text | +| **recipients** | `string[]` | ✅ | User IDs or device tokens | +| **data** | `Record` | optional | Additional data payload | +| **badge** | `number` | optional | Badge count (iOS) | +| **sound** | `string` | optional | Notification sound | +| **clickAction** | `string` | optional | Action/URL when notification is clicked | diff --git a/content/docs/references/data/SlackMessageAction.mdx b/content/docs/references/data/SlackMessageAction.mdx new file mode 100644 index 000000000..5e634be8a --- /dev/null +++ b/content/docs/references/data/SlackMessageAction.mdx @@ -0,0 +1,15 @@ +--- +title: SlackMessageAction +description: SlackMessageAction Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Action name | +| **type** | `string` | ✅ | | +| **channel** | `string` | ✅ | Slack channel ID or name (#channel) | +| **message** | `string` | ✅ | Message text with optional markdown | +| **mentions** | `string[]` | optional | User IDs or @username to mention | +| **threadId** | `string` | optional | Thread ID for replies | diff --git a/content/docs/references/data/SmsNotificationAction.mdx b/content/docs/references/data/SmsNotificationAction.mdx new file mode 100644 index 000000000..9025b4e4a --- /dev/null +++ b/content/docs/references/data/SmsNotificationAction.mdx @@ -0,0 +1,15 @@ +--- +title: SmsNotificationAction +description: SmsNotificationAction Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Action name | +| **type** | `string` | ✅ | | +| **provider** | `Enum<'twilio' \| 'vonage'>` | ✅ | SMS provider | +| **recipients** | `string[]` | ✅ | List of phone numbers or user field references | +| **message** | `string` | ✅ | SMS message text or template | +| **fromNumber** | `string` | optional | Sender phone number (provider-specific) | diff --git a/content/docs/references/data/TaskCreationAction.mdx b/content/docs/references/data/TaskCreationAction.mdx new file mode 100644 index 000000000..7b55f6a17 --- /dev/null +++ b/content/docs/references/data/TaskCreationAction.mdx @@ -0,0 +1,19 @@ +--- +title: TaskCreationAction +description: TaskCreationAction Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Action name | +| **type** | `string` | ✅ | | +| **taskObject** | `string` | ✅ | Task object name (e.g., "task", "project_task") | +| **subject** | `string` | ✅ | Task subject/title | +| **description** | `string` | optional | Task description | +| **assignedTo** | `string` | optional | User ID or field reference for assignee | +| **dueDate** | `string` | optional | Due date (ISO string or formula) | +| **priority** | `string` | optional | Task priority | +| **relatedTo** | `string` | optional | Related record ID or field reference | +| **additionalFields** | `Record` | optional | Additional custom fields | diff --git a/content/docs/references/data/TeamsMessageAction.mdx b/content/docs/references/data/TeamsMessageAction.mdx new file mode 100644 index 000000000..71804dbf5 --- /dev/null +++ b/content/docs/references/data/TeamsMessageAction.mdx @@ -0,0 +1,15 @@ +--- +title: TeamsMessageAction +description: TeamsMessageAction Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Action name | +| **type** | `string` | ✅ | | +| **channel** | `string` | ✅ | Teams channel ID | +| **message** | `string` | ✅ | Message text with optional markdown | +| **mentions** | `string[]` | optional | User IDs to mention | +| **teamId** | `string` | optional | Team ID (if not in default team) | diff --git a/content/docs/references/data/WebhookTriggerAction.mdx b/content/docs/references/data/WebhookTriggerAction.mdx new file mode 100644 index 000000000..207882a30 --- /dev/null +++ b/content/docs/references/data/WebhookTriggerAction.mdx @@ -0,0 +1,17 @@ +--- +title: WebhookTriggerAction +description: WebhookTriggerAction Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Action name | +| **type** | `string` | ✅ | | +| **url** | `string` | ✅ | Webhook URL to call | +| **method** | `Enum<'POST' \| 'PUT'>` | optional | HTTP method | +| **headers** | `Record` | optional | Custom headers | +| **payload** | `any` | optional | Webhook payload (uses record data if not specified) | +| **retryOnFailure** | `boolean` | optional | Retry if webhook fails | +| **maxRetries** | `number` | optional | Maximum retry attempts | diff --git a/content/docs/references/data/automation/WorkflowRule.mdx b/content/docs/references/data/automation/WorkflowRule.mdx index 86545124f..ed68b9892 100644 --- a/content/docs/references/data/automation/WorkflowRule.mdx +++ b/content/docs/references/data/automation/WorkflowRule.mdx @@ -11,5 +11,5 @@ description: WorkflowRule Schema Reference | **objectName** | `string` | ✅ | Target Object | | **triggerType** | `Enum<'on_create' \| 'on_update' \| 'on_create_or_update' \| 'on_delete' \| 'schedule'>` | ✅ | When to evaluate | | **criteria** | `string` | optional | Formula condition. If TRUE, actions execute. | -| **actions** | `object \| object \| object[]` | optional | Immediate actions | +| **actions** | `object \| object \| object \| object \| object \| object \| object \| object \| object \| object[]` | optional | Immediate actions | | **active** | `boolean` | optional | Whether this workflow is active | diff --git a/packages/spec/json-schema/CustomScriptAction.json b/packages/spec/json-schema/CustomScriptAction.json new file mode 100644 index 000000000..09b9eacb5 --- /dev/null +++ b/packages/spec/json-schema/CustomScriptAction.json @@ -0,0 +1,49 @@ +{ + "$ref": "#/definitions/CustomScriptAction", + "definitions": { + "CustomScriptAction": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "custom_script" + }, + "language": { + "type": "string", + "enum": [ + "javascript", + "typescript", + "python" + ], + "default": "javascript", + "description": "Script language" + }, + "code": { + "type": "string", + "description": "Script code to execute" + }, + "timeout": { + "type": "number", + "default": 30000, + "description": "Execution timeout in milliseconds" + }, + "context": { + "type": "object", + "additionalProperties": {}, + "description": "Additional context variables" + } + }, + "required": [ + "name", + "type", + "code" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/HttpCallAction.json b/packages/spec/json-schema/HttpCallAction.json new file mode 100644 index 000000000..e6c62175a --- /dev/null +++ b/packages/spec/json-schema/HttpCallAction.json @@ -0,0 +1,81 @@ +{ + "$ref": "#/definitions/HttpCallAction", + "definitions": { + "HttpCallAction": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "http_call" + }, + "url": { + "type": "string", + "description": "Target URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Request headers" + }, + "body": { + "description": "Request body (object/string)" + }, + "authentication": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "none", + "basic", + "bearer", + "api_key", + "oauth2" + ] + }, + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Authentication configuration" + }, + "timeout": { + "type": "number", + "description": "Request timeout in milliseconds" + } + }, + "required": [ + "name", + "type", + "url", + "method" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/PushNotificationAction.json b/packages/spec/json-schema/PushNotificationAction.json new file mode 100644 index 000000000..17c5d8a17 --- /dev/null +++ b/packages/spec/json-schema/PushNotificationAction.json @@ -0,0 +1,59 @@ +{ + "$ref": "#/definitions/PushNotificationAction", + "definitions": { + "PushNotificationAction": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "push_notification" + }, + "title": { + "type": "string", + "description": "Notification title" + }, + "body": { + "type": "string", + "description": "Notification body text" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs or device tokens" + }, + "data": { + "type": "object", + "additionalProperties": {}, + "description": "Additional data payload" + }, + "badge": { + "type": "number", + "description": "Badge count (iOS)" + }, + "sound": { + "type": "string", + "description": "Notification sound" + }, + "clickAction": { + "type": "string", + "description": "Action/URL when notification is clicked" + } + }, + "required": [ + "name", + "type", + "title", + "body", + "recipients" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/SlackMessageAction.json b/packages/spec/json-schema/SlackMessageAction.json new file mode 100644 index 000000000..b1c8adef4 --- /dev/null +++ b/packages/spec/json-schema/SlackMessageAction.json @@ -0,0 +1,45 @@ +{ + "$ref": "#/definitions/SlackMessageAction", + "definitions": { + "SlackMessageAction": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "slack_message" + }, + "channel": { + "type": "string", + "description": "Slack channel ID or name (#channel)" + }, + "message": { + "type": "string", + "description": "Message text with optional markdown" + }, + "mentions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs or @username to mention" + }, + "threadId": { + "type": "string", + "description": "Thread ID for replies" + } + }, + "required": [ + "name", + "type", + "channel", + "message" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/SmsNotificationAction.json b/packages/spec/json-schema/SmsNotificationAction.json new file mode 100644 index 000000000..7053c0b08 --- /dev/null +++ b/packages/spec/json-schema/SmsNotificationAction.json @@ -0,0 +1,50 @@ +{ + "$ref": "#/definitions/SmsNotificationAction", + "definitions": { + "SmsNotificationAction": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "sms_notification" + }, + "provider": { + "type": "string", + "enum": [ + "twilio", + "vonage" + ], + "description": "SMS provider" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of phone numbers or user field references" + }, + "message": { + "type": "string", + "description": "SMS message text or template" + }, + "fromNumber": { + "type": "string", + "description": "Sender phone number (provider-specific)" + } + }, + "required": [ + "name", + "type", + "provider", + "recipients", + "message" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/TaskCreationAction.json b/packages/spec/json-schema/TaskCreationAction.json new file mode 100644 index 000000000..827a00b6d --- /dev/null +++ b/packages/spec/json-schema/TaskCreationAction.json @@ -0,0 +1,59 @@ +{ + "$ref": "#/definitions/TaskCreationAction", + "definitions": { + "TaskCreationAction": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "task_creation" + }, + "taskObject": { + "type": "string", + "description": "Task object name (e.g., \"task\", \"project_task\")" + }, + "subject": { + "type": "string", + "description": "Task subject/title" + }, + "description": { + "type": "string", + "description": "Task description" + }, + "assignedTo": { + "type": "string", + "description": "User ID or field reference for assignee" + }, + "dueDate": { + "type": "string", + "description": "Due date (ISO string or formula)" + }, + "priority": { + "type": "string", + "description": "Task priority" + }, + "relatedTo": { + "type": "string", + "description": "Related record ID or field reference" + }, + "additionalFields": { + "type": "object", + "additionalProperties": {}, + "description": "Additional custom fields" + } + }, + "required": [ + "name", + "type", + "taskObject", + "subject" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/TeamsMessageAction.json b/packages/spec/json-schema/TeamsMessageAction.json new file mode 100644 index 000000000..2f17241d4 --- /dev/null +++ b/packages/spec/json-schema/TeamsMessageAction.json @@ -0,0 +1,45 @@ +{ + "$ref": "#/definitions/TeamsMessageAction", + "definitions": { + "TeamsMessageAction": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "teams_message" + }, + "channel": { + "type": "string", + "description": "Teams channel ID" + }, + "message": { + "type": "string", + "description": "Message text with optional markdown" + }, + "mentions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs to mention" + }, + "teamId": { + "type": "string", + "description": "Team ID (if not in default team)" + } + }, + "required": [ + "name", + "type", + "channel", + "message" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/WebhookTriggerAction.json b/packages/spec/json-schema/WebhookTriggerAction.json new file mode 100644 index 000000000..67075e893 --- /dev/null +++ b/packages/spec/json-schema/WebhookTriggerAction.json @@ -0,0 +1,58 @@ +{ + "$ref": "#/definitions/WebhookTriggerAction", + "definitions": { + "WebhookTriggerAction": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "webhook_trigger" + }, + "url": { + "type": "string", + "description": "Webhook URL to call" + }, + "method": { + "type": "string", + "enum": [ + "POST", + "PUT" + ], + "default": "POST", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom headers" + }, + "payload": { + "description": "Webhook payload (uses record data if not specified)" + }, + "retryOnFailure": { + "type": "boolean", + "default": true, + "description": "Retry if webhook fails" + }, + "maxRetries": { + "type": "number", + "default": 3, + "description": "Maximum retry attempts" + } + }, + "required": [ + "name", + "type", + "url" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/WorkflowAction.json b/packages/spec/json-schema/WorkflowAction.json index 4f4c00c09..e12e88b91 100644 --- a/packages/spec/json-schema/WorkflowAction.json +++ b/packages/spec/json-schema/WorkflowAction.json @@ -64,16 +64,397 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "sms_notification" + }, + "provider": { + "type": "string", + "enum": [ + "twilio", + "vonage" + ], + "description": "SMS provider" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of phone numbers or user field references" + }, + "message": { + "type": "string", + "description": "SMS message text or template" + }, + "fromNumber": { + "type": "string", + "description": "Sender phone number (provider-specific)" + } + }, + "required": [ + "name", + "type", + "provider", + "recipients", + "message" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "slack_message" + }, + "channel": { + "type": "string", + "description": "Slack channel ID or name (#channel)" + }, + "message": { + "type": "string", + "description": "Message text with optional markdown" + }, + "mentions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs or @username to mention" + }, + "threadId": { + "type": "string", + "description": "Thread ID for replies" + } + }, + "required": [ + "name", + "type", + "channel", + "message" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "teams_message" + }, + "channel": { + "type": "string", + "description": "Teams channel ID" + }, + "message": { + "type": "string", + "description": "Message text with optional markdown" + }, + "mentions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs to mention" + }, + "teamId": { + "type": "string", + "description": "Team ID (if not in default team)" + } + }, + "required": [ + "name", + "type", + "channel", + "message" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "http_call" + }, + "url": { + "type": "string", + "description": "Target URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Request headers" + }, + "body": { + "description": "Request body (object/string)" + }, + "authentication": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "none", + "basic", + "bearer", + "api_key", + "oauth2" + ] + }, + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Authentication configuration" + }, + "timeout": { + "type": "number", + "description": "Request timeout in milliseconds" + } + }, + "required": [ + "name", + "type", + "url", + "method" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "webhook_trigger" + }, + "url": { + "type": "string", + "description": "Webhook URL to call" + }, + "method": { + "type": "string", + "enum": [ + "POST", + "PUT" + ], + "default": "POST", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom headers" + }, + "payload": { + "description": "Webhook payload (uses record data if not specified)" + }, + "retryOnFailure": { + "type": "boolean", + "default": true, + "description": "Retry if webhook fails" + }, + "maxRetries": { + "type": "number", + "default": 3, + "description": "Maximum retry attempts" + } + }, + "required": [ + "name", + "type", + "url" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "task_creation" + }, + "taskObject": { + "type": "string", + "description": "Task object name (e.g., \"task\", \"project_task\")" + }, + "subject": { + "type": "string", + "description": "Task subject/title" + }, + "description": { + "type": "string", + "description": "Task description" + }, + "assignedTo": { + "type": "string", + "description": "User ID or field reference for assignee" + }, + "dueDate": { + "type": "string", + "description": "Due date (ISO string or formula)" + }, + "priority": { + "type": "string", + "description": "Task priority" + }, + "relatedTo": { + "type": "string", + "description": "Related record ID or field reference" + }, + "additionalFields": { + "type": "object", + "additionalProperties": {}, + "description": "Additional custom fields" + } + }, + "required": [ + "name", + "type", + "taskObject", + "subject" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "push_notification" + }, + "title": { + "type": "string", + "description": "Notification title" + }, + "body": { + "type": "string", + "description": "Notification body text" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs or device tokens" + }, + "data": { + "type": "object", + "additionalProperties": {}, + "description": "Additional data payload" + }, + "badge": { + "type": "number", + "description": "Badge count (iOS)" + }, + "sound": { + "type": "string", + "description": "Notification sound" + }, + "clickAction": { + "type": "string", + "description": "Action/URL when notification is clicked" + } + }, + "required": [ + "name", + "type", + "title", + "body", + "recipients" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" }, "type": { - "type": "string" + "type": "string", + "const": "custom_script" }, - "options": {} + "language": { + "type": "string", + "enum": [ + "javascript", + "typescript", + "python" + ], + "default": "javascript", + "description": "Script language" + }, + "code": { + "type": "string", + "description": "Script code to execute" + }, + "timeout": { + "type": "number", + "default": 30000, + "description": "Execution timeout in milliseconds" + }, + "context": { + "type": "object", + "additionalProperties": {}, + "description": "Additional context variables" + } }, "required": [ "name", - "type" + "type", + "code" ], "additionalProperties": false } diff --git a/packages/spec/json-schema/WorkflowRule.json b/packages/spec/json-schema/WorkflowRule.json index 1b4d35cc8..64c0f5f64 100644 --- a/packages/spec/json-schema/WorkflowRule.json +++ b/packages/spec/json-schema/WorkflowRule.json @@ -93,16 +93,397 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "sms_notification" + }, + "provider": { + "type": "string", + "enum": [ + "twilio", + "vonage" + ], + "description": "SMS provider" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of phone numbers or user field references" + }, + "message": { + "type": "string", + "description": "SMS message text or template" + }, + "fromNumber": { + "type": "string", + "description": "Sender phone number (provider-specific)" + } + }, + "required": [ + "name", + "type", + "provider", + "recipients", + "message" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "slack_message" + }, + "channel": { + "type": "string", + "description": "Slack channel ID or name (#channel)" + }, + "message": { + "type": "string", + "description": "Message text with optional markdown" + }, + "mentions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs or @username to mention" + }, + "threadId": { + "type": "string", + "description": "Thread ID for replies" + } + }, + "required": [ + "name", + "type", + "channel", + "message" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "teams_message" + }, + "channel": { + "type": "string", + "description": "Teams channel ID" + }, + "message": { + "type": "string", + "description": "Message text with optional markdown" + }, + "mentions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs to mention" + }, + "teamId": { + "type": "string", + "description": "Team ID (if not in default team)" + } + }, + "required": [ + "name", + "type", + "channel", + "message" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "http_call" + }, + "url": { + "type": "string", + "description": "Target URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Request headers" + }, + "body": { + "description": "Request body (object/string)" + }, + "authentication": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "none", + "basic", + "bearer", + "api_key", + "oauth2" + ] + }, + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "description": "Authentication configuration" + }, + "timeout": { + "type": "number", + "description": "Request timeout in milliseconds" + } + }, + "required": [ + "name", + "type", + "url", + "method" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "webhook_trigger" + }, + "url": { + "type": "string", + "description": "Webhook URL to call" + }, + "method": { + "type": "string", + "enum": [ + "POST", + "PUT" + ], + "default": "POST", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom headers" + }, + "payload": { + "description": "Webhook payload (uses record data if not specified)" + }, + "retryOnFailure": { + "type": "boolean", + "default": true, + "description": "Retry if webhook fails" + }, + "maxRetries": { + "type": "number", + "default": 3, + "description": "Maximum retry attempts" + } + }, + "required": [ + "name", + "type", + "url" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "task_creation" + }, + "taskObject": { + "type": "string", + "description": "Task object name (e.g., \"task\", \"project_task\")" + }, + "subject": { + "type": "string", + "description": "Task subject/title" + }, + "description": { + "type": "string", + "description": "Task description" + }, + "assignedTo": { + "type": "string", + "description": "User ID or field reference for assignee" + }, + "dueDate": { + "type": "string", + "description": "Due date (ISO string or formula)" + }, + "priority": { + "type": "string", + "description": "Task priority" + }, + "relatedTo": { + "type": "string", + "description": "Related record ID or field reference" + }, + "additionalFields": { + "type": "object", + "additionalProperties": {}, + "description": "Additional custom fields" + } + }, + "required": [ + "name", + "type", + "taskObject", + "subject" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" + }, + "type": { + "type": "string", + "const": "push_notification" + }, + "title": { + "type": "string", + "description": "Notification title" + }, + "body": { + "type": "string", + "description": "Notification body text" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + }, + "description": "User IDs or device tokens" + }, + "data": { + "type": "object", + "additionalProperties": {}, + "description": "Additional data payload" + }, + "badge": { + "type": "number", + "description": "Badge count (iOS)" + }, + "sound": { + "type": "string", + "description": "Notification sound" + }, + "clickAction": { + "type": "string", + "description": "Action/URL when notification is clicked" + } + }, + "required": [ + "name", + "type", + "title", + "body", + "recipients" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Action name" }, "type": { - "type": "string" + "type": "string", + "const": "custom_script" }, - "options": {} + "language": { + "type": "string", + "enum": [ + "javascript", + "typescript", + "python" + ], + "default": "javascript", + "description": "Script language" + }, + "code": { + "type": "string", + "description": "Script code to execute" + }, + "timeout": { + "type": "number", + "default": 30000, + "description": "Execution timeout in milliseconds" + }, + "context": { + "type": "object", + "additionalProperties": {}, + "description": "Additional context variables" + } }, "required": [ "name", - "type" + "type", + "code" ], "additionalProperties": false } diff --git a/packages/spec/src/data/workflow.test.ts b/packages/spec/src/data/workflow.test.ts index 9d2e4d1a0..4ddeec6a3 100644 --- a/packages/spec/src/data/workflow.test.ts +++ b/packages/spec/src/data/workflow.test.ts @@ -4,6 +4,14 @@ import { WorkflowTriggerType, FieldUpdateActionSchema, EmailAlertActionSchema, + SmsNotificationActionSchema, + SlackMessageActionSchema, + TeamsMessageActionSchema, + HttpCallActionSchema, + WebhookTriggerActionSchema, + TaskCreationActionSchema, + PushNotificationActionSchema, + CustomScriptActionSchema, WorkflowActionSchema, type WorkflowRule, } from './workflow.zod'; @@ -71,6 +79,318 @@ describe('EmailAlertActionSchema', () => { }); }); +describe('SmsNotificationActionSchema', () => { + it('should accept SMS notification with Twilio', () => { + const action = { + name: 'send_sms_alert', + type: 'sms_notification' as const, + provider: 'twilio' as const, + recipients: ['+1234567890', '{owner.phone}'], + message: 'Your order has been shipped!', + fromNumber: '+0987654321', + }; + + expect(() => SmsNotificationActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept SMS notification with Vonage', () => { + const action = { + name: 'send_alert', + type: 'sms_notification' as const, + provider: 'vonage' as const, + recipients: ['+1234567890'], + message: 'Alert: High priority case assigned', + }; + + expect(() => SmsNotificationActionSchema.parse(action)).not.toThrow(); + }); + + it('should reject invalid provider', () => { + const action = { + name: 'send_sms', + type: 'sms_notification' as const, + provider: 'invalid_provider', + recipients: ['+1234567890'], + message: 'Test message', + }; + + expect(() => SmsNotificationActionSchema.parse(action)).toThrow(); + }); +}); + +describe('SlackMessageActionSchema', () => { + it('should accept basic Slack message', () => { + const action = { + name: 'notify_slack', + type: 'slack_message' as const, + channel: '#general', + message: 'New lead created!', + }; + + expect(() => SlackMessageActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept Slack message with mentions', () => { + const action = { + name: 'notify_team', + type: 'slack_message' as const, + channel: 'C1234567890', + message: 'Urgent: High value deal needs attention', + mentions: ['@john', '@jane', 'U9876543210'], + }; + + expect(() => SlackMessageActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept Slack message in thread', () => { + const action = { + name: 'reply_thread', + type: 'slack_message' as const, + channel: '#deals', + message: 'Update: Deal closed!', + threadId: '1234567890.123456', + }; + + expect(() => SlackMessageActionSchema.parse(action)).not.toThrow(); + }); +}); + +describe('TeamsMessageActionSchema', () => { + it('should accept basic Teams message', () => { + const action = { + name: 'notify_teams', + type: 'teams_message' as const, + channel: 'channel-id-123', + message: 'New case assigned to your team', + }; + + expect(() => TeamsMessageActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept Teams message with mentions and team', () => { + const action = { + name: 'escalate_teams', + type: 'teams_message' as const, + channel: 'channel-id-456', + message: '**Critical Issue**: Immediate attention required', + mentions: ['user-id-1', 'user-id-2'], + teamId: 'team-id-789', + }; + + expect(() => TeamsMessageActionSchema.parse(action)).not.toThrow(); + }); +}); + +describe('HttpCallActionSchema', () => { + it('should accept basic GET request', () => { + const action = { + name: 'fetch_data', + type: 'http_call' as const, + url: 'https://api.example.com/data', + method: 'GET' as const, + }; + + expect(() => HttpCallActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept POST request with body and headers', () => { + const action = { + name: 'send_data', + type: 'http_call' as const, + url: 'https://api.example.com/create', + method: 'POST' as const, + headers: { + 'Content-Type': 'application/json', + 'X-API-Version': 'v1', + }, + body: { + name: '{record.name}', + value: '{record.value}', + }, + }; + + expect(() => HttpCallActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept request with authentication', () => { + const action = { + name: 'api_call', + type: 'http_call' as const, + url: 'https://api.example.com/secure', + method: 'POST' as const, + authentication: { + type: 'bearer' as const, + credentials: { + token: '{$Credential.ApiToken}', + }, + }, + timeout: 5000, + }; + + expect(() => HttpCallActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept all HTTP methods', () => { + const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const; + + methods.forEach(method => { + const action = { + name: `test_${method.toLowerCase()}`, + type: 'http_call' as const, + url: 'https://api.example.com/test', + method, + }; + expect(() => HttpCallActionSchema.parse(action)).not.toThrow(); + }); + }); +}); + +describe('WebhookTriggerActionSchema', () => { + it('should accept basic webhook trigger', () => { + const action = { + name: 'trigger_webhook', + type: 'webhook_trigger' as const, + url: 'https://webhook.site/unique-id', + }; + + const result = WebhookTriggerActionSchema.parse(action); + expect(result.method).toBe('POST'); + expect(result.retryOnFailure).toBe(true); + expect(result.maxRetries).toBe(3); + }); + + it('should accept webhook with custom configuration', () => { + const action = { + name: 'custom_webhook', + type: 'webhook_trigger' as const, + url: 'https://api.example.com/webhook', + method: 'PUT' as const, + headers: { + 'X-Webhook-Secret': 'secret-key', + }, + payload: { + event: 'record.created', + data: '{record}', + }, + retryOnFailure: false, + maxRetries: 5, + }; + + expect(() => WebhookTriggerActionSchema.parse(action)).not.toThrow(); + }); +}); + +describe('TaskCreationActionSchema', () => { + it('should accept minimal task creation', () => { + const action = { + name: 'create_followup_task', + type: 'task_creation' as const, + taskObject: 'task', + subject: 'Follow up with customer', + }; + + expect(() => TaskCreationActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept complete task creation', () => { + const action = { + name: 'create_project_task', + type: 'task_creation' as const, + taskObject: 'project_task', + subject: 'Review and approve proposal', + description: 'Please review the proposal and provide feedback', + assignedTo: '{owner.manager_id}', + dueDate: 'TODAY() + 7', + priority: 'high', + relatedTo: '{record.id}', + additionalFields: { + project_id: '{record.project_id}', + estimated_hours: 4, + }, + }; + + expect(() => TaskCreationActionSchema.parse(action)).not.toThrow(); + }); +}); + +describe('PushNotificationActionSchema', () => { + it('should accept basic push notification', () => { + const action = { + name: 'send_push', + type: 'push_notification' as const, + title: 'New Message', + body: 'You have a new message from support', + recipients: ['user-123', 'user-456'], + }; + + expect(() => PushNotificationActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept push notification with full configuration', () => { + const action = { + name: 'send_rich_push', + type: 'push_notification' as const, + title: 'Order Shipped', + body: 'Your order #12345 has been shipped!', + recipients: ['{customer.device_token}'], + data: { + orderId: '{record.id}', + trackingNumber: '{record.tracking_number}', + }, + badge: 1, + sound: 'notification.wav', + clickAction: '/orders/{record.id}', + }; + + expect(() => PushNotificationActionSchema.parse(action)).not.toThrow(); + }); +}); + +describe('CustomScriptActionSchema', () => { + it('should accept JavaScript script with defaults', () => { + const action = { + name: 'run_custom_logic', + type: 'custom_script' as const, + code: 'console.log("Hello from workflow");', + }; + + const result = CustomScriptActionSchema.parse(action); + expect(result.language).toBe('javascript'); + expect(result.timeout).toBe(30000); + }); + + it('should accept TypeScript script', () => { + const action = { + name: 'calculate_score', + type: 'custom_script' as const, + language: 'typescript' as const, + code: ` + const score = record.value1 * 0.3 + record.value2 * 0.7; + return { score }; + `, + timeout: 10000, + context: { + record: '{record}', + user: '{$User}', + }, + }; + + expect(() => CustomScriptActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept Python script', () => { + const action = { + name: 'data_processing', + type: 'custom_script' as const, + language: 'python' as const, + code: 'import json\nresult = json.dumps({"processed": True})', + timeout: 60000, + }; + + expect(() => CustomScriptActionSchema.parse(action)).not.toThrow(); + }); +}); + describe('WorkflowActionSchema', () => { it('should accept field update action', () => { const action = { @@ -94,18 +414,103 @@ describe('WorkflowActionSchema', () => { expect(() => WorkflowActionSchema.parse(action)).not.toThrow(); }); - it('should accept generic action with custom type', () => { + it('should accept SMS notification action', () => { const action = { - name: 'custom_action', - type: 'webhook', - options: { - url: 'https://api.example.com/webhook', - method: 'POST', - }, + name: 'send_sms', + type: 'sms_notification' as const, + provider: 'twilio' as const, + recipients: ['+1234567890'], + message: 'Test message', + }; + + expect(() => WorkflowActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept Slack message action', () => { + const action = { + name: 'send_slack', + type: 'slack_message' as const, + channel: '#general', + message: 'Test message', + }; + + expect(() => WorkflowActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept Teams message action', () => { + const action = { + name: 'send_teams', + type: 'teams_message' as const, + channel: 'channel-id', + message: 'Test message', + }; + + expect(() => WorkflowActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept HTTP call action', () => { + const action = { + name: 'api_call', + type: 'http_call' as const, + url: 'https://api.example.com/endpoint', + method: 'POST' as const, + }; + + expect(() => WorkflowActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept webhook trigger action', () => { + const action = { + name: 'trigger_webhook', + type: 'webhook_trigger' as const, + url: 'https://webhook.example.com', + }; + + expect(() => WorkflowActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept task creation action', () => { + const action = { + name: 'create_task', + type: 'task_creation' as const, + taskObject: 'task', + subject: 'Follow up', }; expect(() => WorkflowActionSchema.parse(action)).not.toThrow(); }); + + it('should accept push notification action', () => { + const action = { + name: 'send_push', + type: 'push_notification' as const, + title: 'Notification', + body: 'You have a new update', + recipients: ['user-123'], + }; + + expect(() => WorkflowActionSchema.parse(action)).not.toThrow(); + }); + + it('should accept custom script action', () => { + const action = { + name: 'run_script', + type: 'custom_script' as const, + code: 'console.log("test");', + }; + + expect(() => WorkflowActionSchema.parse(action)).not.toThrow(); + }); + + it('should reject action with invalid type', () => { + const action = { + name: 'invalid_action', + type: 'invalid_type', + options: {}, + }; + + expect(() => WorkflowActionSchema.parse(action)).toThrow(); + }); }); describe('WorkflowRuleSchema', () => { @@ -358,5 +763,183 @@ describe('WorkflowRuleSchema', () => { expect(() => WorkflowRuleSchema.parse(workflow)).not.toThrow(); }); + + it('should accept multi-channel notification workflow', () => { + const workflow: WorkflowRule = { + name: 'notify_high_value_deal', + objectName: 'opportunity', + triggerType: 'on_create_or_update', + criteria: 'amount > 100000', + actions: [ + { + name: 'send_email', + type: 'email_alert', + template: 'high_value_deal_alert', + recipients: ['sales-director@example.com'], + }, + { + name: 'send_sms', + type: 'sms_notification', + provider: 'twilio', + recipients: ['{sales_director.phone}'], + message: 'High value deal alert: ${record.name} - $${record.amount}', + }, + { + name: 'post_slack', + type: 'slack_message', + channel: '#sales-wins', + message: '🎉 New high-value opportunity: *${record.name}* worth $${record.amount}', + mentions: ['@sales-team'], + }, + { + name: 'send_push', + type: 'push_notification', + title: 'High Value Deal', + body: 'New opportunity: ${record.name}', + recipients: ['{sales_director.device_token}'], + }, + ], + }; + + expect(() => WorkflowRuleSchema.parse(workflow)).not.toThrow(); + }); + + it('should accept webhook integration workflow', () => { + const workflow: WorkflowRule = { + name: 'sync_to_external_system', + objectName: 'order', + triggerType: 'on_create', + actions: [ + { + name: 'call_inventory_api', + type: 'http_call', + url: 'https://inventory.example.com/api/reserve', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: { + order_id: '{record.id}', + items: '{record.line_items}', + }, + authentication: { + type: 'bearer', + credentials: { + token: '{$Credential.InventoryApiToken}', + }, + }, + }, + { + name: 'trigger_fulfillment_webhook', + type: 'webhook_trigger', + url: 'https://fulfillment.example.com/webhook/new-order', + payload: { + order: '{record}', + }, + }, + ], + }; + + expect(() => WorkflowRuleSchema.parse(workflow)).not.toThrow(); + }); + + it('should accept task creation workflow', () => { + const workflow: WorkflowRule = { + name: 'create_followup_tasks', + objectName: 'lead', + triggerType: 'on_create', + criteria: 'status == "qualified"', + actions: [ + { + name: 'create_initial_contact_task', + type: 'task_creation', + taskObject: 'task', + subject: 'Initial contact with ${record.company}', + description: 'Reach out to discuss their needs', + assignedTo: '{owner.id}', + dueDate: 'TODAY() + 1', + priority: 'high', + relatedTo: '{record.id}', + }, + { + name: 'create_followup_task', + type: 'task_creation', + taskObject: 'task', + subject: 'Follow up with ${record.company}', + assignedTo: '{owner.id}', + dueDate: 'TODAY() + 7', + priority: 'normal', + relatedTo: '{record.id}', + }, + ], + }; + + expect(() => WorkflowRuleSchema.parse(workflow)).not.toThrow(); + }); + + it('should accept custom script workflow', () => { + const workflow: WorkflowRule = { + name: 'calculate_complex_metrics', + objectName: 'project', + triggerType: 'on_update', + criteria: 'status == "in_progress"', + actions: [ + { + name: 'calculate_health_score', + type: 'custom_script', + language: 'javascript', + code: ` + const completion = record.tasks_completed / record.total_tasks; + const timeElapsed = (Date.now() - record.start_date) / (record.end_date - record.start_date); + const healthScore = (completion / timeElapsed) * 100; + return { health_score: Math.min(100, Math.max(0, healthScore)) }; + `, + timeout: 5000, + context: { + record: '{record}', + }, + }, + { + name: 'update_health_score', + type: 'field_update', + field: 'health_score', + value: '{$Script.health_score}', + }, + ], + }; + + expect(() => WorkflowRuleSchema.parse(workflow)).not.toThrow(); + }); + + it('should accept Teams notification workflow', () => { + const workflow: WorkflowRule = { + name: 'notify_teams_on_escalation', + objectName: 'support_ticket', + triggerType: 'on_update', + criteria: 'is_escalated == true', + actions: [ + { + name: 'post_to_teams', + type: 'teams_message', + channel: 'escalations-channel', + message: '⚠️ **Escalated Ticket**: ${record.subject}\n\nCustomer: ${record.customer_name}\nPriority: ${record.priority}', + mentions: ['{record.assigned_to}', '{team_lead.id}'], + teamId: 'support-team', + }, + { + name: 'create_escalation_task', + type: 'task_creation', + taskObject: 'task', + subject: 'Resolve escalated ticket: ${record.subject}', + assignedTo: '{team_lead.id}', + dueDate: 'NOW() + 4 HOURS', + priority: 'critical', + relatedTo: '{record.id}', + }, + ], + }; + + expect(() => WorkflowRuleSchema.parse(workflow)).not.toThrow(); + }); }); }); diff --git a/packages/spec/src/data/workflow.zod.ts b/packages/spec/src/data/workflow.zod.ts index 0f683b4e3..755820f94 100644 --- a/packages/spec/src/data/workflow.zod.ts +++ b/packages/spec/src/data/workflow.zod.ts @@ -31,17 +31,133 @@ export const EmailAlertActionSchema = z.object({ recipients: z.array(z.string()).describe('List of recipient emails or user IDs'), }); +/** + * Schema for Workflow SMS Notification Action + * Supports providers like Twilio, Vonage + */ +export const SmsNotificationActionSchema = z.object({ + name: z.string().describe('Action name'), + type: z.literal('sms_notification'), + provider: z.enum(['twilio', 'vonage']).describe('SMS provider'), + recipients: z.array(z.string()).describe('List of phone numbers or user field references'), + message: z.string().describe('SMS message text or template'), + fromNumber: z.string().optional().describe('Sender phone number (provider-specific)'), +}); + +/** + * Schema for Workflow Slack Message Action + */ +export const SlackMessageActionSchema = z.object({ + name: z.string().describe('Action name'), + type: z.literal('slack_message'), + channel: z.string().describe('Slack channel ID or name (#channel)'), + message: z.string().describe('Message text with optional markdown'), + mentions: z.array(z.string()).optional().describe('User IDs or @username to mention'), + threadId: z.string().optional().describe('Thread ID for replies'), +}); + +/** + * Schema for Workflow Teams Message Action + */ +export const TeamsMessageActionSchema = z.object({ + name: z.string().describe('Action name'), + type: z.literal('teams_message'), + channel: z.string().describe('Teams channel ID'), + message: z.string().describe('Message text with optional markdown'), + mentions: z.array(z.string()).optional().describe('User IDs to mention'), + teamId: z.string().optional().describe('Team ID (if not in default team)'), +}); + +/** + * Schema for Workflow HTTP Call Action + * REST API integration support + */ +export const HttpCallActionSchema = z.object({ + name: z.string().describe('Action name'), + type: z.literal('http_call'), + url: z.string().describe('Target URL'), + method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).describe('HTTP method'), + headers: z.record(z.string()).optional().describe('Request headers'), + body: z.any().optional().describe('Request body (object/string)'), + authentication: z.object({ + type: z.enum(['none', 'basic', 'bearer', 'api_key', 'oauth2']), + credentials: z.record(z.string()).optional(), + }).optional().describe('Authentication configuration'), + timeout: z.number().optional().describe('Request timeout in milliseconds'), +}); + +/** + * Schema for Workflow Webhook Trigger Action + */ +export const WebhookTriggerActionSchema = z.object({ + name: z.string().describe('Action name'), + type: z.literal('webhook_trigger'), + url: z.string().describe('Webhook URL to call'), + method: z.enum(['POST', 'PUT']).default('POST').describe('HTTP method'), + headers: z.record(z.string()).optional().describe('Custom headers'), + payload: z.any().optional().describe('Webhook payload (uses record data if not specified)'), + retryOnFailure: z.boolean().default(true).describe('Retry if webhook fails'), + maxRetries: z.number().default(3).describe('Maximum retry attempts'), +}); + +/** + * Schema for Workflow Task Creation Action + */ +export const TaskCreationActionSchema = z.object({ + name: z.string().describe('Action name'), + type: z.literal('task_creation'), + taskObject: z.string().describe('Task object name (e.g., "task", "project_task")'), + subject: z.string().describe('Task subject/title'), + description: z.string().optional().describe('Task description'), + assignedTo: z.string().optional().describe('User ID or field reference for assignee'), + dueDate: z.string().optional().describe('Due date (ISO string or formula)'), + priority: z.string().optional().describe('Task priority'), + relatedTo: z.string().optional().describe('Related record ID or field reference'), + additionalFields: z.record(z.any()).optional().describe('Additional custom fields'), +}); + +/** + * Schema for Workflow Push Notification Action + */ +export const PushNotificationActionSchema = z.object({ + name: z.string().describe('Action name'), + type: z.literal('push_notification'), + title: z.string().describe('Notification title'), + body: z.string().describe('Notification body text'), + recipients: z.array(z.string()).describe('User IDs or device tokens'), + data: z.record(z.any()).optional().describe('Additional data payload'), + badge: z.number().optional().describe('Badge count (iOS)'), + sound: z.string().optional().describe('Notification sound'), + clickAction: z.string().optional().describe('Action/URL when notification is clicked'), +}); + +/** + * Schema for Workflow Custom Script Action + */ +export const CustomScriptActionSchema = z.object({ + name: z.string().describe('Action name'), + type: z.literal('custom_script'), + language: z.enum(['javascript', 'typescript', 'python']).default('javascript').describe('Script language'), + code: z.string().describe('Script code to execute'), + timeout: z.number().default(30000).describe('Execution timeout in milliseconds'), + context: z.record(z.any()).optional().describe('Additional context variables'), +}); + /** * Generic Workflow Action Wrapper + * Supports 10+ action types for comprehensive automation */ -export const WorkflowActionSchema = z.union([ +export const WorkflowActionSchema = z.discriminatedUnion('type', [ FieldUpdateActionSchema, EmailAlertActionSchema, - z.object({ - name: z.string(), - type: z.string(), - options: z.any() - }) + SmsNotificationActionSchema, + SlackMessageActionSchema, + TeamsMessageActionSchema, + HttpCallActionSchema, + WebhookTriggerActionSchema, + TaskCreationActionSchema, + PushNotificationActionSchema, + CustomScriptActionSchema, ]); /** From 121911c94f342a09931993b8620375d574aff575 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:34:09 +0000 Subject: [PATCH 3/3] Update comment to reflect exact count of 10 action types Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/spec/src/data/workflow.zod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spec/src/data/workflow.zod.ts b/packages/spec/src/data/workflow.zod.ts index 755820f94..6abbb86eb 100644 --- a/packages/spec/src/data/workflow.zod.ts +++ b/packages/spec/src/data/workflow.zod.ts @@ -145,7 +145,7 @@ export const CustomScriptActionSchema = z.object({ /** * Generic Workflow Action Wrapper - * Supports 10+ action types for comprehensive automation + * Supports 10 action types for comprehensive automation */ export const WorkflowActionSchema = z.discriminatedUnion('type', [ FieldUpdateActionSchema,