Workflow Schema Reference
Workflow Schema Reference
Reliant workflows are defined in YAML files. This reference documents every field in the workflow schema.
Top-Level Fields
The top level of a workflow file contains metadata and the core structure.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier for the workflow |
apiVersion | string | No | Schema version (currently 0.0.5) |
version | string | No | Semantic version (e.g., v0.0.11) |
description | string | No | Human-readable description |
status | string | No | Workflow visibility: draft, published, or internal |
tag | string | No | Tag for preset matching (e.g., agent, orchestration) |
entry | string or string[] | Yes | Entry point node ID(s) |
inputs | map | No | Input parameter definitions |
outputs | map | No | Output expressions evaluated on completion |
groups | map | No | Named input parameter groups |
nodes | Node[] | Yes | Workflow execution nodes |
edges | Edge[] | Conditional | Connections between nodes (required unless single-node with entry) |
thread | ThreadConfig | No | Top-level thread configuration |
ui | UIMetadata | No | Visual editor positioning data |
Entry Points
The entry field specifies which node(s) start execution. It replaces the deprecated from: started edge pattern.
Single entry point:
entry: agent_loopParallel entry (multiple nodes start simultaneously):
entry: [racer_1, racer_2, racer_3]Status Values
| Value | Description |
|---|---|
draft | Work in progress, not listed in workflow picker |
published | Available to users |
internal | System workflow, hidden from users |
Example
name: agent
apiVersion: "0.0.5"
version: v0.0.11
description: Interactive agent workflow with trigger model
status: published
tag: agent
entry: agent_loop
inputs:
mode:
type: enum
enum: ["agent", "plan", "manual"]
default: "agent"
description: Execution mode
outputs:
response_text: "{{nodes.agent_loop.response_text}}"
iterations: "{{nodes.agent_loop.iterations}}"
nodes:
- id: agent_loop
# ... node definitionInput Schema
Inputs define configurable parameters for the workflow. Each input has a type and optional validation constraints.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Data type (see Input Types below) |
required | boolean | No | If true, must be provided by caller |
default | any | No | Default value if not provided |
description | string | No | Human-readable description |
ui | string | No | UI visibility: hidden or omit for visible |
Input Types
| Type | Description | UI Rendering |
|---|---|---|
string | Text value | Text input |
integer | Whole number | Number input |
number | Decimal number | Number input or slider (if min/max) |
boolean | True/false | Toggle switch |
enum | Selection from allowed values | Dropdown |
model | LLM model identifier | Model selector dropdown |
tools | Tool filter expressions | Tool multi-select |
attachments | File attachment references | File picker |
any | Any JSON value | Generic input |
String Validation
| Field | Type | Description |
|---|---|---|
pattern | string | Regex pattern to match |
enum | string[] | Allowed values (for enum type) |
multi | boolean | Allow multiple selections (enum only) |
min_length | integer | Minimum string length |
max_length | integer | Maximum string length |
Number Validation
| Field | Type | Description |
|---|---|---|
min | number | Minimum allowed value |
max | number | Maximum allowed value |
UI Hints
Use ui: hidden for internal parameters that shouldn’t appear in the configuration panel:
inputs:
# Shown in params panel
model:
type: model
default: ""
description: LLM model to use
# Hidden from UI - internal use only
chat_id:
type: string
ui: hidden
default: ""Validation Rules
Every input must satisfy one of these conditions:
- Have
required: true, OR - Have a non-null
defaultvalue
This prevents runtime “no such key” errors in CEL expressions.
Example
inputs:
mode:
type: enum
enum: ["manual", "agent", "plan"]
default: "agent"
description: |
Execution mode:
- manual: Requires approval for tool calls
- agent: Auto-approves all tool calls
- plan: Read-only tools only
temperature:
type: number
default: 1.0
min: 0
max: 1
description: Response randomness (0 = focused, 1 = creative)
max_turns:
type: integer
default: 100
min: 1
max: 500
description: Maximum agent loop iterations
tools:
type: tools
default: ["tag:default"]
description: Available tools for the agent
spawn_presets:
type: enum
multi: true
enum:
- general
- researcher
- code_reviewer
default:
- general
- researcher
description: Presets available for spawn toolNodes
Nodes are the execution units in a workflow. Each node must have exactly one type.
Common Node Fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique node identifier |
args | map | No | Arguments passed to the node (supports CEL expressions) |
thread | ThreadConfig | No | Thread configuration for this node |
timeout | string | No | Activity timeout (e.g., 5m, 1h) |
save_message | SaveMessageConfig | No | Auto-save output as message |
Node Types
Each node must have exactly one of these type fields:
| Field | Description |
|---|---|
action | Execute a registered activity |
workflow | Execute a child workflow |
loop | Execute a sub-workflow repeatedly |
run | Execute a shell command (shorthand) |
join | Synchronization point |
Action Nodes
Execute a registered activity:
- id: call_llm
action: CallLLM
args:
thread: "{{thread.id}}"
model: "{{inputs.model}}"
temperature: "{{inputs.temperature}}"Workflow Nodes
Execute a child workflow:
- id: impl_1
workflow: builtin://agent
thread:
mode: new()
key: impl_1
inject:
role: user
content: "Implement the feature..."
args:
mode: "{{inputs.mode}}"
model: "{{inputs.model}}"Workflow references:
builtin://agent- Built-in workflowmy-workflow- Workflow file in same directory
Loop Nodes
Execute a sub-workflow repeatedly until a condition is met or max iterations reached.
- id: agent_loop
loop:
max: "{{inputs.max_turns}}"
until: outputs.tool_calls == null || size(outputs.tool_calls) == 0
inline:
entry: call_llm
inputs:
model:
type: model
default: ""
outputs:
tool_calls: "{{nodes.call_llm.tool_calls}}"
nodes:
- id: call_llm
action: CallLLM
args:
thread: "{{thread.id}}"
edges:
- from: call_llm
cases:
- to: execute_tools
condition: nodes.call_llm.tool_calls != nullLoop Configuration:
| Field | Type | Required | Description |
|---|---|---|---|
max | integer | Yes | Maximum iterations |
until | string | No | CEL condition to exit early (uses outputs.* namespace) |
workflow | string | No | External workflow reference |
inline | InlineLoopBody | No | Inline workflow definition |
entry | string | No | Entry node for inline loops |
Must specify either workflow OR inline, not both.
Loop Outputs:
After loop completion, these outputs are available:
| Output | Type | Description |
|---|---|---|
iterations | integer | Number of iterations executed |
max | integer | Configured max iterations |
succeeded | boolean | True if exited via until, false if hit max |
<output_name> | any | Last iteration’s workflow outputs |
Loop Context:
Inside loops, additional context is available:
iter.iteration- Current iteration (0-indexed)iter.max- Maximum iterations
Run Nodes
Shorthand for shell command execution:
- id: run_tests
run: npm testJoin Nodes
Synchronization point for parallel branches:
- id: all_complete
join: all # Wait for all incoming edges
- id: race_winner
join: any # First one wins, others abandoned| Mode | Description |
|---|---|
all | Wait for all incoming edges to complete |
any | Continue when any one incoming edge completes |
Save Message Configuration
Auto-save node output as a chat message:
- id: call_llm
action: CallLLM
save_message:
role: "{{output.message.role}}"
content: "{{output.message.text}}"
tool_calls: "{{output.tool_calls}}"
input_tokens: "{{output.input_tokens}}"
output_tokens: "{{output.output_tokens}}"| Field | Type | Description |
|---|---|---|
condition | string | CEL expression - only save if true |
role | string | Message role: user, assistant, tool, system |
content | string | Message text content |
thread | string | Thread to save to (defaults to workflow thread) |
tool_calls | string | Tool calls for assistant messages |
tool_results | string | Tool results for tool messages |
input_tokens | string | LLM input token count |
output_tokens | string | LLM output token count |
cache_creation_tokens | string | Cache write tokens |
cache_read_tokens | string | Cache read tokens |
thinking | string | Extended thinking/reasoning content |
context_sequence | string | Context sequence override |
attachments | string | Attachment IDs for user messages |
The save_message block has access to the output.* namespace containing the node’s execution output.
Example: Complete Node
- id: compact
action: Compact
timeout: "10m"
save_message:
condition: "{{output.compacted}}"
role: "{{output.message.role}}"
content: "{{output.message.text}}"
context_sequence: "{{output.context_sequence}}"
args:
thread: "{{inputs.thread}}"Edges
Edges define the flow between nodes using conditional routing.
| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Source node ID |
cases | EdgeCase[] | Yes | Routing cases |
Edge Cases
| Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Target node ID |
condition | string | No | CEL expression (omit for default case) |
label | string | No | Label for visualization |
Conditional Routing
Edges support CEL expressions for conditional routing. The first matching case wins:
edges:
- from: call_llm
cases:
# Conditional case - tool calls present, needs approval
- to: approval
condition: nodes.call_llm.tool_calls != null && size(nodes.call_llm.tool_calls) > 0 && inputs.mode == 'manual'
label: "require_approval"
# Conditional case - tool calls present, auto-approve
- to: execute_tools
condition: nodes.call_llm.tool_calls != null && size(nodes.call_llm.tool_calls) > 0 && inputs.mode != 'manual'
label: "auto_approve"
# Default case - no condition means fallback
- to: done
label: "complete"Parallel Execution
Multiple edges from the same node execute their targets in parallel:
# Three worktrees created simultaneously
- from: save_improved_prompt
cases:
- to: create_worktree_1
- from: save_improved_prompt
cases:
- to: create_worktree_2
- from: save_improved_prompt
cases:
- to: create_worktree_3CEL Expression Context
Edge conditions have access to these namespaces:
| Namespace | Description |
|---|---|
inputs.* | Workflow input values |
nodes.<id>.* | Outputs from completed nodes |
workflow.* | Workflow metadata (id, name, chat_id) |
Validation Rules
- Source node (
from) must exist innodes - Target node (
to) must exist innodes from: startedis deprecated - useentryfield instead- Don’t use
.completedsuffix on node IDs
Thread Modes
Thread configuration controls message isolation between workflow components.
Thread Config Structure
thread:
mode: new()
key: impl_1
memo: true
inject:
role: user
content: "Review the above."| Field | Type | Default | Description |
|---|---|---|---|
mode | string | inherit | Thread relationship mode |
key | string | static | Thread identity key |
memo | boolean | true | Reuse thread across loop iterations |
inject | InjectConfig | - | Message to add to thread |
Mode Values
| Mode | Description |
|---|---|
inherit | Use parent workflow’s thread (default) |
new() | Create new empty thread |
fork | Copy parent thread context, then diverge |
Shorthand Syntax
Thread mode can be specified as a simple string:
# Shorthand
thread: inherit
thread: new()
thread: fork
# Full form (equivalent)
thread:
mode: inheritInject Configuration
Add a message to the thread before execution:
| Field | Type | Default | Description |
|---|---|---|---|
role | string | user | Message role |
content | string | Required | Message content (CEL expression) |
attachments | string | - | Attachment IDs (CEL expression) |
Examples
Inherit parent thread (default):
- id: call_llm
action: CallLLM
thread: inherit # Uses parent's threadNew isolated thread:
- id: impl_1
workflow: builtin://agent
thread:
mode: new()
key: impl_1
inject:
role: user
content: |
You are Implementation Candidate #1.
Task: {{nodes.improve_prompt.message.text}}Fork with context:
- id: review
workflow: builtin://agent
thread:
mode: fork
key: reviewer
inject:
role: user
content: |
Review the implementations above.
Pick the winner.Thread Keys
Thread keys provide stable identities for deterministic thread IDs:
| Pattern | Use Case |
|---|---|
key: advocate | Debate workflows with named participants |
key: impl_1 | Parallel implementations |
key: reviewer | Reviewer forks |
Without a key, threads use static (single thread) or iteration index (in loops).
Memo Behavior
In loops, memo controls thread reuse:
thread:
mode: new()
memo: true # Same thread reused across iterations (default)
thread:
mode: new()
memo: false # Fresh thread each iterationGroups
Groups organize inputs into named collections, enabling preset reuse across different workflow contexts.
Group Structure
| Field | Type | Description |
|---|---|---|
description | string | Human-readable description |
tag | string | Tag for preset matching (e.g., agent) |
inputs | map | Input definitions for this group |
Example
groups:
Implementer:
tag: agent
description: Settings for implementer agents
inputs:
model:
type: model
default: ""
description: Model for implementers
temperature:
type: number
default: 1.0
min: 0
max: 2
max_turns:
type: integer
default: 100
Reviewer:
tag: agent
description: Settings for the reviewer agent
inputs:
model:
type: model
default: ""
temperature:
type: number
default: 0.5Accessing Grouped Inputs
Grouped inputs are accessed as nested objects in CEL expressions:
# Access grouped input
model: "{{inputs.Implementer.model}}"
temperature: "{{inputs.Reviewer.temperature}}"
# Fallback to workflow default
model: "{{inputs.Implementer.model != '' ? inputs.Implementer.model : inputs.model}}"Tag-Based Preset Matching
Groups with the same tag share preset options. A preset with tag: agent can apply to:
- Top-level inputs (if workflow has
tag: agent) Implementergroup (if it hastag: agent)Reviewergroup (if it hastag: agent)
Validation Rules
- Group names must be valid identifiers (alphanumeric + underscore)
- Group names cannot conflict with top-level input names
- All grouped inputs must have either
required: trueor a default value
CEL Expression Reference
CEL (Common Expression Language) expressions are used throughout workflows for dynamic values.
Syntax
Expressions are wrapped in {{...}}:
content: "{{nodes.call_llm.message.text}}"
condition: "{{inputs.mode == 'manual'}}"Available Namespaces
| Namespace | Description | Example |
|---|---|---|
inputs.* | Workflow input values | inputs.model, inputs.temperature |
nodes.<id>.* | Outputs from completed nodes | nodes.call_llm.tool_calls |
workflow.* | Workflow metadata | workflow.id, workflow.name |
thread.* | Thread information | thread.id, thread.message.content |
trigger.* | Trigger event data | trigger.message.content |
output.* | Current node output (save_message only) | output.message.text |
iter.* | Loop iteration context (loops only) | iter.iteration, iter.max |
outputs.* | Sub-workflow outputs (loop until only) | outputs.exit_code |
Common Functions
| Function | Description | Example |
|---|---|---|
size() | Length of array/string | size(nodes.call_llm.tool_calls) > 0 |
has() | Check if field exists | has(inputs.model) && inputs.model != '' |
Conditional Expressions
# Ternary operator
model: "{{inputs.override_model != '' ? inputs.override_model : inputs.model}}"
# Boolean logic
condition: "{{nodes.call_llm.tool_calls != null && size(nodes.call_llm.tool_calls) > 0}}"
# Equality
condition: "{{inputs.mode == 'manual'}}"String Interpolation
Multi-value templates in content fields:
content: |
Task: {{trigger.message.content}}
Implementation: {{nodes.impl.response_text}}Outputs
Workflow outputs define values available to parent workflows after completion.
Structure
outputs:
message: "{{nodes.agent_loop.message}}"
response_text: "{{nodes.agent_loop.response_text}}"
iterations: "{{nodes.agent_loop.iterations}}"
succeeded: "{{nodes.agent_loop.succeeded}}"Validation Rules
- Must use
nodes.<id>.<field>format to reference node outputs - Referenced nodes must exist in the workflow
- Referenced fields must be declared in the child workflow’s outputs (for workflow nodes)
Accessing Outputs
Parent workflows access child outputs via nodes.<node_id>.<output_name>:
# Child workflow outputs
outputs:
summary: "{{nodes.summarize.text}}"
# Parent workflow uses them
- id: final_save
action: SaveMessage
args:
content: "{{nodes.child_workflow.summary}}"Complete Example
A workflow demonstrating multiple features:
name: parallel-compete
version: v0.0.11
description: Three agents compete to implement task, reviewer picks winner
status: published
tag: orchestration
entry: improve_prompt
inputs:
mode:
type: enum
enum: ["manual", "agent", "plan"]
default: "agent"
description: Execution mode
model:
type: model
default: ""
description: Default LLM model
groups:
Implementer:
tag: agent
description: Settings for implementer agents
inputs:
model:
type: model
default: ""
temperature:
type: number
default: 1.0
min: 0
max: 2
Reviewer:
tag: agent
description: Settings for reviewer agent
inputs:
model:
type: model
default: ""
temperature:
type: number
default: 1.0
nodes:
- id: improve_prompt
action: CallLLM
thread:
mode: new()
args:
thread: "{{thread.id}}"
model: "{{inputs.model}}"
tools: false
messages:
- role: user
content: |
Improve this task into a clear specification:
{{trigger.message.content}}
- id: impl_1
workflow: builtin://agent
thread:
mode: new()
key: impl_1
inject:
role: user
content: |
You are Implementation Candidate #1.
Task: {{nodes.improve_prompt.message.text}}
args:
mode: "{{inputs.mode}}"
model: "{{inputs.Implementer.model != '' ? inputs.Implementer.model : inputs.model}}"
temperature: "{{inputs.Implementer.temperature}}"
- id: impl_2
workflow: builtin://agent
thread:
mode: new()
key: impl_2
inject:
role: user
content: |
You are Implementation Candidate #2.
Task: {{nodes.improve_prompt.message.text}}
args:
mode: "{{inputs.mode}}"
model: "{{inputs.Implementer.model != '' ? inputs.Implementer.model : inputs.model}}"
- id: implementations_done
join: all
- id: review
workflow: builtin://agent
thread:
mode: fork
key: reviewer
inject:
role: user
content: |
Review both implementations and pick the winner.
args:
mode: "{{inputs.mode}}"
model: "{{inputs.Reviewer.model != '' ? inputs.Reviewer.model : inputs.model}}"
edges:
- from: improve_prompt
cases:
- to: impl_1
label: "start_impl_1"
- to: impl_2
label: "start_impl_2"
- from: impl_1
cases:
- to: implementations_done
- from: impl_2
cases:
- to: implementations_done
- from: implementations_done
cases:
- to: review
ui:
positions:
improve_prompt: { x: 250, y: 200 }
impl_1: { x: 500, y: 100 }
impl_2: { x: 500, y: 300 }
implementations_done: { x: 750, y: 200 }
review: { x: 1000, y: 200 }Validation Summary
Workflows are validated at parse time. Common validation errors:
| Error | Cause | Fix |
|---|---|---|
workflow name is required | Missing name field | Add name: my-workflow |
entry field is required | No entry point defined | Add entry: start_node |
step ID is required | Node missing id | Add id: my_step |
duplicate step ID | Same ID used twice | Use unique IDs |
must have one of: run, agent, action... | Node has no type | Add action:, workflow:, etc. |
can only have one of: run, agent... | Node has multiple types | Keep only one type |
references non-existent source step | Edge from invalid | Check step ID spelling |
references non-existent target step | Edge to invalid | Check step ID spelling |
must be either required or have a default | Input without required/default | Add required: true or default: value |
from: started is no longer supported | Using deprecated pattern | Use entry: field instead |