From 3fd06675d03f91f8c409aa7d1cbd7811b23781ba Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 12:15:45 -0700 Subject: [PATCH 1/2] feat(temporal): add Temporal integration with workflow, schedule, and task queue tools --- apps/docs/components/icons.tsx | 11 + apps/docs/components/ui/icon-mapping.ts | 2 + .../content/docs/en/integrations/meta.json | 1 + .../content/docs/en/integrations/temporal.mdx | 542 +++++++++++ apps/sim/blocks/blocks/temporal.ts | 917 ++++++++++++++++++ apps/sim/blocks/registry.ts | 3 + apps/sim/components/icons.tsx | 11 + apps/sim/lib/integrations/icon-mapping.ts | 2 + apps/sim/lib/integrations/integrations.json | 99 ++ apps/sim/tools/registry.ts | 42 + apps/sim/tools/temporal/cancel_workflow.ts | 94 ++ apps/sim/tools/temporal/count_workflows.ts | 93 ++ apps/sim/tools/temporal/create_schedule.ts | 175 ++++ apps/sim/tools/temporal/delete_schedule.ts | 70 ++ apps/sim/tools/temporal/describe_schedule.ts | 147 +++ .../sim/tools/temporal/describe_task_queue.ts | 107 ++ apps/sim/tools/temporal/describe_workflow.ts | 151 +++ .../tools/temporal/get_workflow_history.ts | 137 +++ apps/sim/tools/temporal/index.ts | 41 + apps/sim/tools/temporal/list_schedules.ts | 135 +++ apps/sim/tools/temporal/list_workflows.ts | 131 +++ apps/sim/tools/temporal/pause_schedule.ts | 79 ++ apps/sim/tools/temporal/query_workflow.ts | 117 +++ apps/sim/tools/temporal/reset_workflow.ts | 107 ++ apps/sim/tools/temporal/signal_with_start.ts | 187 ++++ apps/sim/tools/temporal/signal_workflow.ts | 101 ++ apps/sim/tools/temporal/start_workflow.ts | 171 ++++ apps/sim/tools/temporal/terminate_workflow.ts | 91 ++ apps/sim/tools/temporal/trigger_schedule.ts | 84 ++ apps/sim/tools/temporal/types.ts | 326 +++++++ apps/sim/tools/temporal/unpause_schedule.ts | 79 ++ apps/sim/tools/temporal/update_workflow.ts | 137 +++ apps/sim/tools/temporal/utils.ts | 277 ++++++ 33 files changed, 4667 insertions(+) create mode 100644 apps/docs/content/docs/en/integrations/temporal.mdx create mode 100644 apps/sim/blocks/blocks/temporal.ts create mode 100644 apps/sim/tools/temporal/cancel_workflow.ts create mode 100644 apps/sim/tools/temporal/count_workflows.ts create mode 100644 apps/sim/tools/temporal/create_schedule.ts create mode 100644 apps/sim/tools/temporal/delete_schedule.ts create mode 100644 apps/sim/tools/temporal/describe_schedule.ts create mode 100644 apps/sim/tools/temporal/describe_task_queue.ts create mode 100644 apps/sim/tools/temporal/describe_workflow.ts create mode 100644 apps/sim/tools/temporal/get_workflow_history.ts create mode 100644 apps/sim/tools/temporal/index.ts create mode 100644 apps/sim/tools/temporal/list_schedules.ts create mode 100644 apps/sim/tools/temporal/list_workflows.ts create mode 100644 apps/sim/tools/temporal/pause_schedule.ts create mode 100644 apps/sim/tools/temporal/query_workflow.ts create mode 100644 apps/sim/tools/temporal/reset_workflow.ts create mode 100644 apps/sim/tools/temporal/signal_with_start.ts create mode 100644 apps/sim/tools/temporal/signal_workflow.ts create mode 100644 apps/sim/tools/temporal/start_workflow.ts create mode 100644 apps/sim/tools/temporal/terminate_workflow.ts create mode 100644 apps/sim/tools/temporal/trigger_schedule.ts create mode 100644 apps/sim/tools/temporal/types.ts create mode 100644 apps/sim/tools/temporal/unpause_schedule.ts create mode 100644 apps/sim/tools/temporal/update_workflow.ts create mode 100644 apps/sim/tools/temporal/utils.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 6f5df2b76b..0348515173 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -5826,6 +5826,17 @@ export function DagsterIcon(props: SVGProps) { ) } +export function TemporalIcon(props: SVGProps) { + return ( + + + + ) +} + export function DatabricksIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 7a8e927a2c..3886f6e3b2 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -194,6 +194,7 @@ import { TailscaleIcon, TavilyIcon, TelegramIcon, + TemporalIcon, TextractIcon, TinybirdIcon, TrelloIcon, @@ -434,6 +435,7 @@ export const blockTypeToIconMap: Record = { tailscale: TailscaleIcon, tavily: TavilyIcon, telegram: TelegramIcon, + temporal: TemporalIcon, textract: TextractIcon, textract_v2: TextractIcon, tinybird: TinybirdIcon, diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index dd4558a99c..3df5aa6908 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -194,6 +194,7 @@ "tailscale", "tavily", "telegram", + "temporal", "textract", "tinybird", "trello", diff --git a/apps/docs/content/docs/en/integrations/temporal.mdx b/apps/docs/content/docs/en/integrations/temporal.mdx new file mode 100644 index 0000000000..f6c1d64917 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/temporal.mdx @@ -0,0 +1,542 @@ +--- +title: Temporal +description: Start, signal, query, and manage Temporal workflow executions +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Temporal](https://temporal.io/) is an open-source durable execution platform that lets teams write workflows as code that survive crashes, retries, and outages. A Temporal cluster tracks every workflow execution's state and event history, so long-running business processes — order fulfillment, payment pipelines, infrastructure provisioning, human-in-the-loop approvals — run reliably for minutes or months at a time. + +With the Temporal integration in Sim, your agents can drive those durable workflows directly. Connect to any Temporal cluster that exposes the server's HTTP API (enabled by default on the frontend's HTTP port, 7243, in modern Temporal servers) and: + +- **Run workflows**: start executions with JSON input, use signal-with-start for exactly-once delivery, and set ID reuse policies, cron schedules, timeouts, memo fields, and search attributes. +- **Communicate with running workflows**: send signals, invoke update handlers and wait for their results, and run queries against live workflow state. +- **Observe executions**: describe a single execution (status, timing, pending activities), list and count executions with Temporal's visibility query language, and fetch full event histories — including just the close event to read a workflow's outcome. +- **Operate the fleet**: cancel or terminate runaway executions, reset a workflow to a previous point in its history, and manage schedules — list, describe, pause, unpause, trigger, and delete them. + +Workflow inputs and results are encoded with Temporal's standard `json/plain` payload converter, so the integration interoperates with workers written in any Temporal SDK. If your server has authentication enabled, provide an API key and Sim sends it as a Bearer token on every request. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Connect to a Temporal cluster over the server's HTTP API to start workflow executions, send signals, run queries against workflow state, describe and list executions, fetch event histories, and cancel or terminate running workflows. API key only required for servers with authentication enabled. + + + +## Actions + +### `temporal_start_workflow` + +Start a new workflow execution on a Temporal cluster. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `workflowId` | string | Yes | Unique workflow ID for the new execution \(e.g., order-1234\) | +| `workflowType` | string | Yes | Registered workflow type name to run \(e.g., OrderWorkflow\) | +| `taskQueue` | string | Yes | Task queue the workflow worker polls \(e.g., orders\) | +| `input` | string | No | Workflow input as JSON. A top-level array is passed as the argument list \(one argument per element\); any other value is passed as a single argument | +| `workflowIdReusePolicy` | string | No | Policy for reusing a closed workflow ID: WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, or WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING | +| `workflowIdConflictPolicy` | string | No | Policy when a workflow with the same ID is already running: WORKFLOW_ID_CONFLICT_POLICY_FAIL, WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, or WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING | +| `cronSchedule` | string | No | Cron schedule for recurring executions \(e.g., "0 12 * * *"\) | +| `executionTimeoutSeconds` | number | No | Total workflow execution timeout in seconds, including retries and continue-as-new | +| `runTimeoutSeconds` | number | No | Timeout for a single workflow run in seconds | +| `memo` | string | No | JSON object of memo fields to attach to the execution | +| `searchAttributes` | string | No | JSON object of search attribute values to index the execution with | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workflowId` | string | Workflow ID of the execution | +| `runId` | string | Run ID of the started workflow execution | +| `started` | boolean | Whether a new execution was started \(false when an existing execution was reused\) | + +### `temporal_signal_workflow` + +Send a signal to a running Temporal workflow execution. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `workflowId` | string | Yes | Workflow ID of the execution to signal | +| `runId` | string | No | Run ID of a specific run to signal \(defaults to the latest run\) | +| `signalName` | string | Yes | Name of the signal handler to invoke \(e.g., approve-order\) | +| `signalInput` | string | No | Signal input as JSON. A top-level array is passed as the argument list \(one argument per element\); any other value is passed as a single argument | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workflowId` | string | Workflow ID of the signaled execution | +| `signalName` | string | Name of the signal that was sent | + +### `temporal_signal_with_start` + +Atomically signal a Temporal workflow, starting it first if it is not already running, so the signal is never lost. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `workflowId` | string | Yes | Workflow ID to signal, or to start and signal \(e.g., order-1234\) | +| `workflowType` | string | Yes | Registered workflow type name to start if the workflow is not running | +| `taskQueue` | string | Yes | Task queue the workflow worker polls \(e.g., orders\) | +| `signalName` | string | Yes | Name of the signal handler to invoke \(e.g., approve-order\) | +| `input` | string | No | Workflow start input as JSON, used only when a new execution is started. A top-level array is passed as the argument list; any other value is passed as a single argument | +| `signalInput` | string | No | Signal input as JSON. A top-level array is passed as the argument list \(one argument per element\); any other value is passed as a single argument | +| `workflowIdReusePolicy` | string | No | Policy for reusing a closed workflow ID: WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, or WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING | +| `workflowIdConflictPolicy` | string | No | Policy when a workflow with the same ID is already running \(defaults to using the existing run\): WORKFLOW_ID_CONFLICT_POLICY_FAIL, WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, or WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING | +| `cronSchedule` | string | No | Cron schedule for recurring executions \(e.g., "0 12 * * *"\) | +| `executionTimeoutSeconds` | number | No | Total workflow execution timeout in seconds, including retries and continue-as-new | +| `runTimeoutSeconds` | number | No | Timeout for a single workflow run in seconds | +| `memo` | string | No | JSON object of memo fields to attach to the execution | +| `searchAttributes` | string | No | JSON object of search attribute values to index the execution with | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workflowId` | string | Workflow ID of the signaled execution | +| `runId` | string | Run ID of the signaled \(or newly started\) execution | +| `started` | boolean | Whether this call started a new execution \(false when only signaled\) | + +### `temporal_query_workflow` + +Run a synchronous query against the state of a Temporal workflow execution and return the result. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `workflowId` | string | Yes | Workflow ID of the execution to query | +| `runId` | string | No | Run ID of a specific run to query \(defaults to the latest run\) | +| `queryType` | string | Yes | Name of the query handler to invoke \(e.g., getStatus\) | +| `queryArgs` | string | No | Query arguments as JSON. A top-level array is passed as the argument list \(one argument per element\); any other value is passed as a single argument | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workflowId` | string | Workflow ID of the queried execution | +| `queryType` | string | Name of the query that was run | +| `result` | json | Decoded query result. A single payload is returned as its JSON value; multiple payloads are returned as an array | + +### `temporal_update_workflow` + +Invoke an update handler on a running Temporal workflow and wait for its result. Unlike a signal, an update is validated by the workflow and returns a response. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `workflowId` | string | Yes | Workflow ID of the execution to update | +| `runId` | string | No | Run ID of a specific run to update \(defaults to the latest run\) | +| `updateName` | string | Yes | Name of the update handler to invoke \(e.g., addItem\) | +| `updateArgs` | string | No | Update arguments as JSON. A top-level array is passed as the argument list \(one argument per element\); any other value is passed as a single argument | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workflowId` | string | Workflow ID of the updated execution | +| `updateName` | string | Name of the update that was invoked | +| `result` | json | Decoded update result. A single payload is returned as its JSON value; multiple payloads are returned as an array | + +### `temporal_describe_workflow` + +Get the current state of a Temporal workflow execution, including status, timing, memo, search attributes, and pending activities. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `workflowId` | string | Yes | Workflow ID of the execution to describe | +| `runId` | string | No | Run ID of a specific run to describe \(defaults to the latest run\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workflowId` | string | Workflow ID of the execution | +| `runId` | string | Run ID of the execution | +| `workflowType` | string | Workflow type name | +| `status` | string | Execution status \(RUNNING, COMPLETED, FAILED, CANCELED, TERMINATED, CONTINUED_AS_NEW, or TIMED_OUT\) | +| `startTime` | string | Start time of the execution \(RFC 3339\) | +| `closeTime` | string | Close time of the execution \(RFC 3339\), null while running | +| `executionTime` | string | Effective execution start time \(RFC 3339\), e.g. the first cron run time | +| `historyLength` | number | Number of events in the workflow history | +| `taskQueue` | string | Task queue of the execution | +| `memo` | json | Decoded memo fields attached to the execution | +| `searchAttributes` | json | Decoded search attribute values | +| `pendingActivities` | array | Activities currently pending on the execution | +| ↳ `activityId` | string | Activity ID | +| ↳ `activityType` | string | Activity type name | +| ↳ `state` | string | Pending state \(SCHEDULED, STARTED, CANCEL_REQUESTED, PAUSED, or PAUSE_REQUESTED\) | +| ↳ `attempt` | number | Current attempt number | +| ↳ `lastFailureMessage` | string | Message of the most recent failure, if the activity is retrying | + +### `temporal_list_workflows` + +List workflow executions in a Temporal namespace, optionally filtered with a visibility query. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `query` | string | No | Visibility list filter, e.g. WorkflowType = "OrderWorkflow" AND ExecutionStatus = "Running" \(empty lists all executions\) | +| `pageSize` | number | No | Maximum number of executions to return per page | +| `nextPageToken` | string | No | Page token from a previous response, for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `executions` | array | Workflow executions matching the query | +| ↳ `workflowId` | string | Workflow ID of the execution | +| ↳ `runId` | string | Run ID of the execution | +| ↳ `workflowType` | string | Workflow type name | +| ↳ `status` | string | Execution status \(RUNNING, COMPLETED, FAILED, CANCELED, TERMINATED, CONTINUED_AS_NEW, or TIMED_OUT\) | +| ↳ `startTime` | string | Start time of the execution \(RFC 3339\) | +| ↳ `closeTime` | string | Close time of the execution \(RFC 3339\), null while running | +| ↳ `executionTime` | string | Effective execution start time \(RFC 3339\) | +| ↳ `historyLength` | number | Number of events in the workflow history | +| ↳ `taskQueue` | string | Task queue of the execution | +| `nextPageToken` | string | Token for the next page of results, null when no more pages exist | + +### `temporal_count_workflows` + +Count workflow executions in a Temporal namespace matching a visibility query, with optional GROUP BY aggregation. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `query` | string | No | Visibility count filter, e.g. ExecutionStatus = "Running" or ... GROUP BY ExecutionStatus \(empty counts all executions\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `count` | number | Number of workflow executions matching the query | +| `groups` | array | Per-group counts when the query uses GROUP BY \(empty otherwise\) | +| ↳ `values` | json | Decoded values of the GROUP BY fields | +| ↳ `count` | number | Number of executions in the group | + +### `temporal_get_workflow_history` + +Fetch the event history of a Temporal workflow execution, optionally filtered to just the close event. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `workflowId` | string | Yes | Workflow ID of the execution | +| `runId` | string | No | Run ID of a specific run \(defaults to the latest run\) | +| `maximumPageSize` | number | No | Maximum number of history events to return per page | +| `nextPageToken` | string | No | Page token from a previous response, for pagination | +| `historyEventFilterType` | string | No | Event filter: HISTORY_EVENT_FILTER_TYPE_ALL_EVENT \(default\) or HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT to return only the final close event | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `events` | array | History events of the workflow execution, in order | +| ↳ `eventId` | number | Sequential ID of the event | +| ↳ `eventTime` | string | Time the event was recorded \(RFC 3339\) | +| ↳ `eventType` | string | Event type \(e.g., WORKFLOW_EXECUTION_STARTED, ACTIVITY_TASK_COMPLETED\) | +| ↳ `attributes` | json | The event's type-specific attributes \(payload data is base64-encoded\) | +| `nextPageToken` | string | Token for the next page of events, null when no more pages exist | + +### `temporal_cancel_workflow` + +Request cooperative cancellation of a running Temporal workflow execution. The workflow decides how to respond to the request. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `workflowId` | string | Yes | Workflow ID of the execution to cancel | +| `runId` | string | No | Run ID of a specific run to cancel \(defaults to the latest run\) | +| `reason` | string | No | Reason for the cancellation, recorded in the workflow history | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workflowId` | string | Workflow ID of the execution whose cancellation was requested | + +### `temporal_terminate_workflow` + +Forcefully terminate a Temporal workflow execution immediately, without giving the workflow a chance to react. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `workflowId` | string | Yes | Workflow ID of the execution to terminate | +| `runId` | string | No | Run ID of a specific run to terminate \(defaults to the latest run\) | +| `reason` | string | No | Reason for the termination, recorded in the workflow history | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workflowId` | string | Workflow ID of the terminated execution | + +### `temporal_reset_workflow` + +Reset a Temporal workflow execution to a past workflow task, terminating the current run and replaying from the reset point in a new run. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `workflowId` | string | Yes | Workflow ID of the execution to reset | +| `runId` | string | No | Run ID of a specific run to reset \(defaults to the latest run\) | +| `workflowTaskFinishEventId` | number | Yes | Event ID of the workflow task finish event to reset to — a WORKFLOW_TASK_COMPLETED, WORKFLOW_TASK_TIMED_OUT, WORKFLOW_TASK_FAILED, or WORKFLOW_TASK_STARTED event \(find it with Get Workflow History\) | +| `reason` | string | No | Reason for the reset, recorded in the workflow history | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workflowId` | string | Workflow ID of the reset execution | +| `runId` | string | Run ID of the new run created by the reset | + +### `temporal_describe_task_queue` + +List the workers currently polling a Temporal task queue, to check whether a workflow or activity has live workers. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `taskQueue` | string | Yes | Name of the task queue to describe \(e.g., orders\) | +| `taskQueueType` | string | No | Type of pollers to list: TASK_QUEUE_TYPE_WORKFLOW \(default\) or TASK_QUEUE_TYPE_ACTIVITY | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `taskQueue` | string | Name of the described task queue | +| `pollers` | array | Workers currently polling the task queue \(empty when no workers are running\) | +| ↳ `identity` | string | Identity of the polling worker | +| ↳ `lastAccessTime` | string | Last time the worker polled the queue \(RFC 3339\) | +| ↳ `ratePerSecond` | number | Poller rate per second | + +### `temporal_create_schedule` + +Create a Temporal schedule that starts a workflow on a cron or interval cadence. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `scheduleId` | string | Yes | Unique ID for the new schedule \(e.g., nightly-report\) | +| `workflowId` | string | Yes | Workflow ID for started workflows \(the schedule appends the run time to keep IDs unique\) | +| `workflowType` | string | Yes | Registered workflow type name the schedule starts \(e.g., ReportWorkflow\) | +| `taskQueue` | string | Yes | Task queue the workflow worker polls \(e.g., reports\) | +| `input` | string | No | Workflow input as JSON. A top-level array is passed as the argument list \(one argument per element\); any other value is passed as a single argument | +| `cronExpressions` | string | No | Cron expressions defining when the schedule fires, comma- or newline-separated for multiple \(e.g., "0 12 * * *"\). At least one of cronExpressions or intervalSeconds is required | +| `intervalSeconds` | number | No | Fixed interval between actions in seconds. At least one of cronExpressions or intervalSeconds is required | +| `timezone` | string | No | IANA time zone for cron evaluation \(e.g., America/New_York; defaults to UTC\) | +| `overlapPolicy` | string | No | Policy when an action would overlap a still-running one \(defaults to skip\): SCHEDULE_OVERLAP_POLICY_SKIP, SCHEDULE_OVERLAP_POLICY_BUFFER_ONE, SCHEDULE_OVERLAP_POLICY_BUFFER_ALL, SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER, SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER, or SCHEDULE_OVERLAP_POLICY_ALLOW_ALL | +| `notes` | string | No | Human-readable notes stored on the schedule | +| `paused` | boolean | No | Create the schedule in a paused state \(defaults to active\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `scheduleId` | string | ID of the created schedule | + +### `temporal_list_schedules` + +List schedules in a Temporal namespace. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `query` | string | No | Visibility filter over schedules, e.g. TemporalSchedulePaused = false \(empty lists all schedules\) | +| `maximumPageSize` | number | No | Maximum number of schedules to return per page | +| `nextPageToken` | string | No | Page token from a previous response, for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `schedules` | array | Schedules in the namespace | +| ↳ `scheduleId` | string | Schedule ID | +| ↳ `workflowType` | string | Workflow type the schedule starts | +| ↳ `paused` | boolean | Whether the schedule is paused | +| ↳ `notes` | string | Human-readable notes on the schedule | +| ↳ `futureActionTimes` | json | Upcoming action times \(RFC 3339\) | +| `nextPageToken` | string | Token for the next page of results, null when no more pages exist | + +### `temporal_describe_schedule` + +Get the configuration and current state of a Temporal schedule, including its spec, recent actions, and upcoming run times. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `scheduleId` | string | Yes | ID of the schedule to describe | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `scheduleId` | string | Schedule ID | +| `paused` | boolean | Whether the schedule is paused | +| `notes` | string | Human-readable notes on the schedule | +| `workflowType` | string | Workflow type the schedule starts | +| `taskQueue` | string | Task queue used for started workflows | +| `workflowId` | string | Workflow ID template for started workflows | +| `spec` | json | Schedule spec \(calendars, intervals, cron strings, jitter, time zone\) | +| `recentActions` | array | Most recent actions taken by the schedule | +| ↳ `scheduleTime` | string | Nominal scheduled time \(RFC 3339\) | +| ↳ `actualTime` | string | Actual time the action ran \(RFC 3339\) | +| ↳ `workflowId` | string | Workflow ID of the started execution | +| ↳ `runId` | string | Run ID of the started execution | +| `futureActionTimes` | json | Upcoming action times \(RFC 3339\) | + +### `temporal_pause_schedule` + +Pause a Temporal schedule so it stops taking actions until unpaused. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `scheduleId` | string | Yes | ID of the schedule to pause | +| `reason` | string | No | Reason recorded in the schedule's notes | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `scheduleId` | string | ID of the paused schedule | + +### `temporal_unpause_schedule` + +Unpause a Temporal schedule so it resumes taking actions. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `scheduleId` | string | Yes | ID of the schedule to unpause | +| `reason` | string | No | Reason recorded in the schedule's notes | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `scheduleId` | string | ID of the unpaused schedule | + +### `temporal_trigger_schedule` + +Trigger an immediate action of a Temporal schedule, outside its normal spec. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `scheduleId` | string | Yes | ID of the schedule to trigger | +| `overlapPolicy` | string | No | Overlap policy for the triggered action \(defaults to the schedule's policy\): SCHEDULE_OVERLAP_POLICY_SKIP, SCHEDULE_OVERLAP_POLICY_BUFFER_ONE, SCHEDULE_OVERLAP_POLICY_BUFFER_ALL, SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER, SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER, or SCHEDULE_OVERLAP_POLICY_ALLOW_ALL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `scheduleId` | string | ID of the triggered schedule | + +### `temporal_delete_schedule` + +Delete a Temporal schedule. Workflows already started by the schedule keep running. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `serverUrl` | string | Yes | Base URL of the Temporal server's HTTP API \(e.g., http://localhost:7243\) | +| `namespace` | string | Yes | Temporal namespace \(e.g., default\) | +| `apiKey` | string | No | API key sent as a Bearer token \(leave blank for servers without auth\) | +| `scheduleId` | string | Yes | ID of the schedule to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `scheduleId` | string | ID of the deleted schedule | + + diff --git a/apps/sim/blocks/blocks/temporal.ts b/apps/sim/blocks/blocks/temporal.ts new file mode 100644 index 0000000000..17a0569f46 --- /dev/null +++ b/apps/sim/blocks/blocks/temporal.ts @@ -0,0 +1,917 @@ +import { TemporalIcon } from '@/components/icons' +import type { BlockConfig, BlockMeta } from '@/blocks/types' +import { IntegrationType } from '@/blocks/types' +import type { TemporalResponse } from '@/tools/temporal/types' + +/** Coerces a subBlock value to a finite number, returning undefined for empty or non-numeric input. */ +function toFiniteNumber(value: unknown): number | undefined { + if (value == null || value === '') return undefined + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined +} + +const WORKFLOW_ID_OPERATIONS = [ + 'start_workflow', + 'signal_workflow', + 'signal_with_start', + 'query_workflow', + 'update_workflow', + 'describe_workflow', + 'get_workflow_history', + 'cancel_workflow', + 'terminate_workflow', + 'reset_workflow', + 'create_schedule', +] + +const RUN_ID_OPERATIONS = [ + 'signal_workflow', + 'query_workflow', + 'update_workflow', + 'describe_workflow', + 'get_workflow_history', + 'cancel_workflow', + 'terminate_workflow', + 'reset_workflow', +] + +const SCHEDULE_ID_OPERATIONS = [ + 'create_schedule', + 'describe_schedule', + 'pause_schedule', + 'unpause_schedule', + 'trigger_schedule', + 'delete_schedule', +] + +const START_OPERATIONS = ['start_workflow', 'signal_with_start'] + +const WORKFLOW_TYPE_OPERATIONS = ['start_workflow', 'signal_with_start', 'create_schedule'] + +const TASK_QUEUE_OPERATIONS = [ + 'start_workflow', + 'signal_with_start', + 'create_schedule', + 'describe_task_queue', +] + +const JSON_ARGS_WAND_PROMPT = `Generate the JSON input arguments based on the user's description. A top-level array is passed as the argument list (one argument per element); any other JSON value is passed as a single argument. + +Examples: +- "order 1234 for alice" -> {"orderId": "1234", "customer": "alice"} +- "two arguments: the user id 42 and the flag true" -> [42, true] + +Return ONLY valid JSON - no explanations, no extra text.` + +export const TemporalBlock: BlockConfig = { + type: 'temporal', + name: 'Temporal', + description: 'Start, signal, query, and manage Temporal workflow executions', + longDescription: + "Connect to a Temporal cluster over the server's HTTP API to start workflow executions, send signals, run queries against workflow state, describe and list executions, fetch event histories, and cancel or terminate running workflows. API key only required for servers with authentication enabled.", + docsLink: 'https://docs.sim.ai/integrations/temporal', + category: 'tools', + integrationType: IntegrationType.DevOps, + bgColor: '#141414', + icon: TemporalIcon, + + subBlocks: [ + // ── Operation selector ───────────────────────────────────────────────────── + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Start Workflow', id: 'start_workflow' }, + { label: 'Signal Workflow', id: 'signal_workflow' }, + { label: 'Signal With Start', id: 'signal_with_start' }, + { label: 'Query Workflow', id: 'query_workflow' }, + { label: 'Update Workflow', id: 'update_workflow' }, + { label: 'Describe Workflow', id: 'describe_workflow' }, + { label: 'List Workflows', id: 'list_workflows' }, + { label: 'Count Workflows', id: 'count_workflows' }, + { label: 'Get Workflow History', id: 'get_workflow_history' }, + { label: 'Cancel Workflow', id: 'cancel_workflow' }, + { label: 'Terminate Workflow', id: 'terminate_workflow' }, + { label: 'Reset Workflow', id: 'reset_workflow' }, + { label: 'Describe Task Queue', id: 'describe_task_queue' }, + { label: 'Create Schedule', id: 'create_schedule' }, + { label: 'List Schedules', id: 'list_schedules' }, + { label: 'Describe Schedule', id: 'describe_schedule' }, + { label: 'Pause Schedule', id: 'pause_schedule' }, + { label: 'Unpause Schedule', id: 'unpause_schedule' }, + { label: 'Trigger Schedule', id: 'trigger_schedule' }, + { label: 'Delete Schedule', id: 'delete_schedule' }, + ], + value: () => 'start_workflow', + }, + + // ── Workflow ID (all operations except list) ─────────────────────────────── + { + id: 'workflowId', + title: 'Workflow ID', + type: 'short-input', + placeholder: 'e.g., order-1234', + condition: { field: 'operation', value: WORKFLOW_ID_OPERATIONS }, + required: { field: 'operation', value: WORKFLOW_ID_OPERATIONS }, + }, + + // ── Start Workflow / Signal With Start / Create Schedule ─────────────────── + { + id: 'workflowType', + title: 'Workflow Type', + type: 'short-input', + placeholder: 'e.g., OrderWorkflow', + condition: { field: 'operation', value: WORKFLOW_TYPE_OPERATIONS }, + required: { field: 'operation', value: WORKFLOW_TYPE_OPERATIONS }, + }, + { + id: 'taskQueue', + title: 'Task Queue', + type: 'short-input', + placeholder: 'e.g., orders', + condition: { field: 'operation', value: TASK_QUEUE_OPERATIONS }, + required: { field: 'operation', value: TASK_QUEUE_OPERATIONS }, + }, + { + id: 'input', + title: 'Workflow Input', + type: 'code', + placeholder: '{"orderId": "1234"} or ["arg1", "arg2"]', + condition: { field: 'operation', value: WORKFLOW_TYPE_OPERATIONS }, + wandConfig: { + enabled: true, + prompt: JSON_ARGS_WAND_PROMPT, + placeholder: 'Describe the workflow input...', + generationType: 'json-object', + }, + }, + + // ── Signal Workflow / Signal With Start ──────────────────────────────────── + { + id: 'signalName', + title: 'Signal Name', + type: 'short-input', + placeholder: 'e.g., approve-order', + condition: { field: 'operation', value: ['signal_workflow', 'signal_with_start'] }, + required: { field: 'operation', value: ['signal_workflow', 'signal_with_start'] }, + }, + { + id: 'signalInput', + title: 'Signal Input', + type: 'code', + placeholder: '{"approvedBy": "alice"}', + condition: { field: 'operation', value: ['signal_workflow', 'signal_with_start'] }, + wandConfig: { + enabled: true, + prompt: JSON_ARGS_WAND_PROMPT, + placeholder: 'Describe the signal input...', + generationType: 'json-object', + }, + }, + + // ── Query Workflow ───────────────────────────────────────────────────────── + { + id: 'queryType', + title: 'Query Type', + type: 'short-input', + placeholder: 'e.g., getStatus', + condition: { field: 'operation', value: 'query_workflow' }, + required: { field: 'operation', value: 'query_workflow' }, + }, + { + id: 'queryArgs', + title: 'Query Arguments', + type: 'code', + placeholder: '{"includeDetails": true}', + condition: { field: 'operation', value: 'query_workflow' }, + wandConfig: { + enabled: true, + prompt: JSON_ARGS_WAND_PROMPT, + placeholder: 'Describe the query arguments...', + generationType: 'json-object', + }, + }, + + // ── Update Workflow ──────────────────────────────────────────────────────── + { + id: 'updateName', + title: 'Update Name', + type: 'short-input', + placeholder: 'e.g., addItem', + condition: { field: 'operation', value: 'update_workflow' }, + required: { field: 'operation', value: 'update_workflow' }, + }, + { + id: 'updateArgs', + title: 'Update Arguments', + type: 'code', + placeholder: '{"sku": "ABC123"}', + condition: { field: 'operation', value: 'update_workflow' }, + wandConfig: { + enabled: true, + prompt: JSON_ARGS_WAND_PROMPT, + placeholder: 'Describe the update arguments...', + generationType: 'json-object', + }, + }, + + // ── List / Count Workflows ───────────────────────────────────────────────── + { + id: 'listQuery', + title: 'Query Filter', + type: 'long-input', + placeholder: 'e.g., WorkflowType = "OrderWorkflow" AND ExecutionStatus = "Running"', + condition: { field: 'operation', value: ['list_workflows', 'count_workflows'] }, + wandConfig: { + enabled: true, + prompt: `Generate a Temporal visibility list filter based on the user's description. + +The filter uses a SQL-like syntax over search attributes. Common attributes: WorkflowId, WorkflowType, ExecutionStatus (Running, Completed, Failed, Canceled, Terminated, ContinuedAsNew, TimedOut), StartTime, CloseTime, TaskQueue. String values are double-quoted; times are ISO 8601 strings. + +Examples: +- "running order workflows" -> WorkflowType = "OrderWorkflow" AND ExecutionStatus = "Running" +- "failed workflows since June 1 2026" -> ExecutionStatus = "Failed" AND StartTime > "2026-06-01T00:00:00Z" +- "count by status" (count operation only) -> GROUP BY ExecutionStatus + +Return ONLY the filter expression - no explanations, no extra text.`, + placeholder: 'Describe which executions to list...', + }, + }, + { + id: 'pageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '20', + condition: { field: 'operation', value: 'list_workflows' }, + mode: 'advanced', + }, + { + id: 'nextPageToken', + title: 'Next Page Token', + type: 'short-input', + placeholder: 'Token from a previous response (for pagination)', + condition: { field: 'operation', value: 'list_workflows' }, + mode: 'advanced', + }, + + // ── Get Workflow History ─────────────────────────────────────────────────── + { + id: 'historyEventFilterType', + title: 'Event Filter', + type: 'dropdown', + options: [ + { label: 'All Events', id: '' }, + { label: 'Close Event Only', id: 'HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT' }, + ], + value: () => '', + condition: { field: 'operation', value: 'get_workflow_history' }, + mode: 'advanced', + }, + { + id: 'historyPageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'get_workflow_history' }, + mode: 'advanced', + }, + { + id: 'historyNextPageToken', + title: 'Next Page Token', + type: 'short-input', + placeholder: 'Token from a previous response (for pagination)', + condition: { field: 'operation', value: 'get_workflow_history' }, + mode: 'advanced', + }, + + // ── Reset Workflow ───────────────────────────────────────────────────────── + { + id: 'workflowTaskFinishEventId', + title: 'Reset Event ID', + type: 'short-input', + placeholder: 'Workflow task event ID to reset to, e.g. a WORKFLOW_TASK_COMPLETED event', + condition: { field: 'operation', value: 'reset_workflow' }, + required: { field: 'operation', value: 'reset_workflow' }, + }, + + // ── Schedule operations ──────────────────────────────────────────────────── + { + id: 'scheduleId', + title: 'Schedule ID', + type: 'short-input', + placeholder: 'e.g., nightly-report', + condition: { field: 'operation', value: SCHEDULE_ID_OPERATIONS }, + required: { field: 'operation', value: SCHEDULE_ID_OPERATIONS }, + }, + { + id: 'scheduleCronExpressions', + title: 'Cron Expressions', + type: 'short-input', + placeholder: 'e.g., 0 12 * * * (comma-separated for multiple)', + condition: { field: 'operation', value: 'create_schedule' }, + wandConfig: { + enabled: true, + prompt: `Generate cron expression(s) for a Temporal schedule based on the user's description. Use standard 5-field cron syntax (minute hour day-of-month month day-of-week). Separate multiple expressions with commas. + +Examples: +- "every day at noon" -> 0 12 * * * +- "weekdays at 9am and 5pm" -> 0 9 * * 1-5, 0 17 * * 1-5 + +Return ONLY the cron expression(s) - no explanations, no extra text.`, + placeholder: 'Describe when the schedule should fire...', + }, + }, + { + id: 'scheduleIntervalSeconds', + title: 'Interval (s)', + type: 'short-input', + placeholder: 'Fixed interval between actions, e.g. 3600 (optional)', + condition: { field: 'operation', value: 'create_schedule' }, + mode: 'advanced', + }, + { + id: 'scheduleTimezone', + title: 'Time Zone', + type: 'short-input', + placeholder: 'e.g., America/New_York (defaults to UTC)', + condition: { field: 'operation', value: 'create_schedule' }, + mode: 'advanced', + }, + { + id: 'scheduleNotes', + title: 'Notes', + type: 'short-input', + placeholder: 'Human-readable notes for the schedule (optional)', + condition: { field: 'operation', value: 'create_schedule' }, + mode: 'advanced', + }, + { + id: 'schedulePaused', + title: 'Initial State', + type: 'dropdown', + options: [ + { label: 'Active', id: '' }, + { label: 'Paused', id: 'true' }, + ], + value: () => '', + condition: { field: 'operation', value: 'create_schedule' }, + mode: 'advanced', + }, + { + id: 'scheduleQuery', + title: 'Query Filter', + type: 'long-input', + placeholder: 'e.g., TemporalSchedulePaused = false (optional)', + condition: { field: 'operation', value: 'list_schedules' }, + mode: 'advanced', + }, + { + id: 'schedulePageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '20', + condition: { field: 'operation', value: 'list_schedules' }, + mode: 'advanced', + }, + { + id: 'scheduleNextPageToken', + title: 'Next Page Token', + type: 'short-input', + placeholder: 'Token from a previous response (for pagination)', + condition: { field: 'operation', value: 'list_schedules' }, + mode: 'advanced', + }, + { + id: 'overlapPolicy', + title: 'Overlap Policy', + type: 'dropdown', + options: [ + { label: 'Schedule Default', id: '' }, + { label: 'Skip', id: 'SCHEDULE_OVERLAP_POLICY_SKIP' }, + { label: 'Buffer One', id: 'SCHEDULE_OVERLAP_POLICY_BUFFER_ONE' }, + { label: 'Buffer All', id: 'SCHEDULE_OVERLAP_POLICY_BUFFER_ALL' }, + { label: 'Cancel Other', id: 'SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER' }, + { label: 'Terminate Other', id: 'SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER' }, + { label: 'Allow All', id: 'SCHEDULE_OVERLAP_POLICY_ALLOW_ALL' }, + ], + value: () => '', + condition: { field: 'operation', value: ['trigger_schedule', 'create_schedule'] }, + mode: 'advanced', + }, + + // ── Describe Task Queue ──────────────────────────────────────────────────── + { + id: 'taskQueueType', + title: 'Task Queue Type', + type: 'dropdown', + options: [ + { label: 'Workflow', id: '' }, + { label: 'Activity', id: 'TASK_QUEUE_TYPE_ACTIVITY' }, + ], + value: () => '', + condition: { field: 'operation', value: 'describe_task_queue' }, + mode: 'advanced', + }, + + // ── Reason (cancel / terminate / reset / pause / unpause) ────────────────── + { + id: 'reason', + title: 'Reason', + type: 'short-input', + placeholder: 'Recorded in the workflow history or schedule notes (optional)', + condition: { + field: 'operation', + value: [ + 'cancel_workflow', + 'terminate_workflow', + 'reset_workflow', + 'pause_schedule', + 'unpause_schedule', + ], + }, + }, + + // ── Shared advanced options ──────────────────────────────────────────────── + { + id: 'runId', + title: 'Run ID', + type: 'short-input', + placeholder: 'Targets a specific run (defaults to the latest run)', + condition: { field: 'operation', value: RUN_ID_OPERATIONS }, + mode: 'advanced', + }, + { + id: 'workflowIdReusePolicy', + title: 'ID Reuse Policy', + type: 'dropdown', + options: [ + { label: 'Default', id: '' }, + { label: 'Allow Duplicate', id: 'WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE' }, + { + label: 'Allow Duplicate Failed Only', + id: 'WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY', + }, + { label: 'Reject Duplicate', id: 'WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE' }, + { label: 'Terminate If Running', id: 'WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING' }, + ], + value: () => '', + condition: { field: 'operation', value: START_OPERATIONS }, + mode: 'advanced', + }, + { + id: 'workflowIdConflictPolicy', + title: 'ID Conflict Policy', + type: 'dropdown', + options: [ + { label: 'Default', id: '' }, + { label: 'Fail', id: 'WORKFLOW_ID_CONFLICT_POLICY_FAIL' }, + { label: 'Use Existing', id: 'WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING' }, + { label: 'Terminate Existing', id: 'WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING' }, + ], + value: () => '', + condition: { field: 'operation', value: START_OPERATIONS }, + mode: 'advanced', + }, + { + id: 'cronSchedule', + title: 'Cron Schedule', + type: 'short-input', + placeholder: 'e.g., 0 12 * * *', + condition: { field: 'operation', value: START_OPERATIONS }, + mode: 'advanced', + }, + { + id: 'executionTimeoutSeconds', + title: 'Execution Timeout (s)', + type: 'short-input', + placeholder: 'Total timeout including retries (optional)', + condition: { field: 'operation', value: START_OPERATIONS }, + mode: 'advanced', + }, + { + id: 'runTimeoutSeconds', + title: 'Run Timeout (s)', + type: 'short-input', + placeholder: 'Timeout for a single run (optional)', + condition: { field: 'operation', value: START_OPERATIONS }, + mode: 'advanced', + }, + { + id: 'memo', + title: 'Memo', + type: 'code', + placeholder: '{"team": "payments"}', + condition: { field: 'operation', value: START_OPERATIONS }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a JSON object of Temporal memo fields based on the user's description. + +Example: "owned by the payments team, priority high" -> {"team": "payments", "priority": "high"} + +Return ONLY a valid JSON object - no explanations, no extra text.`, + placeholder: 'Describe the memo fields...', + generationType: 'json-object', + }, + }, + { + id: 'searchAttributes', + title: 'Search Attributes', + type: 'code', + placeholder: '{"CustomerId": "cust-42"}', + condition: { field: 'operation', value: START_OPERATIONS }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a JSON object of Temporal search attribute values based on the user's description. Keys must be search attributes registered on the namespace. + +Example: "customer cust-42, region us-east" -> {"CustomerId": "cust-42", "Region": "us-east"} + +Return ONLY a valid JSON object - no explanations, no extra text.`, + placeholder: 'Describe the search attributes...', + generationType: 'json-object', + }, + }, + + // ── Connection (common to all operations) ────────────────────────────────── + { + id: 'serverUrl', + title: 'Server URL', + type: 'short-input', + placeholder: "http://localhost:7243 (the Temporal server's HTTP API)", + required: true, + }, + { + id: 'namespace', + title: 'Namespace', + type: 'short-input', + placeholder: 'e.g., default', + required: true, + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Bearer token (leave blank for servers without auth)', + password: true, + }, + ], + + tools: { + access: [ + 'temporal_start_workflow', + 'temporal_signal_workflow', + 'temporal_signal_with_start', + 'temporal_query_workflow', + 'temporal_update_workflow', + 'temporal_describe_workflow', + 'temporal_list_workflows', + 'temporal_count_workflows', + 'temporal_get_workflow_history', + 'temporal_cancel_workflow', + 'temporal_terminate_workflow', + 'temporal_reset_workflow', + 'temporal_describe_task_queue', + 'temporal_create_schedule', + 'temporal_list_schedules', + 'temporal_describe_schedule', + 'temporal_pause_schedule', + 'temporal_unpause_schedule', + 'temporal_trigger_schedule', + 'temporal_delete_schedule', + ], + config: { + tool: (params) => `temporal_${params.operation}`, + params: (params) => { + const result: Record = {} + + // start_workflow / signal_with_start: coerce timeouts, drop empty policies + if (params.operation === 'start_workflow' || params.operation === 'signal_with_start') { + result.executionTimeoutSeconds = toFiniteNumber(params.executionTimeoutSeconds) + result.runTimeoutSeconds = toFiniteNumber(params.runTimeoutSeconds) + if (!params.workflowIdReusePolicy) result.workflowIdReusePolicy = undefined + if (!params.workflowIdConflictPolicy) result.workflowIdConflictPolicy = undefined + } + + // list_workflows / count_workflows: remap listQuery → query + if (params.operation === 'list_workflows' || params.operation === 'count_workflows') { + result.query = params.listQuery || undefined + } + + // list_workflows: coerce page size + if (params.operation === 'list_workflows') { + result.pageSize = toFiniteNumber(params.pageSize) + } + + // get_workflow_history: remap history-prefixed fields, drop empty filter + if (params.operation === 'get_workflow_history') { + result.maximumPageSize = toFiniteNumber(params.historyPageSize) + result.nextPageToken = params.historyNextPageToken || undefined + if (!params.historyEventFilterType) result.historyEventFilterType = undefined + } + + // reset_workflow: coerce the reset point event ID + if (params.operation === 'reset_workflow') { + result.workflowTaskFinishEventId = toFiniteNumber(params.workflowTaskFinishEventId) + } + + // create_schedule: remap schedule-prefixed fields, coerce interval and paused state + if (params.operation === 'create_schedule') { + result.cronExpressions = params.scheduleCronExpressions || undefined + result.intervalSeconds = toFiniteNumber(params.scheduleIntervalSeconds) + result.timezone = params.scheduleTimezone || undefined + result.notes = params.scheduleNotes || undefined + result.paused = params.schedulePaused === 'true' ? true : undefined + } + + // list_schedules: remap schedule-prefixed fields + if (params.operation === 'list_schedules') { + result.query = params.scheduleQuery || undefined + result.maximumPageSize = toFiniteNumber(params.schedulePageSize) + result.nextPageToken = params.scheduleNextPageToken || undefined + } + + // trigger_schedule / create_schedule: drop empty overlap policy + if ( + (params.operation === 'trigger_schedule' || params.operation === 'create_schedule') && + !params.overlapPolicy + ) { + result.overlapPolicy = undefined + } + + // describe_task_queue: drop empty task queue type + if (params.operation === 'describe_task_queue' && !params.taskQueueType) { + result.taskQueueType = undefined + } + + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + serverUrl: { type: 'string', description: "Base URL of the Temporal server's HTTP API" }, + namespace: { type: 'string', description: 'Temporal namespace' }, + apiKey: { + type: 'string', + description: 'API key sent as a Bearer token (optional for servers without auth)', + }, + workflowId: { type: 'string', description: 'Workflow ID of the execution' }, + runId: { type: 'string', description: 'Run ID targeting a specific run' }, + // Start Workflow / Signal With Start + workflowType: { type: 'string', description: 'Workflow type name to run' }, + taskQueue: { type: 'string', description: 'Task queue the workflow worker polls' }, + input: { type: 'string', description: 'Workflow input as JSON' }, + workflowIdReusePolicy: { + type: 'string', + description: 'Policy for reusing a closed workflow ID', + }, + workflowIdConflictPolicy: { + type: 'string', + description: 'Policy when a workflow with the same ID is already running', + }, + cronSchedule: { type: 'string', description: 'Cron schedule for recurring executions' }, + executionTimeoutSeconds: { + type: 'number', + description: 'Total workflow execution timeout in seconds', + }, + runTimeoutSeconds: { type: 'number', description: 'Single-run timeout in seconds' }, + memo: { type: 'string', description: 'JSON object of memo fields' }, + searchAttributes: { type: 'string', description: 'JSON object of search attribute values' }, + // Signal + signalName: { type: 'string', description: 'Name of the signal handler to invoke' }, + signalInput: { type: 'string', description: 'Signal input as JSON' }, + // Query + queryType: { type: 'string', description: 'Name of the query handler to invoke' }, + queryArgs: { type: 'string', description: 'Query arguments as JSON' }, + // Update + updateName: { type: 'string', description: 'Name of the update handler to invoke' }, + updateArgs: { type: 'string', description: 'Update arguments as JSON' }, + // List / Count Workflows + listQuery: { type: 'string', description: 'Visibility filter expression' }, + pageSize: { type: 'number', description: 'Maximum executions to return per page' }, + nextPageToken: { type: 'string', description: 'Page token for list pagination' }, + // Get Workflow History + historyEventFilterType: { + type: 'string', + description: 'History event filter (all events or close event only)', + }, + historyPageSize: { type: 'number', description: 'Maximum history events to return per page' }, + historyNextPageToken: { type: 'string', description: 'Page token for history pagination' }, + // Reset + workflowTaskFinishEventId: { + type: 'number', + description: 'WORKFLOW_TASK_COMPLETED event ID to reset to', + }, + // Schedules + scheduleId: { type: 'string', description: 'Schedule ID' }, + scheduleCronExpressions: { + type: 'string', + description: 'Cron expressions for a new schedule (comma- or newline-separated)', + }, + scheduleIntervalSeconds: { + type: 'number', + description: 'Fixed interval between schedule actions in seconds', + }, + scheduleTimezone: { type: 'string', description: 'IANA time zone for cron evaluation' }, + scheduleNotes: { type: 'string', description: 'Notes stored on a new schedule' }, + schedulePaused: { + type: 'string', + description: 'Whether a new schedule starts paused ("true") or active (empty)', + }, + scheduleQuery: { type: 'string', description: 'Visibility filter over schedules' }, + schedulePageSize: { type: 'number', description: 'Maximum schedules to return per page' }, + scheduleNextPageToken: { type: 'string', description: 'Page token for schedule pagination' }, + overlapPolicy: { + type: 'string', + description: 'Overlap policy for triggered or scheduled actions', + }, + // Task queues + taskQueueType: { + type: 'string', + description: 'Type of pollers to list (workflow or activity)', + }, + // Cancel / Terminate / Reset / Pause / Unpause + reason: { + type: 'string', + description: 'Reason recorded in the workflow history or schedule notes', + }, + }, + + outputs: { + // Start / Signal With Start + workflowId: { type: 'string', description: 'Workflow ID of the execution' }, + runId: { type: 'string', description: 'Run ID of the execution' }, + started: { type: 'boolean', description: 'Whether a new execution was started' }, + // Signal + signalName: { type: 'string', description: 'Name of the signal that was sent' }, + // Query / Update + queryType: { type: 'string', description: 'Name of the query that was run' }, + updateName: { type: 'string', description: 'Name of the update that was invoked' }, + result: { type: 'json', description: 'Decoded query or update result' }, + // Describe / List + workflowType: { type: 'string', description: 'Workflow type name' }, + status: { + type: 'string', + description: + 'Execution status (RUNNING, COMPLETED, FAILED, CANCELED, TERMINATED, CONTINUED_AS_NEW, or TIMED_OUT)', + }, + startTime: { type: 'string', description: 'Start time of the execution (RFC 3339)' }, + closeTime: { type: 'string', description: 'Close time of the execution (RFC 3339)' }, + executionTime: { type: 'string', description: 'Effective execution start time (RFC 3339)' }, + historyLength: { type: 'number', description: 'Number of events in the workflow history' }, + taskQueue: { type: 'string', description: 'Task queue of the execution' }, + memo: { type: 'json', description: 'Decoded memo fields attached to the execution' }, + searchAttributes: { type: 'json', description: 'Decoded search attribute values' }, + pendingActivities: { + type: 'json', + description: + 'Pending activities (activityId, activityType, state, attempt, lastFailureMessage)', + }, + executions: { + type: 'json', + description: + 'Workflow executions matching the list filter (workflowId, runId, workflowType, status, startTime, closeTime, executionTime, historyLength, taskQueue)', + }, + // Count Workflows + count: { type: 'number', description: 'Number of workflow executions matching the query' }, + groups: { + type: 'json', + description: 'Per-group counts when the count query uses GROUP BY', + }, + // Get Workflow History + events: { + type: 'json', + description: 'History events (eventId, eventTime, eventType, attributes)', + }, + nextPageToken: { + type: 'string', + description: 'Token for the next page of results (list and history operations)', + }, + // Schedules + scheduleId: { type: 'string', description: 'Schedule ID' }, + schedules: { + type: 'json', + description: 'Schedules (scheduleId, workflowType, paused, notes, futureActionTimes)', + }, + paused: { type: 'boolean', description: 'Whether the schedule is paused' }, + notes: { type: 'string', description: 'Human-readable notes on the schedule' }, + spec: { + type: 'json', + description: 'Schedule spec (calendars, intervals, cron strings, jitter, time zone)', + }, + recentActions: { + type: 'json', + description: 'Recent schedule actions (scheduleTime, actualTime, workflowId, runId)', + }, + futureActionTimes: { type: 'json', description: 'Upcoming schedule action times (RFC 3339)' }, + // Describe Task Queue + pollers: { + type: 'json', + description: 'Workers polling the task queue (identity, lastAccessTime, ratePerSecond)', + }, + }, +} + +export const TemporalBlockMeta = { + tags: ['automation'], + templates: [ + { + icon: TemporalIcon, + title: 'Temporal order approval gate', + prompt: + 'Create a workflow that receives an approval decision from a form, signals the matching Temporal order workflow with the decision, and posts a confirmation to Slack with the workflow ID and current status.', + modules: ['agent', 'workflows'], + category: 'operations', + tags: ['automation', 'approvals'], + alsoIntegrations: ['slack'], + }, + { + icon: TemporalIcon, + title: 'Temporal failed workflow digest', + prompt: + 'Build a scheduled daily workflow that lists Temporal executions that failed or timed out in the last 24 hours, pulls the close event from each history to extract the failure, and posts a digest to Slack grouped by workflow type.', + modules: ['scheduled', 'agent', 'workflows'], + category: 'engineering', + tags: ['devops', 'reporting'], + alsoIntegrations: ['slack'], + }, + { + icon: TemporalIcon, + title: 'Temporal stuck workflow watcher', + prompt: + 'Create a scheduled workflow that lists running Temporal executions, describes each one to inspect pending activities, flags workflows whose activities are retrying with high attempt counts, and opens a Linear ticket with the failure details.', + modules: ['scheduled', 'agent', 'workflows'], + category: 'engineering', + tags: ['devops', 'monitoring'], + alsoIntegrations: ['linear'], + }, + { + icon: TemporalIcon, + title: 'Temporal kickoff from intake form', + prompt: + 'Build a workflow that starts a Temporal workflow execution with input assembled from an intake form submission, polls describe until the execution closes, and writes the final status and timing to a tracking table.', + modules: ['tables', 'agent', 'workflows'], + category: 'operations', + tags: ['automation'], + }, + { + icon: TemporalIcon, + title: 'Temporal status lookup agent', + prompt: + 'Create an agent that answers "where is my order" questions by querying the matching Temporal workflow for its current state and summarizing the progress, falling back to the latest history events when no query handler responds.', + modules: ['agent'], + category: 'support', + tags: ['customer-support'], + }, + { + icon: TemporalIcon, + title: 'Temporal runaway workflow janitor', + prompt: + 'Build a scheduled weekly workflow that lists Temporal executions running longer than seven days, describes each to confirm it is stalled, requests cancellation with a recorded reason, and terminates any execution that ignores the cancellation after a grace period.', + modules: ['scheduled', 'agent', 'workflows'], + category: 'engineering', + tags: ['devops', 'automation'], + }, + { + icon: TemporalIcon, + title: 'Temporal incident escalation bridge', + prompt: + 'Create a workflow that signals a Temporal incident-response workflow with escalation details when a monitoring alert fires, using signal-with-start so a new incident workflow is created if one is not already running.', + modules: ['agent', 'workflows'], + category: 'engineering', + tags: ['incident-management', 'automation'], + alsoIntegrations: ['pagerduty'], + }, + ], + skills: [ + { + name: 'start-workflow-execution', + description: + 'Start a Temporal workflow execution with the right input and report the workflow and run IDs.', + content: + '# Start a Temporal Workflow\n\nKick off a workflow execution on the cluster.\n\n## Steps\n1. Confirm the workflow type, task queue, and a unique workflow ID.\n2. Assemble the JSON input arguments for the workflow.\n3. Start the workflow and capture the run ID.\n4. Describe the execution to confirm it is running.\n\n## Output\nA confirmation with the workflow ID, run ID, and initial status.', + }, + { + name: 'investigate-failed-workflow', + description: + 'Describe a failed Temporal workflow and pull its close event to explain why it failed.', + content: + '# Investigate a Failed Temporal Workflow\n\nDiagnose a workflow failure.\n\n## Steps\n1. Describe the workflow to confirm its status and timing.\n2. Fetch the history filtered to the close event to get the failure details.\n3. If needed, fetch earlier history pages to trace the failing activity.\n4. Summarize the root cause.\n\n## Output\nA failure summary with the failing event, error message, and a recommendation.', + }, + { + name: 'signal-running-workflow', + description: 'Send a signal to a running Temporal workflow and confirm it was delivered.', + content: + '# Signal a Temporal Workflow\n\nDeliver data or a decision to a running execution.\n\n## Steps\n1. Find the target execution by workflow ID (or list executions to locate it).\n2. Send the signal with the JSON payload.\n3. Query or describe the workflow to confirm the signal took effect.\n\n## Output\nA confirmation with the workflow ID, signal name, and resulting state.', + }, + { + name: 'audit-running-workflows', + description: 'List running Temporal executions and surface long-running or stuck workflows.', + content: + '# Audit Running Temporal Workflows\n\nFind executions that need attention.\n\n## Steps\n1. List executions filtered to ExecutionStatus = "Running".\n2. Sort by start time and flag the longest-running executions.\n3. Describe flagged executions to inspect pending activities and retry counts.\n4. Recommend cancellation or escalation for stuck workflows.\n\n## Output\nA per-workflow report with age, pending activities, and a recommended action.', + }, + ], +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 2703ead3bd..6a4197eae7 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -273,6 +273,7 @@ import { TableBlock } from '@/blocks/blocks/table' import { TailscaleBlock, TailscaleBlockMeta } from '@/blocks/blocks/tailscale' import { TavilyBlock, TavilyBlockMeta } from '@/blocks/blocks/tavily' import { TelegramBlock, TelegramBlockMeta } from '@/blocks/blocks/telegram' +import { TemporalBlock, TemporalBlockMeta } from '@/blocks/blocks/temporal' import { TextractBlock, TextractBlockMeta, TextractV2Block } from '@/blocks/blocks/textract' import { ThinkingBlock } from '@/blocks/blocks/thinking' import { TinybirdBlock, TinybirdBlockMeta } from '@/blocks/blocks/tinybird' @@ -565,6 +566,7 @@ const BLOCK_REGISTRY: Record = { tailscale: TailscaleBlock, tavily: TavilyBlock, telegram: TelegramBlock, + temporal: TemporalBlock, textract: TextractBlock, textract_v2: TextractV2Block, thinking: ThinkingBlock, @@ -808,6 +810,7 @@ const BLOCK_META_REGISTRY: Record = { tailscale: TailscaleBlockMeta, tavily: TavilyBlockMeta, telegram: TelegramBlockMeta, + temporal: TemporalBlockMeta, textract: TextractBlockMeta, tinybird: TinybirdBlockMeta, trello: TrelloBlockMeta, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 6f5df2b76b..0348515173 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -5826,6 +5826,17 @@ export function DagsterIcon(props: SVGProps) { ) } +export function TemporalIcon(props: SVGProps) { + return ( + + + + ) +} + export function DatabricksIcon(props: SVGProps) { return ( diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index b7d26906bb..038bb532a9 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -193,6 +193,7 @@ import { TailscaleIcon, TavilyIcon, TelegramIcon, + TemporalIcon, TextractIcon, TinybirdIcon, TrelloIcon, @@ -409,6 +410,7 @@ export const blockTypeToIconMap: Record = { tailscale: TailscaleIcon, tavily: TavilyIcon, telegram: TelegramIcon, + temporal: TemporalIcon, textract_v2: TextractIcon, tinybird: TinybirdIcon, trello: TrelloIcon, diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index b6b6beeaf4..6ee035c066 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -14722,6 +14722,105 @@ "integrationType": "communication", "tags": ["messaging", "webhooks", "automation"] }, + { + "type": "temporal", + "slug": "temporal", + "name": "Temporal", + "description": "Start, signal, query, and manage Temporal workflow executions", + "longDescription": "Connect to a Temporal cluster over the server's HTTP API to start workflow executions, send signals, run queries against workflow state, describe and list executions, fetch event histories, and cancel or terminate running workflows. API key only required for servers with authentication enabled.", + "bgColor": "#141414", + "iconName": "TemporalIcon", + "docsUrl": "https://docs.sim.ai/integrations/temporal", + "operations": [ + { + "name": "Start Workflow", + "description": "Start a new workflow execution on a Temporal cluster." + }, + { + "name": "Signal Workflow", + "description": "Send a signal to a running Temporal workflow execution." + }, + { + "name": "Signal With Start", + "description": "Atomically signal a Temporal workflow, starting it first if it is not already running, so the signal is never lost." + }, + { + "name": "Query Workflow", + "description": "Run a synchronous query against the state of a Temporal workflow execution and return the result." + }, + { + "name": "Update Workflow", + "description": "Invoke an update handler on a running Temporal workflow and wait for its result. Unlike a signal, an update is validated by the workflow and returns a response." + }, + { + "name": "Describe Workflow", + "description": "Get the current state of a Temporal workflow execution, including status, timing, memo, search attributes, and pending activities." + }, + { + "name": "List Workflows", + "description": "List workflow executions in a Temporal namespace, optionally filtered with a visibility query." + }, + { + "name": "Count Workflows", + "description": "Count workflow executions in a Temporal namespace matching a visibility query, with optional GROUP BY aggregation." + }, + { + "name": "Get Workflow History", + "description": "Fetch the event history of a Temporal workflow execution, optionally filtered to just the close event." + }, + { + "name": "Cancel Workflow", + "description": "Request cooperative cancellation of a running Temporal workflow execution. The workflow decides how to respond to the request." + }, + { + "name": "Terminate Workflow", + "description": "Forcefully terminate a Temporal workflow execution immediately, without giving the workflow a chance to react." + }, + { + "name": "Reset Workflow", + "description": "Reset a Temporal workflow execution to a past workflow task, terminating the current run and replaying from the reset point in a new run." + }, + { + "name": "Describe Task Queue", + "description": "List the workers currently polling a Temporal task queue, to check whether a workflow or activity has live workers." + }, + { + "name": "Create Schedule", + "description": "Create a Temporal schedule that starts a workflow on a cron or interval cadence." + }, + { + "name": "List Schedules", + "description": "List schedules in a Temporal namespace." + }, + { + "name": "Describe Schedule", + "description": "Get the configuration and current state of a Temporal schedule, including its spec, recent actions, and upcoming run times." + }, + { + "name": "Pause Schedule", + "description": "Pause a Temporal schedule so it stops taking actions until unpaused." + }, + { + "name": "Unpause Schedule", + "description": "Unpause a Temporal schedule so it resumes taking actions." + }, + { + "name": "Trigger Schedule", + "description": "Trigger an immediate action of a Temporal schedule, outside its normal spec." + }, + { + "name": "Delete Schedule", + "description": "Delete a Temporal schedule. Workflows already started by the schedule keep running." + } + ], + "operationCount": 20, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "devops", + "tags": ["automation"] + }, { "type": "tinybird", "slug": "tinybird", diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 35572109fb..0fedf7ee45 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -3171,6 +3171,28 @@ import { telegramSendPhotoTool, telegramSendVideoTool, } from '@/tools/telegram' +import { + temporalCancelWorkflowTool, + temporalCountWorkflowsTool, + temporalCreateScheduleTool, + temporalDeleteScheduleTool, + temporalDescribeScheduleTool, + temporalDescribeTaskQueueTool, + temporalDescribeWorkflowTool, + temporalGetWorkflowHistoryTool, + temporalListSchedulesTool, + temporalListWorkflowsTool, + temporalPauseScheduleTool, + temporalQueryWorkflowTool, + temporalResetWorkflowTool, + temporalSignalWithStartTool, + temporalSignalWorkflowTool, + temporalStartWorkflowTool, + temporalTerminateWorkflowTool, + temporalTriggerScheduleTool, + temporalUnpauseScheduleTool, + temporalUpdateWorkflowTool, +} from '@/tools/temporal' import { textractParserTool, textractParserV2Tool } from '@/tools/textract' import { thinkingTool } from '@/tools/thinking' import { @@ -5562,6 +5584,26 @@ export const tools: Record = { telegram_send_photo: telegramSendPhotoTool, telegram_send_video: telegramSendVideoTool, telegram_send_document: telegramSendDocumentTool, + temporal_start_workflow: temporalStartWorkflowTool, + temporal_signal_workflow: temporalSignalWorkflowTool, + temporal_signal_with_start: temporalSignalWithStartTool, + temporal_query_workflow: temporalQueryWorkflowTool, + temporal_update_workflow: temporalUpdateWorkflowTool, + temporal_describe_workflow: temporalDescribeWorkflowTool, + temporal_list_workflows: temporalListWorkflowsTool, + temporal_count_workflows: temporalCountWorkflowsTool, + temporal_get_workflow_history: temporalGetWorkflowHistoryTool, + temporal_cancel_workflow: temporalCancelWorkflowTool, + temporal_terminate_workflow: temporalTerminateWorkflowTool, + temporal_reset_workflow: temporalResetWorkflowTool, + temporal_describe_task_queue: temporalDescribeTaskQueueTool, + temporal_create_schedule: temporalCreateScheduleTool, + temporal_list_schedules: temporalListSchedulesTool, + temporal_describe_schedule: temporalDescribeScheduleTool, + temporal_pause_schedule: temporalPauseScheduleTool, + temporal_unpause_schedule: temporalUnpauseScheduleTool, + temporal_trigger_schedule: temporalTriggerScheduleTool, + temporal_delete_schedule: temporalDeleteScheduleTool, clay_populate: clayPopulateTool, clickhouse_query: clickhouseQueryTool, clickhouse_insert: clickhouseInsertTool, diff --git a/apps/sim/tools/temporal/cancel_workflow.ts b/apps/sim/tools/temporal/cancel_workflow.ts new file mode 100644 index 0000000000..5421f75a69 --- /dev/null +++ b/apps/sim/tools/temporal/cancel_workflow.ts @@ -0,0 +1,94 @@ +import type { + TemporalCancelWorkflowParams, + TemporalCancelWorkflowResponse, +} from '@/tools/temporal/types' +import { + parseTemporalResponse, + TEMPORAL_CLIENT_IDENTITY, + temporalRequestHeaders, + temporalWorkflowUrl, + workflowExecutionRef, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const cancelWorkflowTool: ToolConfig< + TemporalCancelWorkflowParams, + TemporalCancelWorkflowResponse +> = { + id: 'temporal_cancel_workflow', + name: 'Temporal Cancel Workflow', + description: + 'Request cooperative cancellation of a running Temporal workflow execution. The workflow decides how to respond to the request.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + workflowId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Workflow ID of the execution to cancel', + }, + runId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Run ID of a specific run to cancel (defaults to the latest run)', + }, + reason: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reason for the cancellation, recorded in the workflow history', + }, + }, + + request: { + url: (params) => + `${temporalWorkflowUrl(params.serverUrl, params.namespace, params.workflowId)}/cancel`, + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => { + const body: Record = { + workflowExecution: workflowExecutionRef(params.workflowId, params.runId), + identity: TEMPORAL_CLIENT_IDENTITY, + } + if (params.reason) body.reason = params.reason + return body + }, + }, + + transformResponse: async (response: Response, params) => { + await parseTemporalResponse(response, 'cancel workflow') + return { + success: true, + output: { + workflowId: params?.workflowId ?? '', + }, + } + }, + + outputs: { + workflowId: { + type: 'string', + description: 'Workflow ID of the execution whose cancellation was requested', + }, + }, +} diff --git a/apps/sim/tools/temporal/count_workflows.ts b/apps/sim/tools/temporal/count_workflows.ts new file mode 100644 index 0000000000..504dd0511a --- /dev/null +++ b/apps/sim/tools/temporal/count_workflows.ts @@ -0,0 +1,93 @@ +import type { + TemporalCountWorkflowsParams, + TemporalCountWorkflowsResponse, +} from '@/tools/temporal/types' +import { + decodePayload, + parseTemporalResponse, + type TemporalPayload, + temporalNamespaceUrl, + temporalRequestHeaders, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const countWorkflowsTool: ToolConfig< + TemporalCountWorkflowsParams, + TemporalCountWorkflowsResponse +> = { + id: 'temporal_count_workflows', + name: 'Temporal Count Workflows', + description: + 'Count workflow executions in a Temporal namespace matching a visibility query, with optional GROUP BY aggregation.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Visibility count filter, e.g. ExecutionStatus = "Running" or ... GROUP BY ExecutionStatus (empty counts all executions)', + }, + }, + + request: { + url: (params) => { + const base = `${temporalNamespaceUrl(params.serverUrl, params.namespace)}/workflow-count` + return params.query ? `${base}?query=${encodeURIComponent(params.query)}` : base + }, + method: 'GET', + headers: (params) => temporalRequestHeaders(params), + }, + + transformResponse: async (response: Response) => { + const data = await parseTemporalResponse<{ + count?: string + groups?: Array<{ groupValues?: TemporalPayload[]; count?: string }> + }>(response, 'count workflows') + + return { + success: true, + output: { + count: data.count != null ? Number(data.count) : 0, + groups: (data.groups ?? []).map((group) => ({ + values: (group.groupValues ?? []).map(decodePayload), + count: group.count != null ? Number(group.count) : 0, + })), + }, + } + }, + + outputs: { + count: { type: 'number', description: 'Number of workflow executions matching the query' }, + groups: { + type: 'array', + description: 'Per-group counts when the query uses GROUP BY (empty otherwise)', + items: { + type: 'object', + properties: { + values: { type: 'json', description: 'Decoded values of the GROUP BY fields' }, + count: { type: 'number', description: 'Number of executions in the group' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/temporal/create_schedule.ts b/apps/sim/tools/temporal/create_schedule.ts new file mode 100644 index 0000000000..3ea76e407d --- /dev/null +++ b/apps/sim/tools/temporal/create_schedule.ts @@ -0,0 +1,175 @@ +import { generateId } from '@sim/utils/id' +import type { + TemporalCreateScheduleParams, + TemporalScheduleMutationResponse, +} from '@/tools/temporal/types' +import { + parseJsonArgs, + parseTemporalResponse, + TEMPORAL_CLIENT_IDENTITY, + temporalRequestHeaders, + temporalScheduleUrl, + toDurationString, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const createScheduleTool: ToolConfig< + TemporalCreateScheduleParams, + TemporalScheduleMutationResponse +> = { + id: 'temporal_create_schedule', + name: 'Temporal Create Schedule', + description: 'Create a Temporal schedule that starts a workflow on a cron or interval cadence.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + scheduleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Unique ID for the new schedule (e.g., nightly-report)', + }, + workflowId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Workflow ID for started workflows (the schedule appends the run time to keep IDs unique)', + }, + workflowType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Registered workflow type name the schedule starts (e.g., ReportWorkflow)', + }, + taskQueue: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Task queue the workflow worker polls (e.g., reports)', + }, + input: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Workflow input as JSON. A top-level array is passed as the argument list (one argument per element); any other value is passed as a single argument', + }, + cronExpressions: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Cron expressions defining when the schedule fires, comma- or newline-separated for multiple (e.g., "0 12 * * *"). At least one of cronExpressions or intervalSeconds is required', + }, + intervalSeconds: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Fixed interval between actions in seconds. At least one of cronExpressions or intervalSeconds is required', + }, + timezone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'IANA time zone for cron evaluation (e.g., America/New_York; defaults to UTC)', + }, + overlapPolicy: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Policy when an action would overlap a still-running one (defaults to skip): SCHEDULE_OVERLAP_POLICY_SKIP, SCHEDULE_OVERLAP_POLICY_BUFFER_ONE, SCHEDULE_OVERLAP_POLICY_BUFFER_ALL, SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER, SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER, or SCHEDULE_OVERLAP_POLICY_ALLOW_ALL', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Human-readable notes stored on the schedule', + }, + paused: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Create the schedule in a paused state (defaults to active)', + }, + }, + + request: { + url: (params) => temporalScheduleUrl(params.serverUrl, params.namespace, params.scheduleId), + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => { + const cronString = (params.cronExpressions ?? '') + .split(/[\n,]/) + .map((expression) => expression.trim()) + .filter(Boolean) + const interval = toDurationString(params.intervalSeconds) + if (cronString.length === 0 && !interval) { + throw new Error('At least one of cronExpressions or intervalSeconds is required') + } + + const spec: Record = {} + if (cronString.length > 0) spec.cronString = cronString + if (interval) spec.interval = [{ interval }] + if (params.timezone?.trim()) spec.timezoneName = params.timezone.trim() + + const startWorkflow: Record = { + workflowId: params.workflowId.trim(), + workflowType: { name: params.workflowType.trim() }, + taskQueue: { name: params.taskQueue.trim() }, + } + const input = parseJsonArgs(params.input, 'input') + if (input) startWorkflow.input = input + + const schedule: Record = { + spec, + action: { startWorkflow }, + } + if (params.overlapPolicy) schedule.policies = { overlapPolicy: params.overlapPolicy } + const state: Record = {} + if (params.notes?.trim()) state.notes = params.notes.trim() + if (params.paused) state.paused = true + if (Object.keys(state).length > 0) schedule.state = state + + return { + schedule, + identity: TEMPORAL_CLIENT_IDENTITY, + requestId: generateId(), + } + }, + }, + + transformResponse: async (response: Response, params) => { + await parseTemporalResponse(response, 'create schedule') + return { + success: true, + output: { + scheduleId: params?.scheduleId ?? '', + }, + } + }, + + outputs: { + scheduleId: { type: 'string', description: 'ID of the created schedule' }, + }, +} diff --git a/apps/sim/tools/temporal/delete_schedule.ts b/apps/sim/tools/temporal/delete_schedule.ts new file mode 100644 index 0000000000..a7fd9e0508 --- /dev/null +++ b/apps/sim/tools/temporal/delete_schedule.ts @@ -0,0 +1,70 @@ +import type { + TemporalDeleteScheduleParams, + TemporalScheduleMutationResponse, +} from '@/tools/temporal/types' +import { + parseTemporalResponse, + TEMPORAL_CLIENT_IDENTITY, + temporalRequestHeaders, + temporalScheduleUrl, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteScheduleTool: ToolConfig< + TemporalDeleteScheduleParams, + TemporalScheduleMutationResponse +> = { + id: 'temporal_delete_schedule', + name: 'Temporal Delete Schedule', + description: + 'Delete a Temporal schedule. Workflows already started by the schedule keep running.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + scheduleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the schedule to delete', + }, + }, + + request: { + url: (params) => + `${temporalScheduleUrl(params.serverUrl, params.namespace, params.scheduleId)}?identity=${encodeURIComponent(TEMPORAL_CLIENT_IDENTITY)}`, + method: 'DELETE', + headers: (params) => temporalRequestHeaders(params), + }, + + transformResponse: async (response: Response, params) => { + await parseTemporalResponse(response, 'delete schedule') + return { + success: true, + output: { + scheduleId: params?.scheduleId ?? '', + }, + } + }, + + outputs: { + scheduleId: { type: 'string', description: 'ID of the deleted schedule' }, + }, +} diff --git a/apps/sim/tools/temporal/describe_schedule.ts b/apps/sim/tools/temporal/describe_schedule.ts new file mode 100644 index 0000000000..b9c5fd0329 --- /dev/null +++ b/apps/sim/tools/temporal/describe_schedule.ts @@ -0,0 +1,147 @@ +import type { + TemporalDescribeScheduleParams, + TemporalDescribeScheduleResponse, +} from '@/tools/temporal/types' +import { + parseTemporalResponse, + temporalRequestHeaders, + temporalScheduleUrl, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +interface RawDescribeScheduleResponse { + schedule?: { + spec?: Record + action?: { + startWorkflow?: { + workflowId?: string + workflowType?: { name?: string } + taskQueue?: { name?: string } + } + } + state?: { + notes?: string + paused?: boolean + } + } + info?: { + recentActions?: Array<{ + scheduleTime?: string + actualTime?: string + startWorkflowResult?: { workflowId?: string; runId?: string } + }> + futureActionTimes?: string[] + } +} + +export const describeScheduleTool: ToolConfig< + TemporalDescribeScheduleParams, + TemporalDescribeScheduleResponse +> = { + id: 'temporal_describe_schedule', + name: 'Temporal Describe Schedule', + description: + 'Get the configuration and current state of a Temporal schedule, including its spec, recent actions, and upcoming run times.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + scheduleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the schedule to describe', + }, + }, + + request: { + url: (params) => temporalScheduleUrl(params.serverUrl, params.namespace, params.scheduleId), + method: 'GET', + headers: (params) => temporalRequestHeaders(params), + }, + + transformResponse: async (response: Response, params) => { + const data = await parseTemporalResponse( + response, + 'describe schedule' + ) + const startWorkflow = data.schedule?.action?.startWorkflow + + return { + success: true, + output: { + scheduleId: params?.scheduleId ?? '', + paused: data.schedule?.state?.paused ?? false, + notes: data.schedule?.state?.notes ?? null, + workflowType: startWorkflow?.workflowType?.name ?? null, + taskQueue: startWorkflow?.taskQueue?.name ?? null, + workflowId: startWorkflow?.workflowId ?? null, + spec: data.schedule?.spec ?? null, + recentActions: (data.info?.recentActions ?? []).map((action) => ({ + scheduleTime: action.scheduleTime ?? null, + actualTime: action.actualTime ?? null, + workflowId: action.startWorkflowResult?.workflowId ?? null, + runId: action.startWorkflowResult?.runId ?? null, + })), + futureActionTimes: data.info?.futureActionTimes ?? [], + }, + } + }, + + outputs: { + scheduleId: { type: 'string', description: 'Schedule ID' }, + paused: { type: 'boolean', description: 'Whether the schedule is paused' }, + notes: { type: 'string', description: 'Human-readable notes on the schedule', optional: true }, + workflowType: { + type: 'string', + description: 'Workflow type the schedule starts', + optional: true, + }, + taskQueue: { + type: 'string', + description: 'Task queue used for started workflows', + optional: true, + }, + workflowId: { + type: 'string', + description: 'Workflow ID template for started workflows', + optional: true, + }, + spec: { + type: 'json', + description: 'Schedule spec (calendars, intervals, cron strings, jitter, time zone)', + optional: true, + }, + recentActions: { + type: 'array', + description: 'Most recent actions taken by the schedule', + items: { + type: 'object', + properties: { + scheduleTime: { type: 'string', description: 'Nominal scheduled time (RFC 3339)' }, + actualTime: { type: 'string', description: 'Actual time the action ran (RFC 3339)' }, + workflowId: { type: 'string', description: 'Workflow ID of the started execution' }, + runId: { type: 'string', description: 'Run ID of the started execution' }, + }, + }, + }, + futureActionTimes: { type: 'json', description: 'Upcoming action times (RFC 3339)' }, + }, +} diff --git a/apps/sim/tools/temporal/describe_task_queue.ts b/apps/sim/tools/temporal/describe_task_queue.ts new file mode 100644 index 0000000000..ef06b2b5eb --- /dev/null +++ b/apps/sim/tools/temporal/describe_task_queue.ts @@ -0,0 +1,107 @@ +import type { + TemporalDescribeTaskQueueParams, + TemporalDescribeTaskQueueResponse, +} from '@/tools/temporal/types' +import { + parseTemporalResponse, + temporalNamespaceUrl, + temporalRequestHeaders, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const describeTaskQueueTool: ToolConfig< + TemporalDescribeTaskQueueParams, + TemporalDescribeTaskQueueResponse +> = { + id: 'temporal_describe_task_queue', + name: 'Temporal Describe Task Queue', + description: + 'List the workers currently polling a Temporal task queue, to check whether a workflow or activity has live workers.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + taskQueue: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the task queue to describe (e.g., orders)', + }, + taskQueueType: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Type of pollers to list: TASK_QUEUE_TYPE_WORKFLOW (default) or TASK_QUEUE_TYPE_ACTIVITY', + }, + }, + + request: { + url: (params) => { + const base = `${temporalNamespaceUrl(params.serverUrl, params.namespace)}/task-queues/${encodeURIComponent(params.taskQueue.trim())}` + return params.taskQueueType + ? `${base}?taskQueueType=${encodeURIComponent(params.taskQueueType)}` + : base + }, + method: 'GET', + headers: (params) => temporalRequestHeaders(params), + }, + + transformResponse: async (response: Response, params) => { + const data = await parseTemporalResponse<{ + pollers?: Array<{ + identity?: string + lastAccessTime?: string + ratePerSecond?: number + }> + }>(response, 'describe task queue') + + return { + success: true, + output: { + taskQueue: params?.taskQueue?.trim() ?? '', + pollers: (data.pollers ?? []).map((poller) => ({ + identity: poller.identity ?? null, + lastAccessTime: poller.lastAccessTime ?? null, + ratePerSecond: poller.ratePerSecond ?? null, + })), + }, + } + }, + + outputs: { + taskQueue: { type: 'string', description: 'Name of the described task queue' }, + pollers: { + type: 'array', + description: 'Workers currently polling the task queue (empty when no workers are running)', + items: { + type: 'object', + properties: { + identity: { type: 'string', description: 'Identity of the polling worker' }, + lastAccessTime: { + type: 'string', + description: 'Last time the worker polled the queue (RFC 3339)', + }, + ratePerSecond: { type: 'number', description: 'Poller rate per second' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/temporal/describe_workflow.ts b/apps/sim/tools/temporal/describe_workflow.ts new file mode 100644 index 0000000000..20a2e09098 --- /dev/null +++ b/apps/sim/tools/temporal/describe_workflow.ts @@ -0,0 +1,151 @@ +import type { + TemporalDescribeWorkflowParams, + TemporalDescribeWorkflowResponse, +} from '@/tools/temporal/types' +import { + decodePayloadMap, + mapExecutionInfo, + parseTemporalResponse, + stripEnumPrefix, + type TemporalPayload, + type TemporalRawExecutionInfo, + temporalRequestHeaders, + temporalWorkflowUrl, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +interface RawDescribeResponse { + workflowExecutionInfo?: TemporalRawExecutionInfo & { + memo?: { fields?: Record } + searchAttributes?: { indexedFields?: Record } + } + pendingActivities?: Array<{ + activityId?: string + activityType?: { name?: string } + state?: string + attempt?: number + lastFailure?: { message?: string } + }> +} + +export const describeWorkflowTool: ToolConfig< + TemporalDescribeWorkflowParams, + TemporalDescribeWorkflowResponse +> = { + id: 'temporal_describe_workflow', + name: 'Temporal Describe Workflow', + description: + 'Get the current state of a Temporal workflow execution, including status, timing, memo, search attributes, and pending activities.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + workflowId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Workflow ID of the execution to describe', + }, + runId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Run ID of a specific run to describe (defaults to the latest run)', + }, + }, + + request: { + url: (params) => { + const base = temporalWorkflowUrl(params.serverUrl, params.namespace, params.workflowId) + const runId = params.runId?.trim() + return runId ? `${base}?execution.runId=${encodeURIComponent(runId)}` : base + }, + method: 'GET', + headers: (params) => temporalRequestHeaders(params), + }, + + transformResponse: async (response: Response) => { + const data = await parseTemporalResponse(response, 'describe workflow') + const info = data.workflowExecutionInfo + + return { + success: true, + output: { + ...mapExecutionInfo(info), + memo: decodePayloadMap(info?.memo?.fields), + searchAttributes: decodePayloadMap(info?.searchAttributes?.indexedFields), + pendingActivities: (data.pendingActivities ?? []).map((activity) => ({ + activityId: activity.activityId ?? null, + activityType: activity.activityType?.name ?? null, + state: stripEnumPrefix(activity.state, 'PENDING_ACTIVITY_STATE_'), + attempt: activity.attempt ?? null, + lastFailureMessage: activity.lastFailure?.message ?? null, + })), + }, + } + }, + + outputs: { + workflowId: { type: 'string', description: 'Workflow ID of the execution' }, + runId: { type: 'string', description: 'Run ID of the execution' }, + workflowType: { type: 'string', description: 'Workflow type name' }, + status: { + type: 'string', + description: + 'Execution status (RUNNING, COMPLETED, FAILED, CANCELED, TERMINATED, CONTINUED_AS_NEW, or TIMED_OUT)', + }, + startTime: { type: 'string', description: 'Start time of the execution (RFC 3339)' }, + closeTime: { + type: 'string', + description: 'Close time of the execution (RFC 3339), null while running', + optional: true, + }, + executionTime: { + type: 'string', + description: 'Effective execution start time (RFC 3339), e.g. the first cron run time', + optional: true, + }, + historyLength: { type: 'number', description: 'Number of events in the workflow history' }, + taskQueue: { type: 'string', description: 'Task queue of the execution' }, + memo: { type: 'json', description: 'Decoded memo fields attached to the execution' }, + searchAttributes: { type: 'json', description: 'Decoded search attribute values' }, + pendingActivities: { + type: 'array', + description: 'Activities currently pending on the execution', + items: { + type: 'object', + properties: { + activityId: { type: 'string', description: 'Activity ID' }, + activityType: { type: 'string', description: 'Activity type name' }, + state: { + type: 'string', + description: + 'Pending state (SCHEDULED, STARTED, CANCEL_REQUESTED, PAUSED, or PAUSE_REQUESTED)', + }, + attempt: { type: 'number', description: 'Current attempt number' }, + lastFailureMessage: { + type: 'string', + description: 'Message of the most recent failure, if the activity is retrying', + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/temporal/get_workflow_history.ts b/apps/sim/tools/temporal/get_workflow_history.ts new file mode 100644 index 0000000000..e8115a11eb --- /dev/null +++ b/apps/sim/tools/temporal/get_workflow_history.ts @@ -0,0 +1,137 @@ +import type { + TemporalGetWorkflowHistoryParams, + TemporalGetWorkflowHistoryResponse, +} from '@/tools/temporal/types' +import { + mapHistoryEvent, + parseTemporalResponse, + type TemporalRawHistoryEvent, + temporalRequestHeaders, + temporalWorkflowUrl, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const getWorkflowHistoryTool: ToolConfig< + TemporalGetWorkflowHistoryParams, + TemporalGetWorkflowHistoryResponse +> = { + id: 'temporal_get_workflow_history', + name: 'Temporal Get Workflow History', + description: + 'Fetch the event history of a Temporal workflow execution, optionally filtered to just the close event.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + workflowId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Workflow ID of the execution', + }, + runId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Run ID of a specific run (defaults to the latest run)', + }, + maximumPageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of history events to return per page', + }, + nextPageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token from a previous response, for pagination', + }, + historyEventFilterType: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Event filter: HISTORY_EVENT_FILTER_TYPE_ALL_EVENT (default) or HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT to return only the final close event', + }, + }, + + request: { + url: (params) => { + const search = new URLSearchParams() + const runId = params.runId?.trim() + if (runId) search.set('execution.runId', runId) + const pageSize = Number(params.maximumPageSize) + if (Number.isFinite(pageSize) && pageSize > 0) { + search.set('maximumPageSize', String(pageSize)) + } + if (params.nextPageToken) search.set('nextPageToken', params.nextPageToken) + if (params.historyEventFilterType) { + search.set('historyEventFilterType', params.historyEventFilterType) + } + const queryString = search.toString() + return `${temporalWorkflowUrl(params.serverUrl, params.namespace, params.workflowId)}/history${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => temporalRequestHeaders(params), + }, + + transformResponse: async (response: Response) => { + const data = await parseTemporalResponse<{ + history?: { events?: TemporalRawHistoryEvent[] } + nextPageToken?: string + }>(response, 'get workflow history') + + return { + success: true, + output: { + events: (data.history?.events ?? []).map(mapHistoryEvent), + nextPageToken: data.nextPageToken || null, + }, + } + }, + + outputs: { + events: { + type: 'array', + description: 'History events of the workflow execution, in order', + items: { + type: 'object', + properties: { + eventId: { type: 'number', description: 'Sequential ID of the event' }, + eventTime: { type: 'string', description: 'Time the event was recorded (RFC 3339)' }, + eventType: { + type: 'string', + description: 'Event type (e.g., WORKFLOW_EXECUTION_STARTED, ACTIVITY_TASK_COMPLETED)', + }, + attributes: { + type: 'json', + description: "The event's type-specific attributes (payload data is base64-encoded)", + }, + }, + }, + }, + nextPageToken: { + type: 'string', + description: 'Token for the next page of events, null when no more pages exist', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/temporal/index.ts b/apps/sim/tools/temporal/index.ts new file mode 100644 index 0000000000..314a85d4b3 --- /dev/null +++ b/apps/sim/tools/temporal/index.ts @@ -0,0 +1,41 @@ +import { cancelWorkflowTool } from '@/tools/temporal/cancel_workflow' +import { countWorkflowsTool } from '@/tools/temporal/count_workflows' +import { createScheduleTool } from '@/tools/temporal/create_schedule' +import { deleteScheduleTool } from '@/tools/temporal/delete_schedule' +import { describeScheduleTool } from '@/tools/temporal/describe_schedule' +import { describeTaskQueueTool } from '@/tools/temporal/describe_task_queue' +import { describeWorkflowTool } from '@/tools/temporal/describe_workflow' +import { getWorkflowHistoryTool } from '@/tools/temporal/get_workflow_history' +import { listSchedulesTool } from '@/tools/temporal/list_schedules' +import { listWorkflowsTool } from '@/tools/temporal/list_workflows' +import { pauseScheduleTool } from '@/tools/temporal/pause_schedule' +import { queryWorkflowTool } from '@/tools/temporal/query_workflow' +import { resetWorkflowTool } from '@/tools/temporal/reset_workflow' +import { signalWithStartTool } from '@/tools/temporal/signal_with_start' +import { signalWorkflowTool } from '@/tools/temporal/signal_workflow' +import { startWorkflowTool } from '@/tools/temporal/start_workflow' +import { terminateWorkflowTool } from '@/tools/temporal/terminate_workflow' +import { triggerScheduleTool } from '@/tools/temporal/trigger_schedule' +import { unpauseScheduleTool } from '@/tools/temporal/unpause_schedule' +import { updateWorkflowTool } from '@/tools/temporal/update_workflow' + +export const temporalStartWorkflowTool = startWorkflowTool +export const temporalSignalWorkflowTool = signalWorkflowTool +export const temporalSignalWithStartTool = signalWithStartTool +export const temporalQueryWorkflowTool = queryWorkflowTool +export const temporalUpdateWorkflowTool = updateWorkflowTool +export const temporalDescribeWorkflowTool = describeWorkflowTool +export const temporalListWorkflowsTool = listWorkflowsTool +export const temporalCountWorkflowsTool = countWorkflowsTool +export const temporalGetWorkflowHistoryTool = getWorkflowHistoryTool +export const temporalCancelWorkflowTool = cancelWorkflowTool +export const temporalTerminateWorkflowTool = terminateWorkflowTool +export const temporalResetWorkflowTool = resetWorkflowTool +export const temporalDescribeTaskQueueTool = describeTaskQueueTool +export const temporalCreateScheduleTool = createScheduleTool +export const temporalListSchedulesTool = listSchedulesTool +export const temporalDescribeScheduleTool = describeScheduleTool +export const temporalPauseScheduleTool = pauseScheduleTool +export const temporalUnpauseScheduleTool = unpauseScheduleTool +export const temporalTriggerScheduleTool = triggerScheduleTool +export const temporalDeleteScheduleTool = deleteScheduleTool diff --git a/apps/sim/tools/temporal/list_schedules.ts b/apps/sim/tools/temporal/list_schedules.ts new file mode 100644 index 0000000000..f028b47007 --- /dev/null +++ b/apps/sim/tools/temporal/list_schedules.ts @@ -0,0 +1,135 @@ +import type { + TemporalListSchedulesParams, + TemporalListSchedulesResponse, +} from '@/tools/temporal/types' +import { + parseTemporalResponse, + temporalNamespaceUrl, + temporalRequestHeaders, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +interface RawScheduleListEntry { + scheduleId?: string + info?: { + workflowType?: { name?: string } + notes?: string + paused?: boolean + futureActionTimes?: string[] + } +} + +export const listSchedulesTool: ToolConfig< + TemporalListSchedulesParams, + TemporalListSchedulesResponse +> = { + id: 'temporal_list_schedules', + name: 'Temporal List Schedules', + description: 'List schedules in a Temporal namespace.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Visibility filter over schedules, e.g. TemporalSchedulePaused = false (empty lists all schedules)', + }, + maximumPageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of schedules to return per page', + }, + nextPageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token from a previous response, for pagination', + }, + }, + + request: { + url: (params) => { + const search = new URLSearchParams() + if (params.query) search.set('query', params.query) + const pageSize = Number(params.maximumPageSize) + if (Number.isFinite(pageSize) && pageSize > 0) { + search.set('maximumPageSize', String(pageSize)) + } + if (params.nextPageToken) search.set('nextPageToken', params.nextPageToken) + const queryString = search.toString() + return `${temporalNamespaceUrl(params.serverUrl, params.namespace)}/schedules${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => temporalRequestHeaders(params), + }, + + transformResponse: async (response: Response) => { + const data = await parseTemporalResponse<{ + schedules?: RawScheduleListEntry[] + nextPageToken?: string + }>(response, 'list schedules') + + return { + success: true, + output: { + schedules: (data.schedules ?? []).map((schedule) => ({ + scheduleId: schedule.scheduleId ?? null, + workflowType: schedule.info?.workflowType?.name ?? null, + paused: schedule.info?.paused ?? false, + notes: schedule.info?.notes ?? null, + futureActionTimes: schedule.info?.futureActionTimes ?? [], + })), + nextPageToken: data.nextPageToken || null, + }, + } + }, + + outputs: { + schedules: { + type: 'array', + description: 'Schedules in the namespace', + items: { + type: 'object', + properties: { + scheduleId: { type: 'string', description: 'Schedule ID' }, + workflowType: { + type: 'string', + description: 'Workflow type the schedule starts', + }, + paused: { type: 'boolean', description: 'Whether the schedule is paused' }, + notes: { type: 'string', description: 'Human-readable notes on the schedule' }, + futureActionTimes: { + type: 'json', + description: 'Upcoming action times (RFC 3339)', + }, + }, + }, + }, + nextPageToken: { + type: 'string', + description: 'Token for the next page of results, null when no more pages exist', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/temporal/list_workflows.ts b/apps/sim/tools/temporal/list_workflows.ts new file mode 100644 index 0000000000..7b012dac48 --- /dev/null +++ b/apps/sim/tools/temporal/list_workflows.ts @@ -0,0 +1,131 @@ +import type { + TemporalListWorkflowsParams, + TemporalListWorkflowsResponse, +} from '@/tools/temporal/types' +import { + mapExecutionInfo, + parseTemporalResponse, + type TemporalRawExecutionInfo, + temporalNamespaceUrl, + temporalRequestHeaders, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const listWorkflowsTool: ToolConfig< + TemporalListWorkflowsParams, + TemporalListWorkflowsResponse +> = { + id: 'temporal_list_workflows', + name: 'Temporal List Workflows', + description: + 'List workflow executions in a Temporal namespace, optionally filtered with a visibility query.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Visibility list filter, e.g. WorkflowType = "OrderWorkflow" AND ExecutionStatus = "Running" (empty lists all executions)', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of executions to return per page', + }, + nextPageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token from a previous response, for pagination', + }, + }, + + request: { + url: (params) => { + const search = new URLSearchParams() + if (params.query) search.set('query', params.query) + const pageSize = Number(params.pageSize) + if (Number.isFinite(pageSize) && pageSize > 0) search.set('pageSize', String(pageSize)) + if (params.nextPageToken) search.set('nextPageToken', params.nextPageToken) + const queryString = search.toString() + return `${temporalNamespaceUrl(params.serverUrl, params.namespace)}/workflows${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => temporalRequestHeaders(params), + }, + + transformResponse: async (response: Response) => { + const data = await parseTemporalResponse<{ + executions?: TemporalRawExecutionInfo[] + nextPageToken?: string + }>(response, 'list workflows') + + return { + success: true, + output: { + executions: (data.executions ?? []).map(mapExecutionInfo), + nextPageToken: data.nextPageToken || null, + }, + } + }, + + outputs: { + executions: { + type: 'array', + description: 'Workflow executions matching the query', + items: { + type: 'object', + properties: { + workflowId: { type: 'string', description: 'Workflow ID of the execution' }, + runId: { type: 'string', description: 'Run ID of the execution' }, + workflowType: { type: 'string', description: 'Workflow type name' }, + status: { + type: 'string', + description: + 'Execution status (RUNNING, COMPLETED, FAILED, CANCELED, TERMINATED, CONTINUED_AS_NEW, or TIMED_OUT)', + }, + startTime: { type: 'string', description: 'Start time of the execution (RFC 3339)' }, + closeTime: { + type: 'string', + description: 'Close time of the execution (RFC 3339), null while running', + }, + executionTime: { + type: 'string', + description: 'Effective execution start time (RFC 3339)', + }, + historyLength: { + type: 'number', + description: 'Number of events in the workflow history', + }, + taskQueue: { type: 'string', description: 'Task queue of the execution' }, + }, + }, + }, + nextPageToken: { + type: 'string', + description: 'Token for the next page of results, null when no more pages exist', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/temporal/pause_schedule.ts b/apps/sim/tools/temporal/pause_schedule.ts new file mode 100644 index 0000000000..56e517be29 --- /dev/null +++ b/apps/sim/tools/temporal/pause_schedule.ts @@ -0,0 +1,79 @@ +import type { + TemporalPatchScheduleParams, + TemporalScheduleMutationResponse, +} from '@/tools/temporal/types' +import { + parseTemporalResponse, + TEMPORAL_CLIENT_IDENTITY, + temporalRequestHeaders, + temporalScheduleUrl, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const pauseScheduleTool: ToolConfig< + TemporalPatchScheduleParams, + TemporalScheduleMutationResponse +> = { + id: 'temporal_pause_schedule', + name: 'Temporal Pause Schedule', + description: 'Pause a Temporal schedule so it stops taking actions until unpaused.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + scheduleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the schedule to pause', + }, + reason: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "Reason recorded in the schedule's notes", + }, + }, + + request: { + url: (params) => + `${temporalScheduleUrl(params.serverUrl, params.namespace, params.scheduleId)}/patch`, + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => ({ + patch: { pause: params.reason || 'Paused via Sim' }, + identity: TEMPORAL_CLIENT_IDENTITY, + }), + }, + + transformResponse: async (response: Response, params) => { + await parseTemporalResponse(response, 'pause schedule') + return { + success: true, + output: { + scheduleId: params?.scheduleId ?? '', + }, + } + }, + + outputs: { + scheduleId: { type: 'string', description: 'ID of the paused schedule' }, + }, +} diff --git a/apps/sim/tools/temporal/query_workflow.ts b/apps/sim/tools/temporal/query_workflow.ts new file mode 100644 index 0000000000..eed03e8dea --- /dev/null +++ b/apps/sim/tools/temporal/query_workflow.ts @@ -0,0 +1,117 @@ +import type { + TemporalQueryWorkflowParams, + TemporalQueryWorkflowResponse, +} from '@/tools/temporal/types' +import { + decodePayloads, + parseJsonArgs, + parseTemporalResponse, + stripEnumPrefix, + type TemporalPayloads, + temporalRequestHeaders, + temporalWorkflowUrl, + workflowExecutionRef, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const queryWorkflowTool: ToolConfig< + TemporalQueryWorkflowParams, + TemporalQueryWorkflowResponse +> = { + id: 'temporal_query_workflow', + name: 'Temporal Query Workflow', + description: + 'Run a synchronous query against the state of a Temporal workflow execution and return the result.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + workflowId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Workflow ID of the execution to query', + }, + runId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Run ID of a specific run to query (defaults to the latest run)', + }, + queryType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the query handler to invoke (e.g., getStatus)', + }, + queryArgs: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Query arguments as JSON. A top-level array is passed as the argument list (one argument per element); any other value is passed as a single argument', + }, + }, + + request: { + url: (params) => + `${temporalWorkflowUrl(params.serverUrl, params.namespace, params.workflowId)}/query/${encodeURIComponent(params.queryType.trim())}`, + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => { + const query: Record = { queryType: params.queryType.trim() } + const args = parseJsonArgs(params.queryArgs, 'queryArgs') + if (args) query.queryArgs = args + return { execution: workflowExecutionRef(params.workflowId, params.runId), query } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await parseTemporalResponse<{ + queryResult?: TemporalPayloads + queryRejected?: { status?: string } + }>(response, 'query workflow') + + if (data.queryRejected) { + const status = stripEnumPrefix(data.queryRejected.status, 'WORKFLOW_EXECUTION_STATUS_') + throw new Error(`Temporal query workflow rejected: workflow status is ${status ?? 'unknown'}`) + } + + const decoded = decodePayloads(data.queryResult) + return { + success: true, + output: { + workflowId: params?.workflowId ?? '', + queryType: params?.queryType ?? '', + result: decoded.length > 1 ? decoded : (decoded[0] ?? null), + }, + } + }, + + outputs: { + workflowId: { type: 'string', description: 'Workflow ID of the queried execution' }, + queryType: { type: 'string', description: 'Name of the query that was run' }, + result: { + type: 'json', + description: + 'Decoded query result. A single payload is returned as its JSON value; multiple payloads are returned as an array', + }, + }, +} diff --git a/apps/sim/tools/temporal/reset_workflow.ts b/apps/sim/tools/temporal/reset_workflow.ts new file mode 100644 index 0000000000..6ee4181332 --- /dev/null +++ b/apps/sim/tools/temporal/reset_workflow.ts @@ -0,0 +1,107 @@ +import { generateId } from '@sim/utils/id' +import type { + TemporalResetWorkflowParams, + TemporalResetWorkflowResponse, +} from '@/tools/temporal/types' +import { + parseTemporalResponse, + TEMPORAL_CLIENT_IDENTITY, + temporalRequestHeaders, + temporalWorkflowUrl, + workflowExecutionRef, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const resetWorkflowTool: ToolConfig< + TemporalResetWorkflowParams, + TemporalResetWorkflowResponse +> = { + id: 'temporal_reset_workflow', + name: 'Temporal Reset Workflow', + description: + 'Reset a Temporal workflow execution to a past workflow task, terminating the current run and replaying from the reset point in a new run.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + workflowId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Workflow ID of the execution to reset', + }, + runId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Run ID of a specific run to reset (defaults to the latest run)', + }, + workflowTaskFinishEventId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: + 'Event ID of the workflow task finish event to reset to — a WORKFLOW_TASK_COMPLETED, WORKFLOW_TASK_TIMED_OUT, WORKFLOW_TASK_FAILED, or WORKFLOW_TASK_STARTED event (find it with Get Workflow History)', + }, + reason: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reason for the reset, recorded in the workflow history', + }, + }, + + request: { + url: (params) => + `${temporalWorkflowUrl(params.serverUrl, params.namespace, params.workflowId)}/reset`, + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => { + const eventId = Number(params.workflowTaskFinishEventId) + if (!Number.isFinite(eventId) || eventId <= 0) { + throw new Error('workflowTaskFinishEventId must be a positive event ID') + } + const body: Record = { + workflowExecution: workflowExecutionRef(params.workflowId, params.runId), + workflowTaskFinishEventId: eventId, + identity: TEMPORAL_CLIENT_IDENTITY, + requestId: generateId(), + } + if (params.reason) body.reason = params.reason + return body + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await parseTemporalResponse<{ runId?: string }>(response, 'reset workflow') + return { + success: true, + output: { + workflowId: params?.workflowId ?? '', + runId: data.runId ?? '', + }, + } + }, + + outputs: { + workflowId: { type: 'string', description: 'Workflow ID of the reset execution' }, + runId: { type: 'string', description: 'Run ID of the new run created by the reset' }, + }, +} diff --git a/apps/sim/tools/temporal/signal_with_start.ts b/apps/sim/tools/temporal/signal_with_start.ts new file mode 100644 index 0000000000..279becbb27 --- /dev/null +++ b/apps/sim/tools/temporal/signal_with_start.ts @@ -0,0 +1,187 @@ +import { generateId } from '@sim/utils/id' +import type { + TemporalSignalWithStartParams, + TemporalSignalWithStartResponse, +} from '@/tools/temporal/types' +import { + parseJsonArgs, + parseJsonPayloadMap, + parseTemporalResponse, + TEMPORAL_CLIENT_IDENTITY, + temporalRequestHeaders, + temporalWorkflowUrl, + toDurationString, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const signalWithStartTool: ToolConfig< + TemporalSignalWithStartParams, + TemporalSignalWithStartResponse +> = { + id: 'temporal_signal_with_start', + name: 'Temporal Signal With Start', + description: + 'Atomically signal a Temporal workflow, starting it first if it is not already running, so the signal is never lost.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + workflowId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Workflow ID to signal, or to start and signal (e.g., order-1234)', + }, + workflowType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Registered workflow type name to start if the workflow is not running', + }, + taskQueue: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Task queue the workflow worker polls (e.g., orders)', + }, + signalName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the signal handler to invoke (e.g., approve-order)', + }, + input: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Workflow start input as JSON, used only when a new execution is started. A top-level array is passed as the argument list; any other value is passed as a single argument', + }, + signalInput: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Signal input as JSON. A top-level array is passed as the argument list (one argument per element); any other value is passed as a single argument', + }, + workflowIdReusePolicy: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Policy for reusing a closed workflow ID: WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, or WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING', + }, + workflowIdConflictPolicy: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Policy when a workflow with the same ID is already running (defaults to using the existing run): WORKFLOW_ID_CONFLICT_POLICY_FAIL, WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, or WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING', + }, + cronSchedule: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cron schedule for recurring executions (e.g., "0 12 * * *")', + }, + executionTimeoutSeconds: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Total workflow execution timeout in seconds, including retries and continue-as-new', + }, + runTimeoutSeconds: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Timeout for a single workflow run in seconds', + }, + memo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of memo fields to attach to the execution', + }, + searchAttributes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of search attribute values to index the execution with', + }, + }, + + request: { + url: (params) => + `${temporalWorkflowUrl(params.serverUrl, params.namespace, params.workflowId)}/signal-with-start/${encodeURIComponent(params.signalName.trim())}`, + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => { + const body: Record = { + workflowType: { name: params.workflowType.trim() }, + taskQueue: { name: params.taskQueue.trim() }, + identity: TEMPORAL_CLIENT_IDENTITY, + requestId: generateId(), + } + const input = parseJsonArgs(params.input, 'input') + if (input) body.input = input + const signalInput = parseJsonArgs(params.signalInput, 'signalInput') + if (signalInput) body.signalInput = signalInput + if (params.workflowIdReusePolicy) body.workflowIdReusePolicy = params.workflowIdReusePolicy + if (params.workflowIdConflictPolicy) { + body.workflowIdConflictPolicy = params.workflowIdConflictPolicy + } + if (params.cronSchedule) body.cronSchedule = params.cronSchedule + const executionTimeout = toDurationString(params.executionTimeoutSeconds) + if (executionTimeout) body.workflowExecutionTimeout = executionTimeout + const runTimeout = toDurationString(params.runTimeoutSeconds) + if (runTimeout) body.workflowRunTimeout = runTimeout + const memoFields = parseJsonPayloadMap(params.memo, 'memo') + if (memoFields) body.memo = { fields: memoFields } + const searchAttributeFields = parseJsonPayloadMap(params.searchAttributes, 'searchAttributes') + if (searchAttributeFields) body.searchAttributes = { indexedFields: searchAttributeFields } + return body + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await parseTemporalResponse<{ runId?: string; started?: boolean }>( + response, + 'signal with start' + ) + return { + success: true, + output: { + workflowId: params?.workflowId ?? '', + runId: data.runId ?? '', + started: data.started ?? false, + }, + } + }, + + outputs: { + workflowId: { type: 'string', description: 'Workflow ID of the signaled execution' }, + runId: { type: 'string', description: 'Run ID of the signaled (or newly started) execution' }, + started: { + type: 'boolean', + description: 'Whether this call started a new execution (false when only signaled)', + }, + }, +} diff --git a/apps/sim/tools/temporal/signal_workflow.ts b/apps/sim/tools/temporal/signal_workflow.ts new file mode 100644 index 0000000000..7e27d36bd4 --- /dev/null +++ b/apps/sim/tools/temporal/signal_workflow.ts @@ -0,0 +1,101 @@ +import type { + TemporalSignalWorkflowParams, + TemporalSignalWorkflowResponse, +} from '@/tools/temporal/types' +import { + parseJsonArgs, + parseTemporalResponse, + TEMPORAL_CLIENT_IDENTITY, + temporalRequestHeaders, + temporalWorkflowUrl, + workflowExecutionRef, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const signalWorkflowTool: ToolConfig< + TemporalSignalWorkflowParams, + TemporalSignalWorkflowResponse +> = { + id: 'temporal_signal_workflow', + name: 'Temporal Signal Workflow', + description: 'Send a signal to a running Temporal workflow execution.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + workflowId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Workflow ID of the execution to signal', + }, + runId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Run ID of a specific run to signal (defaults to the latest run)', + }, + signalName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the signal handler to invoke (e.g., approve-order)', + }, + signalInput: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Signal input as JSON. A top-level array is passed as the argument list (one argument per element); any other value is passed as a single argument', + }, + }, + + request: { + url: (params) => + `${temporalWorkflowUrl(params.serverUrl, params.namespace, params.workflowId)}/signal/${encodeURIComponent(params.signalName.trim())}`, + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => { + const body: Record = { + workflowExecution: workflowExecutionRef(params.workflowId, params.runId), + identity: TEMPORAL_CLIENT_IDENTITY, + } + const input = parseJsonArgs(params.signalInput, 'signalInput') + if (input) body.input = input + return body + }, + }, + + transformResponse: async (response: Response, params) => { + await parseTemporalResponse(response, 'signal workflow') + return { + success: true, + output: { + workflowId: params?.workflowId ?? '', + signalName: params?.signalName ?? '', + }, + } + }, + + outputs: { + workflowId: { type: 'string', description: 'Workflow ID of the signaled execution' }, + signalName: { type: 'string', description: 'Name of the signal that was sent' }, + }, +} diff --git a/apps/sim/tools/temporal/start_workflow.ts b/apps/sim/tools/temporal/start_workflow.ts new file mode 100644 index 0000000000..5221e56a58 --- /dev/null +++ b/apps/sim/tools/temporal/start_workflow.ts @@ -0,0 +1,171 @@ +import { generateId } from '@sim/utils/id' +import type { + TemporalStartWorkflowParams, + TemporalStartWorkflowResponse, +} from '@/tools/temporal/types' +import { + parseJsonArgs, + parseJsonPayloadMap, + parseTemporalResponse, + TEMPORAL_CLIENT_IDENTITY, + temporalRequestHeaders, + temporalWorkflowUrl, + toDurationString, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const startWorkflowTool: ToolConfig< + TemporalStartWorkflowParams, + TemporalStartWorkflowResponse +> = { + id: 'temporal_start_workflow', + name: 'Temporal Start Workflow', + description: 'Start a new workflow execution on a Temporal cluster.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + workflowId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Unique workflow ID for the new execution (e.g., order-1234)', + }, + workflowType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Registered workflow type name to run (e.g., OrderWorkflow)', + }, + taskQueue: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Task queue the workflow worker polls (e.g., orders)', + }, + input: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Workflow input as JSON. A top-level array is passed as the argument list (one argument per element); any other value is passed as a single argument', + }, + workflowIdReusePolicy: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Policy for reusing a closed workflow ID: WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, or WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING', + }, + workflowIdConflictPolicy: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Policy when a workflow with the same ID is already running: WORKFLOW_ID_CONFLICT_POLICY_FAIL, WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING, or WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING', + }, + cronSchedule: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cron schedule for recurring executions (e.g., "0 12 * * *")', + }, + executionTimeoutSeconds: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Total workflow execution timeout in seconds, including retries and continue-as-new', + }, + runTimeoutSeconds: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Timeout for a single workflow run in seconds', + }, + memo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of memo fields to attach to the execution', + }, + searchAttributes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of search attribute values to index the execution with', + }, + }, + + request: { + url: (params) => temporalWorkflowUrl(params.serverUrl, params.namespace, params.workflowId), + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => { + const body: Record = { + workflowType: { name: params.workflowType.trim() }, + taskQueue: { name: params.taskQueue.trim() }, + identity: TEMPORAL_CLIENT_IDENTITY, + requestId: generateId(), + } + const input = parseJsonArgs(params.input, 'input') + if (input) body.input = input + if (params.workflowIdReusePolicy) body.workflowIdReusePolicy = params.workflowIdReusePolicy + if (params.workflowIdConflictPolicy) { + body.workflowIdConflictPolicy = params.workflowIdConflictPolicy + } + if (params.cronSchedule) body.cronSchedule = params.cronSchedule + const executionTimeout = toDurationString(params.executionTimeoutSeconds) + if (executionTimeout) body.workflowExecutionTimeout = executionTimeout + const runTimeout = toDurationString(params.runTimeoutSeconds) + if (runTimeout) body.workflowRunTimeout = runTimeout + const memoFields = parseJsonPayloadMap(params.memo, 'memo') + if (memoFields) body.memo = { fields: memoFields } + const searchAttributeFields = parseJsonPayloadMap(params.searchAttributes, 'searchAttributes') + if (searchAttributeFields) body.searchAttributes = { indexedFields: searchAttributeFields } + return body + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await parseTemporalResponse<{ runId?: string; started?: boolean }>( + response, + 'start workflow' + ) + return { + success: true, + output: { + workflowId: params?.workflowId ?? '', + runId: data.runId ?? '', + started: data.started ?? false, + }, + } + }, + + outputs: { + workflowId: { type: 'string', description: 'Workflow ID of the execution' }, + runId: { type: 'string', description: 'Run ID of the started workflow execution' }, + started: { + type: 'boolean', + description: + 'Whether a new execution was started (false when an existing execution was reused)', + }, + }, +} diff --git a/apps/sim/tools/temporal/terminate_workflow.ts b/apps/sim/tools/temporal/terminate_workflow.ts new file mode 100644 index 0000000000..998894c8a1 --- /dev/null +++ b/apps/sim/tools/temporal/terminate_workflow.ts @@ -0,0 +1,91 @@ +import type { + TemporalTerminateWorkflowParams, + TemporalTerminateWorkflowResponse, +} from '@/tools/temporal/types' +import { + parseTemporalResponse, + TEMPORAL_CLIENT_IDENTITY, + temporalRequestHeaders, + temporalWorkflowUrl, + workflowExecutionRef, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const terminateWorkflowTool: ToolConfig< + TemporalTerminateWorkflowParams, + TemporalTerminateWorkflowResponse +> = { + id: 'temporal_terminate_workflow', + name: 'Temporal Terminate Workflow', + description: + 'Forcefully terminate a Temporal workflow execution immediately, without giving the workflow a chance to react.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + workflowId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Workflow ID of the execution to terminate', + }, + runId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Run ID of a specific run to terminate (defaults to the latest run)', + }, + reason: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reason for the termination, recorded in the workflow history', + }, + }, + + request: { + url: (params) => + `${temporalWorkflowUrl(params.serverUrl, params.namespace, params.workflowId)}/terminate`, + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => { + const body: Record = { + workflowExecution: workflowExecutionRef(params.workflowId, params.runId), + identity: TEMPORAL_CLIENT_IDENTITY, + } + if (params.reason) body.reason = params.reason + return body + }, + }, + + transformResponse: async (response: Response, params) => { + await parseTemporalResponse(response, 'terminate workflow') + return { + success: true, + output: { + workflowId: params?.workflowId ?? '', + }, + } + }, + + outputs: { + workflowId: { type: 'string', description: 'Workflow ID of the terminated execution' }, + }, +} diff --git a/apps/sim/tools/temporal/trigger_schedule.ts b/apps/sim/tools/temporal/trigger_schedule.ts new file mode 100644 index 0000000000..a490836897 --- /dev/null +++ b/apps/sim/tools/temporal/trigger_schedule.ts @@ -0,0 +1,84 @@ +import type { + TemporalScheduleMutationResponse, + TemporalTriggerScheduleParams, +} from '@/tools/temporal/types' +import { + parseTemporalResponse, + TEMPORAL_CLIENT_IDENTITY, + temporalRequestHeaders, + temporalScheduleUrl, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const triggerScheduleTool: ToolConfig< + TemporalTriggerScheduleParams, + TemporalScheduleMutationResponse +> = { + id: 'temporal_trigger_schedule', + name: 'Temporal Trigger Schedule', + description: 'Trigger an immediate action of a Temporal schedule, outside its normal spec.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + scheduleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the schedule to trigger', + }, + overlapPolicy: { + type: 'string', + required: false, + visibility: 'user-only', + description: + "Overlap policy for the triggered action (defaults to the schedule's policy): SCHEDULE_OVERLAP_POLICY_SKIP, SCHEDULE_OVERLAP_POLICY_BUFFER_ONE, SCHEDULE_OVERLAP_POLICY_BUFFER_ALL, SCHEDULE_OVERLAP_POLICY_CANCEL_OTHER, SCHEDULE_OVERLAP_POLICY_TERMINATE_OTHER, or SCHEDULE_OVERLAP_POLICY_ALLOW_ALL", + }, + }, + + request: { + url: (params) => + `${temporalScheduleUrl(params.serverUrl, params.namespace, params.scheduleId)}/patch`, + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => { + const triggerImmediately: Record = {} + if (params.overlapPolicy) triggerImmediately.overlapPolicy = params.overlapPolicy + return { + patch: { triggerImmediately }, + identity: TEMPORAL_CLIENT_IDENTITY, + } + }, + }, + + transformResponse: async (response: Response, params) => { + await parseTemporalResponse(response, 'trigger schedule') + return { + success: true, + output: { + scheduleId: params?.scheduleId ?? '', + }, + } + }, + + outputs: { + scheduleId: { type: 'string', description: 'ID of the triggered schedule' }, + }, +} diff --git a/apps/sim/tools/temporal/types.ts b/apps/sim/tools/temporal/types.ts new file mode 100644 index 0000000000..385ff6cd57 --- /dev/null +++ b/apps/sim/tools/temporal/types.ts @@ -0,0 +1,326 @@ +import type { ToolResponse } from '@/tools/types' + +export interface TemporalBaseParams { + serverUrl: string + namespace: string + apiKey?: string +} + +export interface TemporalExecutionSummary { + workflowId: string | null + runId: string | null + workflowType: string | null + status: string | null + startTime: string | null + closeTime: string | null + executionTime: string | null + historyLength: number | null + taskQueue: string | null +} + +export interface TemporalPendingActivity { + activityId: string | null + activityType: string | null + state: string | null + attempt: number | null + lastFailureMessage: string | null +} + +export interface TemporalHistoryEventSummary { + eventId: number | null + eventTime: string | null + eventType: string | null + attributes: Record | null +} + +export interface TemporalStartWorkflowParams extends TemporalBaseParams { + workflowId: string + workflowType: string + taskQueue: string + input?: string + workflowIdReusePolicy?: string + workflowIdConflictPolicy?: string + cronSchedule?: string + executionTimeoutSeconds?: number + runTimeoutSeconds?: number + memo?: string + searchAttributes?: string +} + +export interface TemporalStartWorkflowResponse extends ToolResponse { + output: { + workflowId: string + runId: string + started: boolean + } +} + +export interface TemporalSignalWorkflowParams extends TemporalBaseParams { + workflowId: string + runId?: string + signalName: string + signalInput?: string +} + +export interface TemporalSignalWorkflowResponse extends ToolResponse { + output: { + workflowId: string + signalName: string + } +} + +export interface TemporalSignalWithStartParams extends TemporalBaseParams { + workflowId: string + workflowType: string + taskQueue: string + signalName: string + input?: string + signalInput?: string + workflowIdReusePolicy?: string + workflowIdConflictPolicy?: string + cronSchedule?: string + executionTimeoutSeconds?: number + runTimeoutSeconds?: number + memo?: string + searchAttributes?: string +} + +export interface TemporalSignalWithStartResponse extends ToolResponse { + output: { + workflowId: string + runId: string + started: boolean + } +} + +export interface TemporalQueryWorkflowParams extends TemporalBaseParams { + workflowId: string + runId?: string + queryType: string + queryArgs?: string +} + +export interface TemporalQueryWorkflowResponse extends ToolResponse { + output: { + workflowId: string + queryType: string + result: unknown + } +} + +export interface TemporalDescribeWorkflowParams extends TemporalBaseParams { + workflowId: string + runId?: string +} + +export interface TemporalDescribeWorkflowResponse extends ToolResponse { + output: TemporalExecutionSummary & { + memo: Record | null + searchAttributes: Record | null + pendingActivities: TemporalPendingActivity[] + } +} + +export interface TemporalListWorkflowsParams extends TemporalBaseParams { + query?: string + pageSize?: number + nextPageToken?: string +} + +export interface TemporalListWorkflowsResponse extends ToolResponse { + output: { + executions: TemporalExecutionSummary[] + nextPageToken: string | null + } +} + +export interface TemporalGetWorkflowHistoryParams extends TemporalBaseParams { + workflowId: string + runId?: string + maximumPageSize?: number + nextPageToken?: string + historyEventFilterType?: string +} + +export interface TemporalGetWorkflowHistoryResponse extends ToolResponse { + output: { + events: TemporalHistoryEventSummary[] + nextPageToken: string | null + } +} + +export interface TemporalUpdateWorkflowParams extends TemporalBaseParams { + workflowId: string + runId?: string + updateName: string + updateArgs?: string +} + +export interface TemporalUpdateWorkflowResponse extends ToolResponse { + output: { + workflowId: string + updateName: string + result: unknown + } +} + +export interface TemporalCountWorkflowsParams extends TemporalBaseParams { + query?: string +} + +export interface TemporalCountWorkflowsResponse extends ToolResponse { + output: { + count: number + groups: Array<{ values: unknown[]; count: number }> + } +} + +export interface TemporalResetWorkflowParams extends TemporalBaseParams { + workflowId: string + runId?: string + workflowTaskFinishEventId: number + reason?: string +} + +export interface TemporalResetWorkflowResponse extends ToolResponse { + output: { + workflowId: string + runId: string + } +} + +export interface TemporalScheduleSummary { + scheduleId: string | null + workflowType: string | null + paused: boolean + notes: string | null + futureActionTimes: string[] +} + +export interface TemporalListSchedulesParams extends TemporalBaseParams { + query?: string + maximumPageSize?: number + nextPageToken?: string +} + +export interface TemporalListSchedulesResponse extends ToolResponse { + output: { + schedules: TemporalScheduleSummary[] + nextPageToken: string | null + } +} + +export interface TemporalDescribeScheduleParams extends TemporalBaseParams { + scheduleId: string +} + +export interface TemporalDescribeScheduleResponse extends ToolResponse { + output: { + scheduleId: string + paused: boolean + notes: string | null + workflowType: string | null + taskQueue: string | null + workflowId: string | null + spec: Record | null + recentActions: Array<{ + scheduleTime: string | null + actualTime: string | null + workflowId: string | null + runId: string | null + }> + futureActionTimes: string[] + } +} + +export interface TemporalPatchScheduleParams extends TemporalBaseParams { + scheduleId: string + reason?: string +} + +export interface TemporalTriggerScheduleParams extends TemporalBaseParams { + scheduleId: string + overlapPolicy?: string +} + +export interface TemporalScheduleMutationResponse extends ToolResponse { + output: { + scheduleId: string + } +} + +export interface TemporalCreateScheduleParams extends TemporalBaseParams { + scheduleId: string + workflowId: string + workflowType: string + taskQueue: string + input?: string + cronExpressions?: string + intervalSeconds?: number + timezone?: string + overlapPolicy?: string + notes?: string + paused?: boolean +} + +export interface TemporalDeleteScheduleParams extends TemporalBaseParams { + scheduleId: string +} + +export interface TemporalDescribeTaskQueueParams extends TemporalBaseParams { + taskQueue: string + taskQueueType?: string +} + +export interface TemporalDescribeTaskQueueResponse extends ToolResponse { + output: { + taskQueue: string + pollers: Array<{ + identity: string | null + lastAccessTime: string | null + ratePerSecond: number | null + }> + } +} + +export interface TemporalCancelWorkflowParams extends TemporalBaseParams { + workflowId: string + runId?: string + reason?: string +} + +export interface TemporalCancelWorkflowResponse extends ToolResponse { + output: { + workflowId: string + } +} + +export interface TemporalTerminateWorkflowParams extends TemporalBaseParams { + workflowId: string + runId?: string + reason?: string +} + +export interface TemporalTerminateWorkflowResponse extends ToolResponse { + output: { + workflowId: string + } +} + +export type TemporalResponse = + | TemporalStartWorkflowResponse + | TemporalSignalWorkflowResponse + | TemporalSignalWithStartResponse + | TemporalQueryWorkflowResponse + | TemporalUpdateWorkflowResponse + | TemporalDescribeWorkflowResponse + | TemporalListWorkflowsResponse + | TemporalCountWorkflowsResponse + | TemporalGetWorkflowHistoryResponse + | TemporalCancelWorkflowResponse + | TemporalTerminateWorkflowResponse + | TemporalResetWorkflowResponse + | TemporalListSchedulesResponse + | TemporalDescribeScheduleResponse + | TemporalScheduleMutationResponse + | TemporalDescribeTaskQueueResponse diff --git a/apps/sim/tools/temporal/unpause_schedule.ts b/apps/sim/tools/temporal/unpause_schedule.ts new file mode 100644 index 0000000000..8749dae54c --- /dev/null +++ b/apps/sim/tools/temporal/unpause_schedule.ts @@ -0,0 +1,79 @@ +import type { + TemporalPatchScheduleParams, + TemporalScheduleMutationResponse, +} from '@/tools/temporal/types' +import { + parseTemporalResponse, + TEMPORAL_CLIENT_IDENTITY, + temporalRequestHeaders, + temporalScheduleUrl, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const unpauseScheduleTool: ToolConfig< + TemporalPatchScheduleParams, + TemporalScheduleMutationResponse +> = { + id: 'temporal_unpause_schedule', + name: 'Temporal Unpause Schedule', + description: 'Unpause a Temporal schedule so it resumes taking actions.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + scheduleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the schedule to unpause', + }, + reason: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "Reason recorded in the schedule's notes", + }, + }, + + request: { + url: (params) => + `${temporalScheduleUrl(params.serverUrl, params.namespace, params.scheduleId)}/patch`, + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => ({ + patch: { unpause: params.reason || 'Unpaused via Sim' }, + identity: TEMPORAL_CLIENT_IDENTITY, + }), + }, + + transformResponse: async (response: Response, params) => { + await parseTemporalResponse(response, 'unpause schedule') + return { + success: true, + output: { + scheduleId: params?.scheduleId ?? '', + }, + } + }, + + outputs: { + scheduleId: { type: 'string', description: 'ID of the unpaused schedule' }, + }, +} diff --git a/apps/sim/tools/temporal/update_workflow.ts b/apps/sim/tools/temporal/update_workflow.ts new file mode 100644 index 0000000000..e2429f5e95 --- /dev/null +++ b/apps/sim/tools/temporal/update_workflow.ts @@ -0,0 +1,137 @@ +import { generateId } from '@sim/utils/id' +import type { + TemporalUpdateWorkflowParams, + TemporalUpdateWorkflowResponse, +} from '@/tools/temporal/types' +import { + decodePayloads, + parseJsonArgs, + parseTemporalResponse, + stripEnumPrefix, + TEMPORAL_CLIENT_IDENTITY, + type TemporalPayloads, + temporalRequestHeaders, + temporalWorkflowUrl, + workflowExecutionRef, +} from '@/tools/temporal/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateWorkflowTool: ToolConfig< + TemporalUpdateWorkflowParams, + TemporalUpdateWorkflowResponse +> = { + id: 'temporal_update_workflow', + name: 'Temporal Update Workflow', + description: + 'Invoke an update handler on a running Temporal workflow and wait for its result. Unlike a signal, an update is validated by the workflow and returns a response.', + version: '1.0.0', + + params: { + serverUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: "Base URL of the Temporal server's HTTP API (e.g., http://localhost:7243)", + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Temporal namespace (e.g., default)', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'API key sent as a Bearer token (leave blank for servers without auth)', + }, + workflowId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Workflow ID of the execution to update', + }, + runId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Run ID of a specific run to update (defaults to the latest run)', + }, + updateName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the update handler to invoke (e.g., addItem)', + }, + updateArgs: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Update arguments as JSON. A top-level array is passed as the argument list (one argument per element); any other value is passed as a single argument', + }, + }, + + request: { + url: (params) => + `${temporalWorkflowUrl(params.serverUrl, params.namespace, params.workflowId)}/update/${encodeURIComponent(params.updateName.trim())}`, + method: 'POST', + headers: (params) => temporalRequestHeaders(params), + body: (params) => { + const input: Record = { name: params.updateName.trim() } + const args = parseJsonArgs(params.updateArgs, 'updateArgs') + if (args) input.args = args + return { + workflowExecution: workflowExecutionRef(params.workflowId, params.runId), + waitPolicy: { lifecycleStage: 'UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED' }, + request: { + meta: { updateId: generateId(), identity: TEMPORAL_CLIENT_IDENTITY }, + input, + }, + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await parseTemporalResponse<{ + stage?: string + outcome?: { + success?: TemporalPayloads + failure?: { message?: string } + } + }>(response, 'update workflow') + + if (data.outcome?.failure) { + throw new Error( + `Temporal update workflow failed: ${data.outcome.failure.message ?? 'update handler returned a failure'}` + ) + } + + if (!data.outcome) { + const stage = stripEnumPrefix(data.stage, 'UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_') + throw new Error( + `Temporal update workflow did not complete before the request timed out (stage: ${stage ?? 'unknown'}). The update is still being processed by the workflow.` + ) + } + + const decoded = decodePayloads(data.outcome?.success) + return { + success: true, + output: { + workflowId: params?.workflowId ?? '', + updateName: params?.updateName ?? '', + result: decoded.length > 1 ? decoded : (decoded[0] ?? null), + }, + } + }, + + outputs: { + workflowId: { type: 'string', description: 'Workflow ID of the updated execution' }, + updateName: { type: 'string', description: 'Name of the update that was invoked' }, + result: { + type: 'json', + description: + 'Decoded update result. A single payload is returned as its JSON value; multiple payloads are returned as an array', + }, + }, +} diff --git a/apps/sim/tools/temporal/utils.ts b/apps/sim/tools/temporal/utils.ts new file mode 100644 index 0000000000..6cd0c511de --- /dev/null +++ b/apps/sim/tools/temporal/utils.ts @@ -0,0 +1,277 @@ +import { truncate } from '@sim/utils/string' + +/** + * Identity reported to the Temporal server on write operations so they are + * attributable in workflow histories. + */ +export const TEMPORAL_CLIENT_IDENTITY = 'sim' + +const JSON_PLAIN_ENCODING = Buffer.from('json/plain').toString('base64') + +/** A Temporal `common.v1.Payload` as serialized by the HTTP API (base64 fields). */ +export interface TemporalPayload { + metadata?: Record + data?: string +} + +/** A Temporal `common.v1.Payloads` collection. */ +export interface TemporalPayloads { + payloads?: TemporalPayload[] +} + +/** Raw `common.v1.WorkflowExecution` shape returned by the HTTP API. */ +export interface TemporalRawExecution { + workflowId?: string + runId?: string +} + +/** Raw `workflow.v1.WorkflowExecutionInfo` shape returned by describe/list responses. */ +export interface TemporalRawExecutionInfo { + execution?: TemporalRawExecution + type?: { name?: string } + status?: string + startTime?: string + closeTime?: string + executionTime?: string + historyLength?: string + taskQueue?: string +} + +/** Raw `history.v1.HistoryEvent` shape returned by the workflow history endpoint. */ +export interface TemporalRawHistoryEvent { + eventId?: string + eventTime?: string + eventType?: string + [key: string]: unknown +} + +/** + * Builds the `/api/v1/namespaces/{namespace}` base URL for a Temporal server's HTTP API, + * tolerating surrounding whitespace and trailing slashes on the server URL + * (e.g. `http://localhost:7243/` → `http://localhost:7243/api/v1/namespaces/default`). + */ +export function temporalNamespaceUrl(serverUrl: string, namespace: string): string { + const base = serverUrl.trim().replace(/\/+$/, '') + return `${base}/api/v1/namespaces/${encodeURIComponent(namespace.trim())}` +} + +/** + * Builds the `/workflows/{workflowId}` URL for a workflow execution, trimming and + * URL-encoding the workflow ID. + */ +export function temporalWorkflowUrl( + serverUrl: string, + namespace: string, + workflowId: string +): string { + return `${temporalNamespaceUrl(serverUrl, namespace)}/workflows/${encodeURIComponent(workflowId.trim())}` +} + +/** + * Builds the `/schedules/{scheduleId}` URL for a schedule, trimming and URL-encoding + * the schedule ID. + */ +export function temporalScheduleUrl( + serverUrl: string, + namespace: string, + scheduleId: string +): string { + return `${temporalNamespaceUrl(serverUrl, namespace)}/schedules/${encodeURIComponent(scheduleId.trim())}` +} + +/** + * Builds a `common.v1.WorkflowExecution` reference with trimmed IDs, omitting the run ID + * when not provided so the server targets the latest run. + */ +export function workflowExecutionRef(workflowId: string, runId?: string): Record { + const ref: Record = { workflowId: workflowId.trim() } + if (runId?.trim()) ref.runId = runId.trim() + return ref +} + +/** + * Builds the request headers for a Temporal HTTP API call, attaching the API key as a + * Bearer token when one is provided (omitted for servers without authentication). + * + * The Accept header opts out of the server's payload "shorthand" JSON form so responses + * always carry full `{metadata, data}` payload objects that {@link decodePayload} understands. + */ +export function temporalRequestHeaders(params: { apiKey?: string }): Record { + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json+no-payload-shorthand', + } + if (params.apiKey) headers.Authorization = `Bearer ${params.apiKey.trim()}` + return headers +} + +/** Encodes a single JSON value as a Temporal `json/plain` payload. */ +export function encodePayload(value: unknown): TemporalPayload { + return { + metadata: { encoding: JSON_PLAIN_ENCODING }, + data: Buffer.from(JSON.stringify(value)).toString('base64'), + } +} + +/** + * Normalizes a JSON field value: strings are parsed as JSON, already-resolved objects + * and arrays are used as-is, and empty input returns undefined so the field is omitted. + */ +function parseJsonValue(value: unknown, fieldName: string): unknown { + if (value == null) return undefined + if (typeof value === 'string') { + if (!value.trim()) return undefined + try { + return JSON.parse(value) + } catch { + throw new Error(`Invalid JSON in ${fieldName}`) + } + } + return value +} + +/** + * Parses a JSON value into Temporal `Payloads`. A top-level array is treated as the + * argument list (one payload per element); any other value becomes a single argument. + * Returns undefined for empty input so optional payload fields can be omitted entirely. + */ +export function parseJsonArgs(value: unknown, fieldName: string): TemporalPayloads | undefined { + const parsed = parseJsonValue(value, fieldName) + if (parsed === undefined) return undefined + const args = Array.isArray(parsed) ? parsed : [parsed] + return { payloads: args.map(encodePayload) } +} + +/** + * Parses a JSON object value into a `map` (memo fields or search + * attribute indexed fields). Returns undefined for empty input. + */ +export function parseJsonPayloadMap( + value: unknown, + fieldName: string +): Record | undefined { + const parsed = parseJsonValue(value, fieldName) + if (parsed === undefined) return undefined + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error(`${fieldName} must be a JSON object`) + } + return Object.fromEntries( + Object.entries(parsed as Record).map(([key, value]) => [ + key, + encodePayload(value), + ]) + ) +} + +/** + * Decodes a single Temporal payload: `json/plain` and `json/protobuf` payloads are parsed + * to their JSON value, `binary/null` becomes null, and unknown encodings are returned as + * the original base64 data string. + */ +export function decodePayload(payload: TemporalPayload | undefined): unknown { + if (!payload) return null + const encoding = payload.metadata?.encoding + ? Buffer.from(payload.metadata.encoding, 'base64').toString('utf8') + : undefined + if (encoding === 'binary/null') return null + if (payload.data == null) return null + if (encoding === 'json/plain' || encoding === 'json/protobuf') { + const raw = Buffer.from(payload.data, 'base64').toString('utf8') + try { + return JSON.parse(raw) + } catch { + return raw + } + } + return payload.data +} + +/** Decodes a Temporal `Payloads` collection into an array of JSON values. */ +export function decodePayloads(payloads: TemporalPayloads | undefined): unknown[] { + return (payloads?.payloads ?? []).map(decodePayload) +} + +/** Decodes a `map` (memo / search attributes) into a plain object. */ +export function decodePayloadMap( + fields: Record | undefined +): Record | null { + if (!fields) return null + return Object.fromEntries( + Object.entries(fields).map(([key, value]) => [key, decodePayload(value)]) + ) +} + +/** Strips a protobuf enum prefix (e.g. `WORKFLOW_EXECUTION_STATUS_RUNNING` → `RUNNING`). */ +export function stripEnumPrefix(value: string | undefined, prefix: string): string | null { + if (!value) return null + return value.startsWith(prefix) ? value.slice(prefix.length) : value +} + +/** + * Formats a seconds count as a protobuf JSON duration string (e.g. 3600 → `"3600s"`). + * Returns undefined for missing, non-numeric, or non-positive values so the field is omitted. + */ +export function toDurationString(seconds: number | string | undefined): string | undefined { + if (seconds == null || seconds === '') return undefined + const parsed = Number(seconds) + if (!Number.isFinite(parsed) || parsed <= 0) return undefined + return `${parsed}s` +} + +/** + * Maps a raw `WorkflowExecutionInfo` to the flat execution summary shared by the + * describe and list tools. int64 fields arrive as JSON strings and are coerced to numbers. + */ +export function mapExecutionInfo(info: TemporalRawExecutionInfo | undefined) { + return { + workflowId: info?.execution?.workflowId ?? null, + runId: info?.execution?.runId ?? null, + workflowType: info?.type?.name ?? null, + status: stripEnumPrefix(info?.status, 'WORKFLOW_EXECUTION_STATUS_'), + startTime: info?.startTime ?? null, + closeTime: info?.closeTime ?? null, + executionTime: info?.executionTime ?? null, + historyLength: info?.historyLength != null ? Number(info.historyLength) : null, + taskQueue: info?.taskQueue ?? null, + } +} + +/** + * Maps a raw history event to a flat shape, extracting the event's `*EventAttributes` + * object (each event carries exactly one, keyed by its type). + */ +export function mapHistoryEvent(event: TemporalRawHistoryEvent) { + const attributesKey = Object.keys(event).find((key) => key.endsWith('EventAttributes')) + return { + eventId: event.eventId != null ? Number(event.eventId) : null, + eventTime: event.eventTime ?? null, + eventType: stripEnumPrefix(event.eventType, 'EVENT_TYPE_'), + attributes: attributesKey ? ((event[attributesKey] as Record) ?? null) : null, + } +} + +/** + * Parses a Temporal HTTP API response body and throws a descriptive error for non-2xx + * replies. grpc-gateway errors carry a top-level `message` field; empty bodies (returned + * by signal/cancel/terminate) parse to an empty object. + */ +export async function parseTemporalResponse( + response: Response, + operation: string +): Promise { + const text = await response.text() + let data: Record = {} + if (text) { + try { + data = JSON.parse(text) as Record + } catch { + data = { message: truncate(text, 300) } + } + } + if (!response.ok) { + const message = + typeof data.message === 'string' && data.message ? data.message : `HTTP ${response.status}` + throw new Error(`Temporal ${operation} failed: ${message}`) + } + return data as T +} From ef53b1bfb75371e9c20680c9711c86ee06e5202e Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 12:46:08 -0700 Subject: [PATCH 2/2] improvement(temporal): send requestId on all dedupe-capable write operations --- apps/sim/tools/temporal/cancel_workflow.ts | 2 ++ apps/sim/tools/temporal/pause_schedule.ts | 2 ++ apps/sim/tools/temporal/signal_workflow.ts | 2 ++ apps/sim/tools/temporal/trigger_schedule.ts | 2 ++ apps/sim/tools/temporal/unpause_schedule.ts | 2 ++ 5 files changed, 10 insertions(+) diff --git a/apps/sim/tools/temporal/cancel_workflow.ts b/apps/sim/tools/temporal/cancel_workflow.ts index 5421f75a69..ad0655cd7b 100644 --- a/apps/sim/tools/temporal/cancel_workflow.ts +++ b/apps/sim/tools/temporal/cancel_workflow.ts @@ -1,3 +1,4 @@ +import { generateId } from '@sim/utils/id' import type { TemporalCancelWorkflowParams, TemporalCancelWorkflowResponse, @@ -69,6 +70,7 @@ export const cancelWorkflowTool: ToolConfig< const body: Record = { workflowExecution: workflowExecutionRef(params.workflowId, params.runId), identity: TEMPORAL_CLIENT_IDENTITY, + requestId: generateId(), } if (params.reason) body.reason = params.reason return body diff --git a/apps/sim/tools/temporal/pause_schedule.ts b/apps/sim/tools/temporal/pause_schedule.ts index 56e517be29..2368b54001 100644 --- a/apps/sim/tools/temporal/pause_schedule.ts +++ b/apps/sim/tools/temporal/pause_schedule.ts @@ -1,3 +1,4 @@ +import { generateId } from '@sim/utils/id' import type { TemporalPatchScheduleParams, TemporalScheduleMutationResponse, @@ -60,6 +61,7 @@ export const pauseScheduleTool: ToolConfig< body: (params) => ({ patch: { pause: params.reason || 'Paused via Sim' }, identity: TEMPORAL_CLIENT_IDENTITY, + requestId: generateId(), }), }, diff --git a/apps/sim/tools/temporal/signal_workflow.ts b/apps/sim/tools/temporal/signal_workflow.ts index 7e27d36bd4..a09b87c594 100644 --- a/apps/sim/tools/temporal/signal_workflow.ts +++ b/apps/sim/tools/temporal/signal_workflow.ts @@ -1,3 +1,4 @@ +import { generateId } from '@sim/utils/id' import type { TemporalSignalWorkflowParams, TemporalSignalWorkflowResponse, @@ -76,6 +77,7 @@ export const signalWorkflowTool: ToolConfig< const body: Record = { workflowExecution: workflowExecutionRef(params.workflowId, params.runId), identity: TEMPORAL_CLIENT_IDENTITY, + requestId: generateId(), } const input = parseJsonArgs(params.signalInput, 'signalInput') if (input) body.input = input diff --git a/apps/sim/tools/temporal/trigger_schedule.ts b/apps/sim/tools/temporal/trigger_schedule.ts index a490836897..39d9b1102e 100644 --- a/apps/sim/tools/temporal/trigger_schedule.ts +++ b/apps/sim/tools/temporal/trigger_schedule.ts @@ -1,3 +1,4 @@ +import { generateId } from '@sim/utils/id' import type { TemporalScheduleMutationResponse, TemporalTriggerScheduleParams, @@ -64,6 +65,7 @@ export const triggerScheduleTool: ToolConfig< return { patch: { triggerImmediately }, identity: TEMPORAL_CLIENT_IDENTITY, + requestId: generateId(), } }, }, diff --git a/apps/sim/tools/temporal/unpause_schedule.ts b/apps/sim/tools/temporal/unpause_schedule.ts index 8749dae54c..4322c065be 100644 --- a/apps/sim/tools/temporal/unpause_schedule.ts +++ b/apps/sim/tools/temporal/unpause_schedule.ts @@ -1,3 +1,4 @@ +import { generateId } from '@sim/utils/id' import type { TemporalPatchScheduleParams, TemporalScheduleMutationResponse, @@ -60,6 +61,7 @@ export const unpauseScheduleTool: ToolConfig< body: (params) => ({ patch: { unpause: params.reason || 'Unpaused via Sim' }, identity: TEMPORAL_CLIENT_IDENTITY, + requestId: generateId(), }), },