Workflows
Run multi-step AI agent pipelines automatically — trigger workflows from webhooks, schedule them with cron, route tasks conditionally, and recover from crashes. Orchestrate your agent swarm with DAG-based automation.
Agent Swarm includes a workflow automation engine that lets you define multi-step processes as directed acyclic graphs (DAGs). Workflows connect triggers, conditions, and actions into pipelines that execute automatically.
Bundled workflows declare the parameters and integrations they require. If one is enabled before setup is complete, it reports needs_setup with exactly what is missing in the dashboard and refuses to trigger until setup is complete. Operator-created workflows remain unaffected unless they opt into the same params, requiredParams, and requires metadata.
Core Concepts
A workflow consists of:
- Nodes — Individual steps with a
nextfield that defines routing (no explicit edges needed) - Executors — Class-based step implementations with Zod-typed config and output schemas
- Triggers — Entry points: webhook (HMAC-SHA256, timestamped HMAC, shared token), schedule (ref), or manual
- Checkpoint durability — Atomic DB write after every step; resume from last checkpoint on crash
- Fan-out / convergence — Parallel execution via array
next, or item-driven agent-task fan-out viaforeach, with automatic convergence gating
Executor Types
The engine includes 12 built-in executor types:
| Executor | Description |
|---|---|
script | Run a shell script or JavaScript expression |
swarm-script | Run a reusable TypeScript script from the swarm catalog with optional args, scope, pinHash, and a configurable timeoutMs wall-clock budget |
agent-task | Create and delegate a task to a swarm agent |
foreach | Fan out one agent task per array item, wait for every child, and emit one aggregate result |
raw-llm | Call an LLM directly with a prompt template |
vcs | Version control operations (create PR, merge, etc.) |
property-match | Route based on matching properties in step input |
code-match | Route based on a custom JavaScript expression in a sandbox |
notify | Send a notification (Slack, email, or internal message) |
validate | Validate step output; can halt or trigger retry |
human-in-the-loop | Pause workflow for human approval or input via the dashboard |
wait | Pause for a fixed duration (mode: "time") or until a workflow event matching an optional filter (mode: "event"); event mode supports run / global scope and an optional timeoutMs that routes to the timeout port |
Each executor has typed config and output schemas available via GET /api/executor-types (returns JSON Schemas via Zod v4 z.toJSONSchema()).
swarm-script is the durable, reusable counterpart to inline script nodes. In workflow definitions it accepts scriptName, optional scope (agent or global), optional pinHash, JSON args, and timeoutMs. The timeout defaults to 30000 ms, accepts integers from 1000 through 300000 (5 minutes), and raises the script runtime's wall-clock budget for I/O-bound work while still respecting the separate 60-second CPU-time ceiling.
For config.args, values written as an exact single token such as {{input.payload}} preserve the resolved JSON type, so objects, arrays, numbers, and booleans reach the script as native values. If you wrap a token with surrounding text, such as "prefix-{{input.id}}", interpolation falls back to string output. Raw-token resolution is scoped to the node's local interpolation context, including any explicit inputs mapping on that node.
Foreach fan-out
A foreach node resolves config.over to an array, creates one agent-task child for each item, waits for all children to finish, and exposes an aggregate result to its successor. Use an exact interpolation token for over so the value remains an array; itemKey must identify a unique, non-empty string property on every item.
- id: review-by-agent
type: foreach
inputs: { agents: "discover.result.agents" }
config:
over: "{{agents}}"
itemKey: id
body:
type: agent-task
config:
agentId: "{{item.id}}"
template: "Review the change as {{item.name}} (index {{index}})"
next: summarizeIn v1, body.type is limited to agent-task, all children fan out together, and concurrency is rejected. Child steps use synthetic IDs such as review-by-agent#agent-id. The default onNodeFailure: "fail" stops the run on a failed child; "continue" lets the remaining children finish and records failed entries in the aggregate alongside okCount and failedCount.
Defining a Workflow
Workflows use a nodes-with-next schema. Each node declares its next field for routing — edges are auto-generated for the UI graph visualization.
The next field supports three formats:
| Format | Example | Description |
|---|---|---|
string | "next-node" | Simple chaining to one node |
string[] | ["node-a", "node-b"] | Fan-out to multiple parallel nodes |
Record<string, string> | { "pass": "ok-node", "fail": "err-node" } | Port-based conditional routing |
{
"name": "pr-review-pipeline",
"description": "Auto-assign PR reviews on webhook",
"triggers": [
{ "type": "webhook", "hmacSecret": "secret.GITHUB_WEBHOOK_SECRET" }
],
"definition": {
"nodes": [
{
"id": "check-type",
"type": "property-match",
"label": "Check Event Type",
"config": { "property": "event", "match": "pull_request.opened" },
"next": { "match": "create-review", "no_match": null }
},
{
"id": "create-review",
"type": "agent-task",
"label": "Create Review Task",
"config": {
"taskTemplate": "Review PR #{{input.number}}: {{input.title}}",
"tags": ["review", "automated"],
"priority": 60
}
}
]
}
}Triggers
| Type | Description |
|---|---|
webhook | HTTP POST to /api/workflows/:id/trigger with optional verification |
schedule | References a schedule by ID — the schedule creates workflow runs at configured times |
manual | Triggered via the trigger-workflow MCP tool or dashboard UI |
event | Starts from a named internal event; slack.message is currently supported |
An event trigger uses { "type": "event", "eventName": "slack.message" }. The Slack message payload becomes the workflow's triggerData and contains channel, text, user, ts, and threadTs; any configured triggerSchema is applied before a run starts.
Workflow runs preserve the human who actually triggered them in createdBy. Authenticated HTTP and MCP triggers use the trusted caller context, schedules use their creator when present, and Kapso routing uses its resolved sender. Generic ownerless webhooks and creatorless schedules remain unattributed rather than inheriting the workflow author's identity. Retry, resume, and recovery paths rehydrate this requester from the run so agent tasks created later keep the same attribution.
Webhook verification formats
Webhook triggers verify signatures only when hmacSecret is set. The secret can be a literal, an environment reference such as ${WEBHOOK_SECRET}, or a swarm secret reference such as secret.SUPERAGENT_WEBHOOK_SECRET.
Omitting verification keeps the legacy behavior: HMAC-SHA256 over the raw request body, accepting either sha256=<hex> or bare hex. The verifier checks hmacHeader first and then the legacy fallback headers.
{
"type": "webhook",
"hmacSecret": "secret.GITHUB_WEBHOOK_SECRET",
"hmacHeader": "X-Hub-Signature-256"
}Set verification.format to make the expected format explicit. Explicit formats read only the configured header.
{
"type": "webhook",
"hmacSecret": "secret.GITHUB_WEBHOOK_SECRET",
"verification": {
"format": "hmac-sha256",
"header": "X-Hub-Signature-256"
}
}Use timestamped-hmac-sha256 for Stripe/Superagent-style headers such as t=<timestamp>,v1=<hex>. The signed payload is <timestamp>.<raw body>, multiple v1 entries are accepted for secret rotation, and timestamps outside the tolerance window are rejected.
{
"type": "webhook",
"hmacSecret": "secret.SUPERAGENT_WEBHOOK_SECRET",
"verification": {
"format": "timestamped-hmac-sha256",
"header": "X-Superagent-Signature",
"timestampKey": "t",
"signatureKey": "v1",
"toleranceSeconds": 300
}
}Use token-equality for shared-token providers such as GitLab. The configured hmacSecret stores the expected token and the request header must match it.
{
"type": "webhook",
"hmacSecret": "secret.GITLAB_WEBHOOK_TOKEN",
"verification": {
"format": "token-equality",
"header": "X-Gitlab-Token"
}
}Cooldown
Workflows support a cooldown period to prevent rapid re-triggering:
{
"cooldown": { "hours": 0, "minutes": 5, "seconds": 0 }
}A second trigger within the cooldown window results in a run with skipped status.
Trigger payload validation (triggerSchema)
Workflows can attach an optional JSON Schema to validate the triggerData payload across every trigger path — manual /trigger, webhooks, schedules, and the trigger-workflow MCP tool. When set, mismatched payloads are rejected with HTTP 400 (or an MCP error) before the workflow starts: no run is created, no nodes execute. When unset, any payload is accepted (current default).
Set or update via create-workflow / update-workflow / patch-workflow (MCP) or POST / PUT / PATCH /api/workflows/{id} (HTTP). The PUT / PATCH paths accept null to clear an existing schema.
The validator supports a deliberate JSON-Schema subset: type, required, properties, enum, const, items. Other keywords (oneOf, anyOf, $ref, pattern, format, additionalProperties, …) are silently ignored. On validation failure, the response echoes the workflow's current triggerSchema so callers can self-correct without a follow-up get-workflow.
See runbooks/workflows.md § Trigger schema for the full reference and authoring examples.
Execution Model
When a workflow triggers:
- The trigger source (webhook, schedule, or manual) creates a new workflow run
- The engine's
walkGraph()finds ready nodes and executes them in parallel - Each node's executor runs and produces output that determines port-based routing via
next - Checkpoint durability — after every step, an atomic DB write saves the step result and execution context
- On crash or restart, execution resumes from the last checkpoint
Fan-Out and Convergence
Workflows support fan-out by specifying an array of node IDs in the next field. All target nodes execute in parallel, and downstream convergence nodes automatically wait for all predecessors to complete before executing.
{
"definition": {
"nodes": [
{
"id": "start",
"type": "agent-task",
"next": ["task-a", "task-b", "task-c"]
},
{ "id": "task-a", "type": "agent-task", "next": "merge" },
{ "id": "task-b", "type": "agent-task", "next": "merge" },
{ "id": "task-c", "type": "agent-task", "next": "merge" },
{
"id": "merge",
"type": "agent-task",
"inputs": { "a": "task-a", "b": "task-b", "c": "task-c" },
"config": { "template": "Combine results from {{a.taskOutput}}, {{b.taskOutput}}, {{c.taskOutput}}" }
}
]
}
}The engine detects convergence nodes (nodes with multiple predecessors) and gates execution until all predecessors complete. This works correctly with async executors like agent-task — the resume logic uses convergence-aware node detection.
Node Failure Behavior (onNodeFailure)
By default, if any node's task fails or is cancelled, the entire workflow run is marked as failed. You can change this with the onNodeFailure field on the workflow definition:
{
"definition": {
"onNodeFailure": "continue",
"nodes": [...]
}
}| Value | Behavior |
|---|---|
"fail" (default) | Mark the entire run as failed immediately |
"continue" | Treat the failed node as completed with error output and proceed — downstream convergence nodes receive [FAILED: reason] and can handle partial results |
This is useful for fan-out patterns where some branches may fail but you still want to collect results from the successful ones.
Per-Step Retry
Each node can define a retryPolicy:
{
"retryPolicy": {
"maxRetries": 3,
"backoff": "exponential",
"initialDelayMs": 1000
}
}Backoff strategies: exponential, linear, static. The poller detects failed steps and re-executes them according to the policy. Before a retry, the engine restores completed upstream outputs from step checkpoints, so declared inputs resolve to the same values as the original execution. Retrying a failed run also reconstructs the branch selected by each completed step and never revives a branch that was not taken.
Per-Node Timeout
Timeouts live in each executor's config so the executor and the workflow watchdog use the same budget. For a swarm-script node:
{
"id": "run-report",
"type": "swarm-script",
"config": {
"scriptName": "build-report",
"timeoutMs": 300000
}
}For swarm-script nodes, set config.timeoutMs to raise or lower both the workflow watchdog and the spawned script's wall-clock budget within the allowed 1000-300000 ms range. Create, update, bulk-patch, and single-node patch operations reject an out-of-range executor config before saving the workflow.
Inline script nodes use config.timeout instead. That value is the wall-clock budget for both the inline executor and the workflow step watchdog, so a long-running script is not stopped by the default 30-second watchdog before its configured timeout. It accepts 1000 through 300000 ms and defaults to 30000 ms.
For orchestration that needs more than 5 minutes, launch a durable one-off script workflow with launch-script-run and split the operation into bounded, journaled ctx.step.swarmScript (or other durable) steps. Durable runs resume across process restarts, but an individual script step still uses its own bounded scripts-runtime window.
Cancelling a Workflow Run
Running or waiting workflow runs can be cancelled using the cancel-workflow-run MCP tool. Cancellation terminates all non-terminal steps and their associated tasks. An optional reason can be provided for audit purposes.
Per-Step Validation
The validate executor runs after a step to check output quality. It can:
- Pass — continue to the next step
- Halt — stop the workflow run
- Retry — re-execute the previous step
Cross-Node Data Access (inputs Mapping)
By default, upstream step outputs are not available for interpolation. Built-in trigger, input, workflow, swarm, and run context remains available for ordinary config values. To access another node's output, declare an inputs mapping on the node:
{
"id": "summarize",
"type": "agent-task",
"inputs": { "cityData": "generate-city" },
"config": {
"template": "The city is {{cityData.taskOutput.city}} in {{cityData.taskOutput.country}}"
}
}- Keys are local names used in
{{interpolation}}, values are context paths (usually a node ID) - Agent-task output shape is
{ taskId, taskOutput }— access fields vialocalName.taskOutput.field - For trigger data:
{ "pr": "trigger.pullRequest" }→{{pr.number}} - Without
inputs, ordinary templates referencing upstream nodes resolve to empty strings and report the token in run diagnostics
Executable source has a stricter boundary. Inline script source may interpolate only input, workflow, swarm, and run; pass trigger or upstream values through config.args so they arrive as argv instead of executable text. Named swarm-script source is never workflow-interpolated, so pass all dynamic values through its args object. Disallowed or unresolved source tokens fail the node before execution, while unrelated mustache strings remain literal data.
Structured Output (outputSchema)
Agent-task nodes can require structured JSON output from the agent via config.outputSchema:
{
"id": "generate-city",
"type": "agent-task",
"config": {
"template": "Pick a random city and return it as JSON",
"outputSchema": {
"type": "object",
"required": ["city", "country"],
"properties": {
"city": { "type": "string" },
"country": { "type": "string" }
}
}
}
}There are two different outputSchema locations — they validate at different layers:
| Location | Validates |
|---|---|
config.outputSchema (inside config) | The agent's raw JSON response |
Node-level outputSchema (sibling of config) | The executor's return envelope (rarely needed) |
For agent-task nodes, put your schema in config.outputSchema. The agent is prompted to produce JSON matching this schema, and store-progress validates it inline.
Workspace Scoping
Workspace scoping can be set at two levels:
Workflow level — Set dir and vcsRepo on the workflow itself. All agent-task nodes that don't explicitly set these fields inherit the workflow-level defaults. These values are also available for interpolation:
{
"name": "my-workflow",
"dir": "/workspace/myrepo",
"vcsRepo": "https://github.com/org/repo",
"definition": {
"nodes": [
{
"id": "build",
"type": "agent-task",
"config": {
"template": "Build and test in {{workflow.dir}}"
}
}
]
}
}Node level — Set config.dir or config.vcsRepo on individual agent-task nodes. Node-level settings override workflow-level defaults.
Interpolation sources available in node config:
{{workflow.dir}}— Workflow-level dir{{workflow.vcsRepo}}— Workflow-level vcsRepo{{trigger.*}}— Trigger payload data{{input.*}}— Workflow-level resolved inputs
Version History
Every workflow update creates a snapshot in the version history table, recording the previous definition. This provides an audit trail and enables rollback.
MCP Tools
| Tool | Description |
|---|---|
create-workflow | Create a new workflow with a DAG definition |
get-workflow | Get workflow details by ID |
list-workflows | List all workflows (optionally filter by enabled status) |
update-workflow | Update a workflow's definition, name, or enabled status |
patch-workflow | Partially update a workflow — create, update, or delete individual nodes with automatic version snapshots |
patch-workflow-node | Partially update a single node in a workflow with automatic version snapshots |
delete-workflow | Delete a workflow and its run history |
trigger-workflow | Manually trigger a workflow execution |
get-workflow-run | Get details of a specific workflow run |
list-workflow-runs | List paginated slim run rows (20 by default, 100 max); use includeContext: true only when a page needs full contexts |
retry-workflow-run | Retry a failed workflow run |
cancel-workflow-run | Cancel a running or waiting workflow run |
list-workflow-runs returns bounded rows by default: run identifiers, status,
timestamps, error, and a trigger-data summary capped at 400 characters. It
omits full context and trigger data to keep routine history queries small.
Follow page.hasMore / page.nextOffset for pagination, call
get-workflow-run to retrieve one run with its steps and context, or pass
includeContext: true for an explicitly full page.
Dashboard UI
The dashboard includes a Workflows section (under Operations) with:
- Workflows list — View all workflows with enable/disable status
- Workflow detail — Tabbed view with Definition (node inspector showing config/input/output schemas) and Runs tabs
- Run detail — Split layout with interactive graph and collapsible steps panel, JsonTree viewer, bidirectional node selection, expand/collapse all, millisecond duration display
Human-in-the-Loop (HITL) Node
The human-in-the-loop executor pauses a workflow until a human responds via the dashboard. This enables approval gates, manual input steps, and review checkpoints within automated pipelines.
{
"id": "approve-deploy",
"type": "human-in-the-loop",
"label": "Approve Deployment",
"config": {
"title": "Deploy to production?",
"questions": [
{ "type": "approval", "label": "Approve this deployment?" },
{ "type": "text", "label": "Any notes?" }
]
},
"next": { "approved": "deploy", "rejected": "notify-rejected" }
}Question types supported: approval (yes/no), text, single-select, multi-select, and boolean.
When the node executes, it creates an approval request accessible at /approval-requests/{id} in the dashboard. The workflow run pauses until a human responds. Upon resolution, a follow-up task is created for the requesting agent and the workflow resumes via the appropriate next port. Cancelling the run or its human-in-the-loop step marks the pending request as cancelled, rejects later responses, and updates any linked Slack approval thread so reviewers do not act on a stale gate.
The dashboard validates every required response before submission. Missing or invalid approval, text, select, and boolean answers are marked inline and keep Submit Response disabled; if the server rejects a response, its error is shown directly so the user can correct it.
Loop Support
Workflows support cycles (loops) in the DAG. A node can route back to a previous node via its next field, enabling iterative patterns like retry-until-success or iterative refinement.
The engine uses iteration-aware idempotency keys to distinguish between loop iterations. Each time a node re-executes in a new iteration, it gets a unique checkpoint, allowing the engine to track progress correctly across multiple passes through the same node.
{
"definition": {
"nodes": [
{
"id": "generate",
"type": "agent-task",
"config": { "template": "Generate a draft" },
"next": "validate"
},
{
"id": "validate",
"type": "validate",
"next": { "pass": "publish", "retry": "generate" }
},
{
"id": "publish",
"type": "agent-task",
"config": { "template": "Publish the final draft" }
}
]
}
}In this example, the validate node routes back to generate on failure, creating a loop that continues until validation passes.
Related
- Scheduled Tasks — Time-based task automation (complementary to event-driven workflows)
- Task Lifecycle — How tasks created by workflows flow through the system
- MCP Tools Reference — Full tool documentation
- Workflows API Reference — REST API endpoints for creating and managing workflows