Pipelines let you define multi-stage AI workflows as directed graphs using Graphviz DOT syntax. Each node in the graph is a task — an LLM call, a human review gate, a conditional branch, or a parallel fan-out — and edges define the flow between them.
Pipelines are declarative: you describe what the workflow looks like, and the execution engine decides how and when to run each stage.
Note
Node and graph attribute names can be written in kebab-case, snake_case, or camelCase. We recommend kebab-case for readability, and this page uses that casing.
Quick start
A pipeline is a digraph block in a workflow file. Here is a minimal pipeline that searches for literature, summarizes findings, and drafts a report:
Key elements:
digraph declares a directed graph. Every pipeline must be a single digraph.
graph [goal="..."] sets a pipeline-level goal. The $goal variable is expanded in node prompts.
Start and End are the entry and exit points, recognized automatically by name.
Nodes are tasks. By default they run as LLM stages.
Edges (->) define the flow between stages.
Nodes
Every node represents a task in the pipeline. The node's shape determines what kind of task it is. In practice, you rarely need to set shape explicitly — the shorthand conventions below (node ID prefixes and property shortcuts) cover most use cases and are more readable.
Shapes
The recommended approach is to use node ID prefixes (e.g. Review, CheckQuality, FanOut) or property shortcuts (e.g. ask="...", branch="...", shell="...") instead of explicit shape= attributes. These shorthands are self-documenting and reduce boilerplate.
The table below shows the underlying shape mechanism for reference:
| Shape | Purpose | Preferred form |
|---|
box (default) | LLM task | Analyze [prompt="Analyze the data"] |
hexagon | Human review gate | Review [ask="Review the draft"] |
diamond | Conditional routing | CheckQuality [branch="Meets criteria?"] |
component | Parallel fan-out | FanOut [label="Search in parallel"] |
tripleoctagon | Parallel fan-in | FanIn [label="Collect results"] |
invtriangle | Explicit failure | Fail |
Shorthand conventions
To reduce boilerplate, the pipeline engine recognizes certain node IDs and attribute names as implying a handler type. You do not need to set shape explicitly when using these conventions.
Node IDs
| Node ID | Implied shape | Handler type |
|---|
Start, start | Mdiamond | Entry point |
End, end, Exit, exit | Msquare | Exit point |
Fail, fail | invtriangle | Failure |
FanOut… | component | Parallel fan-out |
FanIn… | tripleoctagon | Parallel fan-in |
Review…, Approve… | hexagon | Human review |
Check…, Branch… | diamond | Conditional |
Shell…, Run… | parallelogram | Shell command |
The first three are exact ID matches; the rest are prefix matches (e.g. FanOutSearch, FanInResults, ReviewDraft, CheckQuality). A node with a fan-out attribute also implies shape=component (parallel fan-out), regardless of its ID.
An explicit shape attribute always takes precedence over ID-based inference. If a node has a prompt or agent attribute, it is treated as an LLM task (box), overriding prefix-based ID inference — so ReviewData [prompt="Summarize the reviews"] stays a codergen node, not a human gate. Reserved structural IDs (Start/End/Exit/Fail) are exempt: they always receive their structural shape.
Property shortcuts
| Shorthand | Expands to |
|---|
ask="Do you approve?" | shape=hexagon, label="Do you approve?" |
workflow="child" | type="workflow" |
shell="make build" | shape=parallelogram, shell_command="make build" |
shell="cargo test" | shape=parallelogram, shell_command="cargo test" |
branch="Quality OK?" | shape=diamond, label="Quality OK?" |
persist="full" | fidelity="full", thread-id="persist:{node_id}" |
Additionally, a node with an interview attribute (typically set via interview-ref) is inferred as shape=hexagon (a human gate).
Property shortcuts never override an explicitly set shape, label, type, or shell_command. All sugar keys (ask, workflow, shell, branch) are always normalized on the node, even when a higher-precedence shortcut wins.
Resolution order
When a node has no explicit shape, the engine applies the first matching rule:
Property shortcuts — ask > shell > branch (all consumed regardless of which wins)
workflow is also treated as a property shortcut and normalized to a workflow handler node
interview — node has an interview attribute (typically from interview-ref), inferred as human gate
prompt or agent — node is an LLM task, prefix-based ID inference is skipped (structural IDs exempt)
Node ID — exact or prefix match from the table above
Example — the combined example from below can be written more concisely:
Here CheckQuality is automatically a conditional node and Review is automatically a human gate — no shape= needed.
Common attributes
| Attribute | Type | Description |
|---|
label | String | Display name for the node. Used as the prompt fallback if prompt is empty. |
prompt | String | Instruction for the LLM. Supports variable expansion (see below). |
agent | String | Stencila agent to execute this node (e.g., "code-engineer"). |
agent.model | String | Override the agent's model (e.g., "gpt-4o", "o3"). |
agent.provider | String | Override the agent's provider (e.g., "openai", "anthropic"). |
agent.reasoning-effort | String | Override reasoning effort ("low", "medium", "high"). |
agent.trust-level | String | Override the agent's trust level ("low", "medium", "high"). |
agent.max-turns | Integer | Override maximum conversation turns (0 = unlimited). |
workflow | String | Run another workflow as a composed child workflow (e.g., "code-implementation"). |
goal | String | For workflow nodes, override the child workflow's goal. If omitted, the child goal defaults to the node's resolved input (prompt, then label). |
max-retries | Integer | Additional retry attempts beyond the initial execution. |
goal-gate | Boolean | If true, this node must succeed before the pipeline can exit. |
timeout | Duration | Maximum execution time (e.g., 900s, 15m). |
class | String | Comma-separated class names for overrides targeting. |
question-type | String | Human node question type: "yes-no", "confirm". "single-select', "freeform", Default: single select from edges. |
store | String | Context key to store a node's output in. On human nodes, stores the answer; on shell nodes, stores trimmed stdout. |
store-as | String | Shell node output format: "json" (force JSON parse), "string" (no parse), or absent (try JSON, fall back to string). |
persist | String | Shorthand for setting fidelity and thread-id together. Values: "true"/"full", "summary", "gist", "details", "false"/"off". |
fidelity | String | Context fidelity mode for session carryover. "full" enables session reuse via thread-id. See Session persistence. |
thread-id | String | Thread key for session reuse. Nodes sharing the same thread-id with fidelity="full" share a conversation session. |
fan-out | String | Dynamic fan-out list key. Resolves a JSON array from context and spawns one branch per item. true derives key from node ID. |
interview-ref | String | Reference to a YAML code block defining a multi-question interview (e.g., "#review-interview"). |
Agent property overrides
When a node references an agent via agent="name", you can override specific agent properties inline using agent.* dotted-key attributes:
This lets you use a shared agent definition while customizing its behavior per-node — for example, running a particular stage with a more capable model or a different provider.
The override precedence order (highest to lowest):
agent.* node attributes — explicit overrides on the node
Overrides rules — from overrides selectors
Agent definition — from the named agent's AGENT.md
System defaults
Workflow composition
Use the workflow node attribute to run another workflow as a child workflow.
In this example, Implement does not execute a prompt directly. Instead, it resolves and runs the code-implementation workflow and maps the child workflow's result back into the parent pipeline.
When to use composition
Composition is most useful when:
a stage is complex enough to deserve its own internal graph
the same subprocess should be reused by multiple parent workflows
the parent workflow should stay readable by delegating detail to a child workflow
Prefer a normal node with prompt, agent, or shell when the stage is simple and unlikely to be reused.
Input, parent context, and output mapping
When a child workflow runs:
the parent run id and parent node id are tracked as parent metadata
the child receives context values identifying its parent workflow and parent node
any node input is passed to the child under input
the child workflow's goal is set from the node's explicit goal attribute when present; otherwise it defaults to the node's resolved input (prompt, then label)
the child workflow's final output is mapped back to the parent as that node's output
So, after a composed node completes, downstream parent nodes can continue using normal variables such as $last_output.
This means child workflows can usually keep using $goal whether they run standalone or as a composed subprocess.
If goal is omitted, the child goal falls back to the composed node's resolved input.
The parent also receives workflow-scoped context updates including:
workflow.output.<node-id> — the child workflow's final output for that composed node
workflow.outcome.<node-id> — the child workflow's full outcome object
Composition should be acyclic
Workflow composition should remain acyclic. A workflow should not compose itself directly or indirectly through another workflow.
Current validation rejects direct self-reference within the same workflow definition. Avoid indirect composition cycles as well, even if they are not yet detected statically.
Prompt variables
Use $-prefixed variables in prompt attributes to inject dynamic values:
| Variable | Description | When expanded |
|---|
$goal | The pipeline-level goal from graph [goal="..."] | Before the pipeline runs |
$last_output | Full text of the previous stage's agent response | At each stage |
$last_outcome | Outcome status of the previous stage (success, fail) | At each stage |
$last_stage | Node ID of the previous completed stage | At each stage |
$KEY | Any value from the pipeline context (see below) | At each stage |
$goal is expanded once before the pipeline starts. The other variables are expanded at execution time, so each stage sees the outputs of the stage that ran before it.
Tool-based alternative
Variable interpolation embeds prior output directly into the prompt text, which can bloat messages in iterative workflows where outputs are long (e.g. full drafts or detailed reviews). For these cases, prefer the workflow_get_output and workflow_get_context tools instead — the engine automatically tells agents about these tools, and agents fetch the content via background tool calls rather than receiving it inline. See Workflow context tools below.
Variable interpolation remains the best choice for short references (e.g. $goal, $last_stage), shell commands, and edge conditions.
Built-in variables
Example using runtime variables:
Context variables ($KEY)
Use $KEY to reference any value stored in the pipeline context. The key can contain letters, digits, underscores, and dots. Missing keys resolve to an empty string.
For example, a short inline reference to a stored context value:
However, for iterative workflows where the interpolated content may be long (full drafts, detailed reviews), prefer using the workflow_get_output and workflow_get_context tools instead — see Workflow context tools below.
Context values can come from several sources:
Human answers stored via the store attribute on human nodes
LLM tool calls that write to the context via workflow_set_context
Handler outputs like human.gate.selected and human.gate.label
Referencing multiline content from Markdown
Instead of escaping long prompts, shell scripts, or human questions inside DOT strings, you can define them in fenced code blocks or code chunks with ids and reference them from node attributes.
Recommended style:
put the executable DOT block first for easier scanning
define the referenced code blocks after the DOT block
use kebab-case attribute names in examples and authored workflows
use refs mainly for long or multiline content; keep short single-line values inline when that is clearer
Supported reference attributes:
| Reference attribute | Resolves to |
|---|
prompt-ref="#id" | prompt |
shell-ref="#id" | shell_command |
ask-ref="#id" | human question label |
interview-ref="#id" | interview (multi-question interview spec) |
Example:
```dot
digraph Example {
Start -> Create -> Check -> Ask -> End
Create [agent="writer", prompt-ref="#creator-prompt"]
Check [shell-ref="#run-checks"]
Ask [ask-ref="#human-question", question-type="freeform"]
}
```
```text #creator-prompt
Create or update the draft for this goal: $goal
Before starting, check for reviewer feedback from a previous iteration.
If feedback is present, use it to revise the existing draft instead of starting over.
```
```sh #run-checks
cargo fmt -p workflows
cargo test -p workflows
```
```text #human-question
What should change before the next revision?
```
References resolve only against code blocks and code chunks in the same WORKFLOW.md. It is an error if a referenced id does not exist, if ids are duplicated, or if a node sets both a literal attribute and its corresponding *_ref attribute.
Edges
Edges define transitions between nodes. They can carry labels, conditions, and weights to control routing.
For authored workflows, the recommended style is to keep outgoing edges close to their source node: define the node, then place its outgoing edge or edges immediately after it. A small exception is the initial Start -> … entry edge, which is often kept near the top of the graph.
Attributes
| Attribute | Type | Description |
|---|
label | String | Display caption. Also used for preferred-label matching. |
condition | String | Boolean guard expression (e.g., "outcome=success"). |
weight | Integer | Priority for edge selection. Higher weight wins among equally eligible edges. |
Edge selection
After a node completes, the engine selects the next edge using this priority order:
Condition match — edges whose condition evaluates to true
Preferred label — edge whose label matches the handler's preferred label
Highest weight — among unconditional edges, the highest weight wins
Lexical tiebreak — alphabetical by target node ID
Chained edges
You can chain edges as shorthand:
This creates individual edges between each consecutive pair.
Conditions
Edge conditions use a simple expression language to gate transitions:
Supported syntax:
| Operator | Meaning | Example |
|---|
= | Equals | outcome=success |
!= | Not equals | outcome!=success |
&& | Logical AND | outcome=success && context.citations_valid=true |
Available variables:
outcome — the current node's result status (success, fail, retry, partial_success)
preferred_label — the handler's preferred edge label
context.* — values from the shared pipeline context
Keys starting with internal. are reserved and cannot be written by agents.
Agent routing with workflow_set_route
When an agent node has outgoing edges with labels, the engine automatically provides routing instructions so the agent can make a structured routing decision instead of relying on text output matching.
For sessions with tool support, the agent receives a workflow_set_route tool and calls it with the chosen label. For sessions without tool support, the agent is instructed to end its response with a <preferred-label> XML tag, which the engine parses.
For example, this review node gives the agent two labeled branches:
The agent sees both labels and signals its choice — via a tool call or XML tag — with either Accept or Revise. The engine matches the chosen label against outgoing edge labels (case-insensitive) and follows the corresponding edge.
This is more reliable than prompting the agent to reply with a specific word and matching against context.last_output, because routing is decoupled from the agent's text response. The agent can provide detailed feedback in its text output while separately signaling the routing decision.
Note that edge conditions (Step 1 in the edge selection algorithm) take priority over preferred labels (Step 2).
Workflow patterns
Linear pipeline
The simplest pattern: stages execute one after another.
Conditional branching
Use conditions on edges to route based on outcomes.
The Check prefix and branch attribute both imply a conditional (diamond) routing point. The engine evaluates edge conditions against the current outcome and context to decide which path to take.
Retry loops
Nodes can retry automatically on failure using max-retries:
max-retries=2 means up to 3 total executions (1 initial + 2 retries).
For more control, use edge-based retry loops that route back to an earlier stage on failure:
Shell nodes
Shell nodes run a command and capture its standard output. Use the Shell… or Run… node ID prefix, or the shell property shortcut:
The command's trimmed stdout is stored as shell.output and last_output in the pipeline context.
Shell output storage
The store attribute on a shell node writes the command's trimmed stdout into the pipeline context under a named key, in addition to the standard shell.output and last_output keys:
By default, the handler tries to parse the output as JSON. If the output is valid JSON it is stored as a typed value (array, object, number, etc.); otherwise it falls back to storing the raw string. This means a command that emits a JSON array produces a real list in the context, while a command that emits plain text is stored as-is.
The store-as attribute overrides this automatic behavior:
store-as value | Behavior |
|---|
"json" | Force JSON parse. Fails the node if the output is not valid JSON. |
"string" | Always store as a plain string, no JSON parsing. |
| (absent) | Try JSON parse, fall back to string (the default). |
This is primarily useful for producing structured data — like JSON arrays — that downstream nodes can consume as typed values. For example, a shell node can emit a JSON list that a dynamic fan-out node iterates over:
Note
Use kebab-case (store-as) in DOT attributes. The canonical form store_as is also accepted but kebab-case is recommended for consistency.
Goal gates
Mark critical stages with goal-gate=true to prevent the pipeline from exiting until they succeed:
If the pipeline reaches the exit node and any goal gate node has not succeeded, the engine looks for a retry-target to jump back to instead of exiting.
Human-in-the-loop
Use a Review (or Approve) node ID prefix, or the ask property shortcut, to create a gate that pauses for human input. The choices are derived from the node's outgoing edge labels:
The human is presented with the choices derived from the outgoing edges:
[A] Approve — continues to publication
[R] Revise — loops back to re-analyze
Accelerator keys
Each outgoing edge from a human gate becomes a selectable option with an accelerator key — a short string the user can type or press to quickly select that option. The engine extracts the key from the edge label using these formats:
| Format | Example | Parsed key |
|---|
[K] Label | [Y] Yes, deploy | Y |
K) Label | A) Option A | A |
K - Label | X - Choice X | X |
| Plain label (fallback) | Deploy | D |
The space after the delimiter (], )) is optional — [Y]Yes works the same as [Y] Yes. Brackets support multi-character keys like [OK] Continue or [AB] Option AB. The parenthesis and dash formats are limited to a single character.
Explicit keys are optional. When a label has no recognized prefix, the engine automatically derives the key from the first character of the label (uppercased). For example, these two blocks are functionally equivalent:
When to use explicit keys:
Disambiguating collisions — If two labels start with the same letter (e.g., Staging and Send), auto-derived keys would both be S. Use [S] Staging and [X] Send to assign distinct keys.
Multi-character keys — The bracket format supports keys like [OK] that can't be expressed with the single-character fallback.
Choosing a more intuitive key — e.g., [N] No, abort deployment assigns N instead of the auto-derived N from "No" — same result here, but explicit keys make intent clear when the first letter isn't the most natural accelerator.
Here is an example with a three-way choice using auto-derived keys:
The engine derives keys S, P, D from the first letter of each label. No brackets are needed because the first letters are already unique.
Question types
By default, human nodes derive a multiple-choice question from their outgoing edge labels. You can override this by setting the question-type attribute:
question-type | Description | Routing |
|---|
| (default) | Single-select (multiple choice) from edge labels | Routes to the selected edge |
"freeform" | Free-form text input | Follows first outgoing edge |
"yes-no" | Yes/no binary choice | Follows first outgoing edge |
"confirm" | Confirmation prompt | Follows first outgoing edge |
For non-choice types, the node always follows its first outgoing edge — there is no choice-matching step. The node still needs at least one outgoing edge for routing.
Storing answers (store)
The store attribute writes the human's answer into the pipeline context under a named key. Later nodes can reference this value using $KEY in their prompts:
When the human provides an answer, it is stored as a string:
Freeform text — the entered text
Single-select — the selected accelerator key
Yes/no — "yes" or "no"
Timeout or skip — key is not set (resolves to "" when referenced)
Collecting human feedback
Combining question-type, store, and workflow context tools enables iterative workflows where a human can provide specific feedback that guides subsequent stages.
Here is a complete example of a create–review–revise workflow:
This pipeline:
Creates the initial skill draft (or revises it based on feedback)
Reviews the draft automatically with a reviewer agent
Routes to human review on success, or loops back to revise on failure
Asks the human whether to accept or revise
If revising, collects freeform feedback that describes what to change
The feedback is stored as human.feedback in the pipeline context. On the next iteration, the Create agent retrieves it using the workflow_get_context tool and retrieves the reviewer's output using workflow_get_output — both happen as background tool calls, keeping the prompt compact
Multi-question interviews
When a single human pause needs to collect multiple pieces of information — such as a routing decision and detailed feedback — use interview-ref to reference a YAML code block that defines a structured interview.
The YAML block specifies a preamble (optional context shown before the questions) and a questions array. Each question can have a type (defaults to freeform), options (for single-select and multi-select types), a default, and a store key for saving the answer to the pipeline context.
preamble: |
Please review the implementation.
questions:
- question: "Is the implementation ready to merge?"
header: Decision
type: single-select
options:
- label: Approve
- label: Revise
store: review.decision
- question: "What specific changes should be made?"
header: Feedback
store: review.feedback
Routing is driven by the first single-select question's answer, matched against outgoing edge labels — the same mechanism as single-question human nodes. When an interview has no single-select question, the node follows its first outgoing edge. An interview node with no outgoing edges succeeds as a terminal node after collecting answers.
Storing answers — each question with a store key writes its answer to the pipeline context. Downstream nodes reference these values using $KEY expansion (e.g., $review.feedback in a prompt). Freeform questions without a store key will trigger a validation warning, since the answer would be collected but never stored.
Conditional questions — use show-if to display a question only when a previous answer matches a condition (e.g., show-if: "decision == Revise"), and finish-if to end the interview early when an answer matches a value (e.g., finish-if: "no" on a yes-no gate question). These can be combined to build branching interviews with early-exit gates.
Use interview-ref when a review step naturally combines routing with structured feedback. Use separate ask / ask-ref nodes when the questions are independent or belong to different pipeline stages.
See Creating Workflows — Multi-question interviews for the full spec format, conditional question examples, and guidance.
Parallel execution
Fan out to multiple branches using a FanOut… node ID prefix and collect results with a fan-in node. Branches can be defined statically in the graph (one edge per branch) or dynamically at runtime using the fan-out attribute. You can make the fan-in point explicit using a FanIn… node ID prefix. These prefixes imply shape=component and shape=tripleoctagon respectively, so you don't need to set the shape explicitly:
Branches execute concurrently. The fan-in node waits for all branches to complete before proceeding.
Dynamic fan-out
Static fan-out (above) fixes the number of branches at graph-definition time. The fan-out attribute adds dynamic fan-out, where the branch count is determined at runtime from a variable-length list in the pipeline context.
A node with fan-out must have exactly one outgoing edge pointing to the template entry node. The engine spawns one concurrent branch per list item, each executing the same downstream subgraph with per-item context.
The list is typically produced by an upstream shell node with store (see Shell output storage), but any source that writes a JSON array into the context works — for example, an agent node with context-writable=true can call workflow_set_context to store a JSON array (e.g., key items), and the fan-out node references it with fan-out="items".
Attribute forms:
| Form | Behavior |
|---|
fan-out="key" | Resolves the context key as a JSON array and fans out over it. |
fan-out=true | Derives the key from the node ID in snake_case (e.g., FanOutRepos → fan_out_repos). |
Per-branch context:
Each branch receives the following variables, accessible via $-expansion or workflow context tools:
| Variable | Description |
|---|
$fan_out.item | The current list item (scalar or JSON object). |
$fan_out.index | Zero-based index of this branch. |
$fan_out.total | Total number of items in the list. |
$fan_out.key | The context key that was fanned out over. |
$fan_out.item.<prop> | For object items, individual properties are accessible directly. |
After all branches complete, results are aggregated into parallel.results (list of per-branch outcomes) and parallel.outputs (list of per-branch outputs). The fan-in successor receives these as context values.
Empty lists: When the resolved list is empty, the handler skips directly to the fan-in successor — no branches are spawned.
Validation rules:
The fan-out node must have exactly one outgoing edge.
A warning is emitted if no tripleoctagon fan-in node is reachable from the template subgraph.
Nested dynamic fan-out (a dynamic fan-out inside another dynamic fan-out's template subgraph) is rejected.
Example — discover repositories with a shell node, then analyze each one in parallel:
This pipeline:
Discovers repositories by running gh repo list and storing the JSON output as a typed array under repos
Fans out dynamically — one branch per repository in the list
Audits each repository concurrently, with $fan_out.item.name and $fan_out.item.url expanded per branch
Collects results at the fan-in node
Summarizes the combined findings
Composed subprocess
Use this pattern when a single stage in the parent workflow should expand into a reusable internal process:
The Review prefix alone implies a human gate — no shape= needed. The parent graph stays focused on orchestration, while paper-drafting can own the internal research, outlining, drafting, and checking stages.
Session persistence
By default, each agent node in a pipeline starts a fresh conversation — it has no memory of what earlier nodes said. Session persistence lets multiple nodes share a conversation session so that later nodes can recall context from earlier ones, enabling multi-turn interactions across pipeline stages.
This is useful when a pipeline has logically connected steps — for example, an agent that first reads a set of research papers and then synthesizes findings across them. Without session persistence, the synthesis step would not remember what the agent read; with it, the LLM sees the full conversation history and can build on its earlier reasoning.
persist shorthand
The simplest way to enable session persistence is the persist attribute, which sets fidelity and thread-id together:
Each node with persist="full" gets fidelity="full" and an auto-generated thread-id of the form persist:{node_id}. Since the thread IDs differ (one per node), these two nodes do not share a session — each starts fresh. To share a session across nodes, use an explicit thread-id (see below) or graph-level defaults.
persist values:
| Value | Expands to |
|---|
"true" or "full" | fidelity="full", thread-id="persist:{node_id}" |
"summary" | fidelity="summary:medium", thread-id="persist:{node_id}" |
"gist" | fidelity="summary:low", thread-id="persist:{node_id}" |
"details" | fidelity="summary:high", thread-id="persist:{node_id}" |
"false" or "off" | (disabled — no fidelity or thread-id set) |
If fidelity is already set explicitly on the node, persist does not override it or generate a thread-id. If thread-id is already set, it is preserved. The persist attribute is always removed after processing — it never appears in the final graph.
fidelity and thread-id
For full control, set fidelity and thread-id directly. The key concept: nodes that share the same thread-id with fidelity="full" reuse the same conversation session, so the LLM sees the full message history from all prior nodes on that thread.
Warning
A shared thread-id means shared conversation history. In practice, nodes that share a thread-id should use the same agent, because different agents may have different system prompts, tools, and behavior. Reusing one thread across different agents is invalid and fails workflow validation. Likewise, do not reuse the same thread-id across parallel branches.
In this example:
Explore starts a session under thread "analysis" and examines the dataset
Interpret joins the same "analysis" thread — the LLM sees the full exploratory analysis and can reference the specific proteins and outliers it identified, without needing them restated in the prompt
WriteUp has no fidelity or thread-id, so it starts a fresh session — appropriate here because drafting the results section is an independent writing task that receives the prior output via the standard $last_output variable
Fidelity modes
The fidelity attribute controls how much conversation context is carried over:
| Mode | Behavior |
|---|
"full" | Full session reuse — the LLM sees the complete message history. |
"summary:low" | Low-detail summary of prior context (available via persist="gist"). |
"summary:medium" | Medium-detail summary (available via persist="summary"). |
"summary:high" | High-detail summary (available via persist="details"). |
"compact" | Compact context carryover. Set directly — not available via persist. |
"truncate" | Truncated context. Set directly — not available via persist. |
Warning
Currently, only full fidelity alters runtime behavior (session reuse by thread-id). The summary, compact, and truncate modes are resolved and validated but do not yet synthesize different context-carryover prompt text.
Graph-level defaults
Use the default-fidelity and default-thread-id graph attributes to set pipeline-wide defaults. This is the most concise way to make an entire pipeline share a single conversation:
All three nodes inherit fidelity="full" and thread-id="main", sharing a single conversation session throughout the pipeline. The Assess step remembers which studies Search found, and Synthesize remembers both the studies and the quality assessment — so the final review can reference specific papers and their limitations without restating them.
When to use session persistence
Multi-step analysis — an agent explores a dataset, identifies patterns, then interprets their significance. Each step should build on what came before.
Literature reviews — an agent searches for papers, screens them for relevance, then synthesizes findings. The synthesis is richer when the agent remembers what it read.
Iterative drafting — a manuscript section is written, then revised based on reviewer feedback, with the revision step aware of the original reasoning.
Session persistence is not needed when nodes are independent or when the relevant context is already passed via $-variable expansion or workflow context tools. Use it when the full conversation history matters — not just a single output value.
Overrides
Centralize agent property overrides with CSS-like rules instead of setting agent.model on every node:
Selectors and specificity:
| Selector | Matches | Specificity |
|---|
* | All nodes | Lowest |
.class_name | Nodes with that class | Medium |
#node_id | Specific node by ID | Highest |
Explicit agent.* attributes on a node always override values from the overrides rules.
Combined example
Here is a more complete pipeline combining several patterns:
This pipeline:
Searches for papers using the default model (Sonnet)
Screens for relevance with up to 2 retries on failure
Analyzes using Opus (via .deep_analysis class) with a goal gate ensuring it must succeed
Branches based on quality check — passing goes to review, failing loops back to re-analyze
Pauses for human review with approve/revise options
Formats the approved review for publication
Graph attributes
| Attribute | Type | Default | Description |
|---|
goal | String | "" | Pipeline-level goal. Expanded as $goal in prompts. |
label | String | "" | Display name for the pipeline. |
overrides | String | "" | CSS-like per-node agent override rules. |
default-max-retry | Integer | 3 | Global retry ceiling for nodes that omit max-retries. |
default-fidelity | String | "" | Default context fidelity mode. |
default-thread-id | String | "" | Default thread key for full fidelity session reuse. |
retry-target | String | "" | Node to jump to when goal gates are unsatisfied at exit. |
Context and state
Nodes communicate through a shared key-value context. After each node executes, its outcome and any context_updates are merged into the context. Subsequent nodes can reference these values in edge conditions (e.g., context.citations_valid=true), in prompt variables (e.g., $last_stage), or by calling workflow context tools.
Context values come from several sources:
Human node store — the store attribute on human nodes writes the answer into a named key (e.g., store="human.feedback")
Shell node store — the store attribute on shell nodes writes trimmed stdout into a named key, with optional JSON parsing controlled by store-as
LLM tool calls — when an LLM writes context via the workflow_set_context tool
Handler outputs — built-in keys like human.gate.selected, human.gate.label, last_output, last_stage
Graph attributes — graph.* keys are mirrored into context at pipeline start
The engine also saves a checkpoint after each node completes. If the pipeline crashes, it can resume from the last checkpoint.
Workflow context tools
Agent nodes in a pipeline automatically receive workflow context tools that let them query pipeline state on demand via tool calls. This is the recommended way for agents to access prior outputs and stored values in iterative workflows, because the content is fetched in the background rather than being interpolated into the prompt text.
Available tools:
| Tool | Purpose |
|---|
workflow_get_output | Get the output of a pipeline node by ID, or the most recent output if no node_id is given |
workflow_get_context | Read a specific context key (e.g., human.feedback) or all context |
workflow_set_context | Write a value to the workflow context (requires context-writable=true on the node) |
workflow_get_run | Get metadata about the current run (name, goal, status, start time) |
workflow_list_nodes | List all workflow nodes with status and duration |
workflow_store_artifact | Store a file artifact associated with this run |
workflow_get_artifact | Retrieve a previously stored artifact by ID |
The engine appends usage instructions to each agent's prompt when these tools are available. Agents are told to call workflow_get_output for prior output (e.g., reviewer feedback or a previous draft) and workflow_get_context for stored values (e.g., human.feedback).
When to use tools vs variables: Use $-variable interpolation for short values like $goal, $last_stage, and $last_outcome, and in shell commands and edge conditions where tools are not available. Prefer tools like workflow_get_output and workflow_get_context when the content may be long (full drafts, detailed reviews) to keep prompts compact.
Syntax reference
DOT basics
One digraph per file
Node IDs must match [A-Za-z_][A-Za-z0-9_]*
Edges use -> (directed only)
Attributes go in [key=value, key=value] blocks
String values use double quotes: "hello world"
Comments: // line and /* block */
Semicolons are optional
Duration values
Duration attributes like timeout use an integer with a unit suffix:
| Unit | Example | Meaning |
|---|
ms | 250ms | Milliseconds |
s | 900s | Seconds |
m | 15m | Minutes |
h | 2h | Hours |
d | 1d | Days |
Subgraphs
Subgraphs scope default attributes for a group of nodes:
Nodes inside the subgraph inherit its defaults unless they explicitly override them.