Workflows
Build DAGs of nodes connected by edges to define workflow logic.
What it is. A deterministic set of instructions: a standard operating procedure you can declare, version, and reuse.
Use it when you already have a procedure and want it to run the same way every time. Workflows are also available to agents as tools, so the reasoning layer can invoke the exact one.
Works with triggers (what starts a run), node types (the steps), reviews (human approval), and streams (where output persists).
A workflow is a directed acyclic graph (DAG) of nodes connected by edges. Each node performs a discrete unit of work. The flow block defines the edges between nodes. Workflows span multiple .swirls files: declarations reference each other by name across the whole workspace, with no 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. |
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.
Example: multi-node workflow
This workflow normalizes form input, summarizes it with an LLM, and sends a confirmation email.
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"
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