Workflows
Build DAGs of nodes connected by edges to define workflow logic.
A workflow defines steps and their dependencies as a directed acyclic graph (DAG). Each node performs a unit of work; edges in the flow block determine which nodes must finish before others start. Use workflows for procedures with explicit steps, including procedures that call an agent for judgment. Agents can also call workflows as tools.
Triggers start runs, node types define the work, reviews pause for human approval, and streams persist output. Declarations reference each other by name across all .swirls files in the workspace, without imports.
Workflow structure
| Field | Type | Required | Description |
|---|---|---|---|
label | string | No | Display name. Defaults to the workflow name. |
description | string | No | Description. Required (non-empty) when the workflow is used as an agent tool. |
concurrency | integer | No | Maximum ready nodes running at once, from 1 through 64. Defaults to 8. |
executionTokenTtl | duration string | No | Execution-authorization lifetime from "5m" through "24h". Defaults to "150m". |
Cycles are not allowed in the outer flow block. For in-workflow iteration, use type: map or type: while nodes (child workflows), or trigger separate runs via a schedule.
Root node
Every workflow has exactly one root node. The root node is the entry point for the workflow run: it receives the trigger payload on context.nodes.root.input. It defines:
inputSchema: the shape of data the workflow accepts from a triggeroutputSchema: the data passed to downstream nodes
Use the root { ... } syntax (not node root). The root can be any node type. inputSchema and outputSchema are root-only; using them on other nodes is a parse error.
Regular nodes
Additional nodes use node name { ... }. Each node has a type and type-specific configuration. Downstream nodes define only schema (the node’s output shape). Their input is derived from the outputs of upstream nodes, accessed via context.nodes.
Flow block
The flow { ... } block defines edges between nodes.
source -> target: a simple edgesource -["label"]-> target: a labeled edge, used withswitchnodes
The validator enforces the DAG rules: no cycles, no self-edges, every edge names a declared node, and exactly one node with no incoming edges (the root; a parentless stream read counts as a second root and fails validation). Fan-out and fan-in are fine: a node can feed several branches and several branches can converge.
Ready nodes run concurrently in stable dependency order. A workflow-level concurrency: field caps active nodes from 1 through 64 and defaults to 8. Each completed node's output is durably checkpointed, so completed nodes do not re-execute on resume. See How execution works.
Managed connections are authorized with an execution-scoped capability. It remains valid for 150m by default. Long-running workflows can set executionTokenTtl: "3h" (whole-number s, m, or h units, bounded from 5m through 24h). The root workflow's value covers its child executions; a child cannot widen the parent's authorization. This is wall-clock time from the start of the run, so waits, reviews, retries, and child executions all consume the same lifetime. Size it for the longest expected path before a managed connection is used.
Example: multi-node workflow
This workflow normalizes form input, summarizes it with an LLM, and sends a confirmation email.
secret vendor_keys {
vars: [OPENROUTER_API_KEY, RESEND_API_KEY]
}
schema contact_payload {
label: "Contact message"
schema: @json {
{
"type": "object",
"required": ["name", "email", "message"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"message": { "type": "string" }
},
"additionalProperties": false
}
}
}
workflow process_form {
label: "Process Form"
description: "Normalize input, summarize with LLM, send email"
executionTokenTtl: "3h"
root {
type: code
label: "Entry"
inputSchema: contact_payload
outputSchema: contact_payload
code: @ts {
const { name, email, message } = context.nodes.root.input
return {
name: name.trim(),
email: email.trim().toLowerCase(),
message: message.trim(),
}
}
}
node summarize {
type: ai
kind: object
label: "Summarize"
schema: @json {
{ "type": "object", "required": ["text"], "properties": { "text": { "type": "string" } } }
}
model: "google/gemini-2.5-flash"
prompt: @ts {
const { name, message } = context.nodes.root.output
return `Summarize this message from ${name}: ${message}`
}
}
node notify {
type: email
label: "Send confirmation"
from: @ts { return "[email protected]" }
to: @ts { return context.nodes.root.output.email }
subject: @ts { return "We received your message" }
text: @ts {
const summary = context.nodes.summarize.output.text ?? ""
return `Thanks for reaching out. Summary: ${summary}`
}
}
flow {
root -> summarize
summarize -> notify
}
}Branching with switch nodes
A switch node routes execution to one of several labeled branches. The router function returns the name of the branch to follow. Each branch is then connected using a labeled edge in the flow block.
schema score_payload {
label: "Score input/output"
schema: @json {
{
"type": "object",
"required": ["score"],
"properties": { "score": { "type": "number" } },
"additionalProperties": false
}
}
}
schema route_branch_result {
label: "Switch branch result"
schema: @json {
{
"type": "object",
"required": ["result"],
"properties": { "result": { "type": "string" } },
"additionalProperties": false
}
}
}
workflow route_by_score {
label: "Route by Score"
root {
type: code
label: "Entry"
inputSchema: score_payload
outputSchema: score_payload
code: @ts {
return { score: Number(context.nodes.root.input.score) || 0 }
}
}
node route {
type: switch
label: "Route"
cases: ["high", "low"]
router: @ts {
return context.nodes.root.output.score > 50 ? "high" : "low"
}
}
node handle_high {
type: code
label: "High path"
schema: route_branch_result
code: @ts { return { result: "High priority" } }
}
node handle_low {
type: code
label: "Low path"
schema: route_branch_result
code: @ts { return { result: "Low priority" } }
}
flow {
root -> route
route -["high"]-> handle_high
route -["low"]-> handle_low
}
}Further reading
- Node types: all available node types and their configuration
- Context: type-safe access to node outputs via
context.nodes - Multi-file workflows: workspace layout and cross-file references
- Streams: typed workflow output persisted for other workflows and integrations
- Testing: playbooks, mocks, and
swirls testfor hosted verification
Decisions and model routing
Use decisions to evaluate typed Choice, Score, and Noul questions with Jev. Route probabilities through switches and separate human review gates, or use them to choose a model for a later AI or agent invocation.
Embed agent chat in your app
Connect a signed-in user of your app to a Swirls agent over the web channel, with conversations and history backed by the Swirls API. OIDC federation keeps access tokens on your server.
Node Types
Configuration reference for node types available in .swirls files, including shell.