Reference: flow.yaml¶
The authoring-layer document. A flow.yaml describes how the app should
behave — teams, chains, gates — without spelling out every node and edge. It
compiles to a Flow and then to a regular Graph, and can be exported as the
compiled graph.yaml artifact:
flow.yaml ──compile()──► Flow ──compile()──► Graph
└── to_yaml() ──► graph.yaml
Choose flow.yaml when you want the concise idiom surface (team:, map:,
loop:); choose graph.yaml when you want every node and
arrow explicit. Both formats are auto-detected by the CLI and the loaders
(looks_like_flow), so teff run, teff validate and teff graph work on
either without any flag.
This page is the complete field reference. Read it top to bottom once, then use it to find where to change what: each key below names the exact YAML key, its type, its default, and which code path consumes it.
Top-level keys¶
| Key | Type | Default | Purpose |
|---|---|---|---|
name |
string | "" |
Optional label; used as the graph name when exporting. |
description |
string | — | Optional free-text description. |
default_provider |
string | — | Provider name every LLM/agent step falls back to when it has no own provider:. Must be declared in providers:. |
default_model |
string | — | Model name every LLM/agent step falls back to when it has no own model:. A step's own model: always wins. |
providers |
list | [] |
Declared provider endpoints (see providers). |
tools |
list | [] |
Tool instances available to agents (see tools). |
state |
object | {} |
{schema, initial} — initial values and per-key reducers (see state). |
steps |
list | — | Required. The idiom steps (see below). Empty → ConfigError. |
plugins |
string/list | — | Extra node/tool modules to import (paths). |
plugins_folder |
string | "plugins" |
Auto-loaded plugin folder. |
checkpoint |
object | — | Durable-runs block (see checkpoint). |
hooks |
object | — | Named hook callbacks (see hooks). |
observability |
object | — | Full-run tracing block (see observability). |
include |
string/list | — | Merge steps/edges/tools/state from other files. Each entry is a path or {path, prefix}. Relative to the including file. |
Every value in the document is interpolated against the process environment —
${ENV_VAR} references are replaced; a variable that is not set stays as a
literal placeholder.
The state:, tools:, providers:, plugins:, checkpoint:, hooks:,
observability: and include: blocks are pass-through — they behave
exactly as they do in graph.yaml.
steps: — the idiom surface¶
Every entry in steps: is a single-key mapping: the key is the idiom name,
the value is a mapping of that idiom's options:
steps:
- llm: {id: replier, system: "...", output_key: answer}
- transform: {id: shout, action: uppercase, input_key: answer, output_key: shout}
The full idiom set:
| Idiom | What it builds | See |
|---|---|---|
llm: |
One model call | llm |
transform: |
String/data transform | transform |
context_builder: |
Compose a plain-text agent input from state | context_builder |
append_assistant: |
Append a reply back to the conversation | append_assistant |
agent: / agent_step: |
One routed ReAct agent (context builder → harness → append) | agent |
team: |
Supervised team: leader decider + routed roles | team |
supervisor: |
Native supervisor decider (with optional agents: loop wiring) |
supervisor |
supervise: |
Route an existing decider to agent chains (advanced) | supervise |
parallel: |
Concurrent branches + optional converge | parallel |
map: |
Dynamic fan-out over a state list | map |
loop: |
Repeat a body until a state value matches | loop |
interrupt: |
Pause for human input, optional validation | interrupt |
branch: |
Conditional routing on a state value | branch |
route: |
Declarative when/goto routing from state |
route |
type: |
An arbitrary registered node type as one step | type |
An unknown idiom raises ConfigError listing the supported set. LLM-bearing
idioms need a model:/provider: — per step, or via default_model: /
default_provider: at the top (otherwise ConfigError).
llm:¶
Compiles to an LLM node. All LLM constructor keys are
accepted verbatim.
- llm:
id: replier
system: "You are a helpful assistant."
prompt: "User said: {input}"
output_key: answer
model: llama3.1:8b # optional if default_model set
provider: ollama # optional if default_provider set
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | llm_N |
Node id in the compiled graph. |
model |
string | default_model |
Required when no default_model. |
provider |
string | default_provider |
Required when no default_provider. |
system |
string | — | System prompt; supports {key} templates. |
prompt |
string | — | User prompt with {key} templates. |
input_key |
string | — | Read a single state key as the user message. |
output_key |
string | "output" |
Where the reply lands. |
json_schema / output_type |
dict/type | — | Structured output validation. |
parse |
bool | false |
Parse the reply as JSON into a dict (no validation). |
use_tools |
bool/list | — | Tool scope for the model. |
skills / skill_dir |
list/str | — | Mount skills onto the call. |
temperature / max_tokens |
float/int | — | Sampling knobs. |
max_retries / fallbacks |
int/list | — | Retry + model failover. |
cache |
bool | false |
Dedupe identical calls. |
memory |
object | — | Long-term memory config. |
any other LLM key |
— | — | Forwarded to LLM(**config). |
transform:¶
Compiles to a Transform node. All Transform keys are
accepted verbatim.
- transform:
id: count
action: count_lines
input_key: text
output_key: lines
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | transform_N |
Node id. |
action |
string | — | One of the transform actions (uppercase, lowercase, trim, count_lines, value, render, json_get, append, contains, compare, split, join, replace, coalesce, pick, to_int, to_float, now). |
input_key |
string | "" |
State key to read from. |
output_key |
string | "" |
State key to write to. |
value |
string | — | Literal value (action: value), needle (contains), RHS (compare), coalesce fallback. |
field |
string | — | Field for json_get / pick. |
template |
string | — | {key} template for render / append. |
raw |
bool | false |
Keep json_get/pick values without stringifying. |
sep / op / old / new |
— | — | Action-specific options (see transform). |
context_builder:¶
Compiles to a ContextBuilder
node. Renders each configured section as <label>:\n<value> plus the latest
user message into a plain-text output_key, and clears scratch keys so a
routed agent starts clean. This is the first half of the agent_step turn
routing pair.
- context_builder:
id: compose
sections:
plan: "Plan"
summary: "Summary"
messages_key: messages
output_key: input
reset_keys: [scratch]
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | context_builder_N |
Node id. |
sections |
mapping | {} |
State key → section label, rendered as <label>:\n<value>. List values are joined with newlines. |
messages_key |
string | messages |
State key holding the conversation; its latest user message is appended as User: .... |
output_key |
string | input |
State key receiving the composed text. |
reset_keys |
list | [] |
Scratch state keys to clear (reset to []) before the agent runs. |
append_assistant:¶
Compiles to an AppendAssistant
node — the second half of the turn routing pair. Copies state[output_key]
back into the shared conversation as an assistant message.
- append_assistant: {output_key: poem, messages_key: messages}
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | append_assistant_N |
Node id. |
output_key |
string | draft |
State key holding the agent's reply. |
messages_key |
string | messages |
State key of the conversation to append to. |
agent: / agent_step:¶
Compiles to a SubFlow running the agent_step recipe:
context builder → ReAct harness → append assistant. One routed agent whose
final answer lands in output_key and is copied into the shared conversation.
agent: and agent_step: are aliases.
- agent:
id: coder
system: "You write code."
output_key: code
model: llama3.1:8b
provider: ollama
use_tools: [python_eval, read_file] # optional
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | agent-N |
Outer node id (the SubFlow). Inner nodes are agent-<id>/.... |
system |
string | "" |
System prompt for the agent. |
output_key |
string | id or "output" |
State key that receives the final answer. |
model |
string | default_model |
Required when no default_model. |
provider |
string | default_provider |
Required when no default_provider. |
sections |
map | {output_key: Capitalized} |
Shared state key → label rendered into the agent's context. |
messages_key |
string | "messages" |
State key holding the shared conversation. |
use_tools |
null/list/"all" |
null |
Tool scope for the agent. tools: is the legacy alias. |
stream |
bool | true |
Emit tokens as stream events. |
| any other key | — | — | Forwarded to the ReAct harness / ToolExec (e.g. max_tool_rounds, tool_error_mode). |
agent_step is also valid as a node-level step (see node-level steps).
team:¶
Compiles a supervised team in one step: a Supervisor
decider plus one routed agent per role, wired into the supervisor loop. The
programmatic twin of Flow.team(...).
- team:
id: lead # names the supervisor node
leader:
system: "Route to planner or coder, then finish."
model: llama3.1:8b
provider: ollama
roles:
planner: {system: "You plan.", output_key: plan}
coder: {system: "You code.", output_key: code, use_tools: [python_eval]}
fallback: planner
max_rounds: 6
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | supervisor |
Supervisor node id. |
leader |
mapping | — | Required. Decider options (below). |
roles |
mapping | — | Required, non-empty. Role name → role spec (below). |
fallback |
string | "" |
Role routed to when finish is picked before anything is produced. |
max_rounds |
int | 6 |
Force finish once the counter reaches it. |
finish |
step/list | — | Chain run when the decider replies finish. |
leader:¶
| Key | Type | Default | Notes |
|---|---|---|---|
system |
string | "" |
Decider system prompt (list the reply values + finish). |
model |
string | default_model |
Decider model. |
provider |
string | default_provider |
Decider provider. |
messages_key |
string | "messages" |
Shared conversation key. |
route_keys |
map | {role: output_key} |
Route value → output slot; a role whose slot already has content is not re-routed. |
done_keys |
list | [] |
When these slots are filled, return finish without a model call. |
done_mode |
string | "all" |
"all" (every done_keys filled) or "any". |
fallback |
string | — | Same as the team-level fallback:. |
max_rounds |
int | 6 |
Same as the team-level max_rounds:. |
roles.<name>: (role spec)¶
Each role is an AgentRole mapping — a dict
recipe accepted for YAML parity. A role may also be a plain node/list of
nodes for that route.
| Key | Type | Default | Notes |
|---|---|---|---|
system |
string | "" |
Role system prompt. |
output_key |
string | role name | State key receiving the role's final answer. |
model / provider |
string | team default | Per-role overrides. |
sections |
map | {output_key: Capitalized} |
Context sections. |
messages_key |
string | "messages" |
Shared conversation key. |
use_tools |
null/list/"all" |
null |
Tool scope. tools: accepted as legacy alias. |
stream |
bool | true |
Emit tokens as stream events. |
| any other key | — | — | Forwarded to the role's ReAct harness. |
supervisor:¶
Compiles a native Supervisor decider node — without
the team wrapper. With an agents: mapping plus finish:/key: it wires the
whole supervisor loop in one step (the team: shape without the sugar).
# Just the decider (pair with a `supervise:` step):
- supervisor:
id: lead
system: "Route to coder or finish."
model: llama3.1:8b
provider: ollama
route_keys: {coder: code}
done_keys: [code]
# Decider + loop wiring in one step:
- supervisor:
id: lead
system: "Route to coder or finish."
route_keys: {coder: code}
done_keys: [code]
agents:
coder: [agent_step: {id: coder, system: "You code.", output_key: code}]
finish:
- transform: {id: done, action: now, output_key: delivered_at}
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | supervisor_N |
Decider node id. |
system |
string | "" |
Decider system prompt. |
model / provider |
string | default | Decider model/provider. |
output_key |
string | "next_agent" |
State key receiving the chosen route. |
sections |
map | {} |
State key → label map rendered into the prompt. |
route_keys |
map | {} |
Route value → output slot; filled slots are not re-routed. |
done_keys / done_mode |
list/str | {} / "all" |
When filled → finish with no model call ("any" = just one). |
fallback |
string | "" |
Alias for fallback_agent: route to this agent when finish is picked early. |
rounds_key / max_rounds |
str/int | "supervisor_rounds" / 6 |
Force finish once the counter reaches max_rounds. |
messages_key |
string | "messages" |
Source of the user message; "" always consults the model. |
agents |
map | — | Route value → chain (node/list). Present → the full loop is wired. |
key |
string | output_key |
The route key for the loop (defaults to output_key when agents: present). |
finish |
step/list | — | Chain run on finish (requires agents:). |
supervise:¶
Routes an existing decider to agent chains — the low-level twin of the route
wiring team: does implicitly. Use it when the decider and its agent groups
are defined separately (full control over fill_order, sections, …). The
decider (the node added just before) writes key; each agents entry maps a
value of key to the chain run for it, after which control returns to the
decider. When key equals finish the loop exits through finish.
- supervisor: {id: lead, system: "Route to coder or finish.", route_keys: {coder: code}}
- supervise:
key: next_agent
agents:
coder: [agent_step: {id: coder, system: "You code.", output_key: code}]
finish:
- transform: {id: done, action: now, output_key: delivered_at}
| Key | Type | Default | Notes |
|---|---|---|---|
key |
string | — | Required. Route key written by the decider. |
agents |
map | — | Required, non-empty. Route value → chain (node/list). |
finish |
step/list | — | Chain run when key == "finish". Omit to terminate on finish. |
parallel:¶
Runs independent branches concurrently and merges their results via the state
reducers. Each branch is a node-level step or a list of them (run sequentially
within the branch). An optional converge: re-joins the branches.
- parallel:
id: fanout
branches:
- transform: {id: web, action: uppercase, input_key: q, output_key: web}
- [transform: {id: upper, action: uppercase, input_key: q, output_key: upper},
transform: {id: n, action: count_lines, input_key: q, output_key: n}]
converge:
transform: {id: join, action: value, value: done, output_key: status}
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | parallel |
Parallel node id. |
branches |
list | — | Required, non-empty. Branch chains (node-level step, or list of node-level steps). |
converge |
mapping | — | Single-key transform: step that merges branch ends. Only Transform is supported in this release. |
Branches run on isolated copies of the state; use
state.schema … reducer: append when branches should accumulate (e.g.
collecting messages) instead of overwriting a key.
map:¶
Dynamically fans a state list out across parallel branches — branch count
is derived from the data at runtime. Each item is passed to the processor
node, and the per-item results are gathered into a list at output_key.
- map:
id: summerize
processor:
llm: {model: llama3.1:8b, system: "Summarize the chunk.", input_key: chunk, output_key: summary}
input_keys: [chunks]
output_key: summaries
max_concurrency: 4
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | map_N |
Map node id. |
processor |
mapping | — | Required. A single-key node-level step (llm:, transform:, agent:, …). |
input_keys |
string/list | "" |
State list key(s); multiple are zipped per index. |
output_key |
string | "" |
State key receiving the list of per-item results. |
max_concurrency |
int | — | Cap on concurrent branches. |
chunk_size |
int | — | Items per branch (default 1). |
loop:¶
Repeats a body chain until state[key] equals until, then runs done
and continues after the loop. The condition uses the edges expression language
(so until: "да" matches "Да" or "да.").
- loop:
key: verdict
until: pass
max_rounds: 3
body:
- llm: {id: revise, system: "Rewrite based on feedback.", prompt: "{feedback}", output_key: draft}
done:
- transform: {id: final, action: now, output_key: delivered_at}
| Key | Type | Default | Notes |
|---|---|---|---|
key |
string | — | Required. State key the condition reads. |
until |
string | — | Required. Value of key that stops the loop. |
body |
list | — | Required. Chain repeated while the loop continues. |
done |
step/list | — | Required. Chain run when the loop terminates. |
max_rounds |
int | — | Maximum body re-runs before giving up (bounded-loop node form). Without it the loop is free-flow until the runtime gives up. |
label |
string | — | Route label attached to the loop decider, so a later route:/command: goto can jump back to the loop. |
interrupt:¶
Pauses the run for human input at this point. graph.run() raises
GraphInterrupt; resume with the same checkpoint_id and resume={key: answer}.
Requires a checkpointer.
- interrupt:
id: approve
key: decision
prompt: "Approve the report? (yes / no)"
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | interrupt_N |
Node id; also prefixes the classifier/validate ids ({id}-classifier, {id}-validate). |
key |
string | — | Required. State key receiving the resume value. |
prompt |
string | "" |
Question shown to the operator. |
strategy |
mapping | — | Validate the answer instead of comparing verbatim (below). |
strategy: — validated interrupts¶
Without strategy: the raw resume value is stored verbatim in key. With a
strategy the loader expands the interrupt into the classifier + validate
chain ({id}-classifier, {id}-validate) — the YAML counterpart of
flow.interrupt(key, prompt, accept=Ask(...)) — and the decision key
(decision by default) is written for edges to route on.
| Strategy | Value | Meaning |
|---|---|---|
equals: |
string | Exact match on the normalized answer. |
any_of: |
list | Answer must be one of the values. |
regex: |
string | Answer must match the pattern. |
llm: |
mapping | An LLM classifier normalizes free-form answers into a verdict (needs model/provider). |
Common strategy passthrough keys: decision_key ("decision"), pass_value
("да"), fail_value ("нет"), value_key, verdict_key ("verdict"),
ok_field ("ok"), clear_field, clarify_value, rounds_key,
max_rounds.
- interrupt:
id: gate
key: approved
prompt: "Approve the report? (yes / no)"
strategy: {equals: да} # or any_of: [да, ок] | regex: "^[A-Z0-9]{4}$"
- transform: {id: ship, action: value, value: shipped, output_key: status}
strategy:
llm:
system: Classify the answer as approval or rejection.
user: "Answer: {approved}"
schema:
type: object
properties: {ok: {type: boolean}}
model: llama3.1:8b
provider: ollama
Edges that would have sourced from the interrupt now source from
{id}-validate, where the decision key is written.
branch:¶
Conditional routing from the preceding node based on a state key. Each case
creates an edge key=<value>; an optional default catches unmatched values
(key!=<all case values>); an optional converge re-joins the branches.
- branch:
key: sentiment
cases:
- {value: positive, steps: [transform: {action: value, value: glad, output_key: reply}]}
- {value: negative, steps: [transform: {action: value, value: sorry, output_key: reply}]}
default:
transform: {action: value, value: neutral, output_key: reply}
converge:
transform: {action: uppercase, input_key: reply, output_key: result}
| Key | Type | Default | Notes |
|---|---|---|---|
key |
string | — | Required. State key to evaluate. |
cases |
list | — | Required, non-empty. Each case is {value, steps} where steps is a list of node-level steps chained for that case. |
default |
step/list | — | Chain run when key matches no case. A list is wrapped as a sub-flow. |
converge |
mapping | — | Single-key node step merging branch ends. |
route:¶
Declarative goto/STOP routing from state — a
CommandNode that routes the graph without code. The
when expressions use the same language as edges: conditions; the first
match wins.
- route:
id: route
routes:
- {when: "score >= 0.8", goto: approve}
- {when: "score < 0.3", goto: reject}
goto: STOP
update: {routed: true}
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | command_N |
Node id. |
key |
string | — | Required. State key whose value is evaluated (used by the when expressions). |
routes |
list | [] |
{when, goto} pairs; first match wins. goto may be a label or node id. |
goto |
string | — | Fallback target when no route matches; STOP ends the run. |
update |
mapping | — | State keys merged after routing. |
goto targets are resolved through label() labels,
so a route can jump back to a loop's decision point (pair with a loop: that
declares label:).
type:¶
Compile an arbitrary registered node type as one step — the escape hatch to any node the registry knows, without an idiom.
- type:
id: parse
type: csv
config:
path: data.csv
| Key | Type | Default | Notes |
|---|---|---|---|
id |
string | {type}_N |
Node id. |
type |
string | — | Required. Registered node type name. |
config |
mapping | {} |
Node config forwarded to the node's constructor. |
Node-level steps¶
Idioms inside containers (loop body/done, parallel branches, map
processor, branch cases) are given as inline single-key steps and support a
slightly smaller surface:
llm:, transform:, context_builder:, append_assistant:,
agent:/agent_step:, interrupt:, map:, type:
- loop:
body:
- llm: {model: llama3.1:8b, system: "Revise.", output_key: draft}
An interrupt with a strategy: used inside a container expands to the same
classifier + validate chain as at the top level.
A supervisor:/supervise:/team: step with its own agents: also embeds
node-level chains (e.g. agent_step:).
providers:¶
Exactly as in graph.yaml — the single source of truth for model endpoints.
See providers and the graph.yaml reference.
providers:
- name: ollama
type: ollama
base_url: http://localhost:11434
chat_path: /api/chat
- name: vllm
base_url: http://vllm:8000/v1 # type defaults to openai_compatible
| Key | Type | Default | Notes |
|---|---|---|---|
name |
string | — | Unique provider key referenced by provider:/default_provider:. |
type |
string | openai_compatible |
openai_compatible, anthropic_compatible, or ollama. |
base_url |
string | — | Endpoint base URL. |
chat_path |
string | — | Chat endpoint path override. |
api_key_env |
string | — | Env var holding the API key. |
auth_header / auth_prefix |
string | — | Custom auth header / prefix. |
timeout |
number | — | Request timeout. |
tools:¶
Exactly as in graph.yaml. Tool instances made available to agents (and
tool_call nodes). See tools for the full list.
tools:
- type: web_search
- type: python_eval
- type: mcp
config:
id: drive
command: [uvx, mcp-server-google-drive]
type: mcp is a special tool — it declares an MCP
server (streamable-http url: or stdio command:) whose tools are exposed
to agents as <id>__<tool>. The connection opens lazily on first use and
lives for the whole graph, so it is shared across daemon ticks and
conversation turns. Exactly one of url/command is required; optional
env, cwd and client_info are passed through. Run the flow with
teff run file.yaml (the CLI closes the connection when the run ends) or
wrap your own call in async with graph:.
For known servers, a preset: supplies the launch command and env keys;
your env: merges over the defaults:
tools:
- type: mcp
config:
preset: google_drive # npx: google_drive | gmail | google_calendar
env: {GOOGLE_DRIVE_REFRESH_TOKEN: "${GDRIVE_TOKEN}"}
- type: mcp
config:
preset: git # uvx (Python): git | fetch | time | sqlite
A preset config needs no url/command. Explicit command:/url:,
id: or extra env: keys override the preset (env merges key-by-key).
Google presets launch via npx and need Node.js; the uvx presets are
pure Python. A missing launcher fails the connection with a clear error.
state:¶
Exactly as in graph.yaml. initial: seeds the state; schema: declares
per-key reducers (override / append / keep).
state:
initial: {topic: "…", messages: []}
schema:
messages:
reducer: append
type: list
status:
reducer: keep
state.initial is validated against state.schema (ConfigError on
mismatch); keys without a reducer default to override.
checkpoint:¶
Exactly as in graph.yaml. Enables durable runs. See durable.
checkpoint:
type: sqlite # file | sqlite | sqlite_history | pg | pg_history
path: data/checkpoints.db
path resolves relative to the workflow file; PG variants require dsn:
(+ optional table:).
hooks:¶
Exactly as in graph.yaml — named hook callbacks registered in a plugin.
See yaml-workflows.md.
plugins: [plugins/telemetry.py]
hooks:
on_node_start: tick
on_node_end: [tick, finalize]
on_node_error: on_error
observability:¶
Exactly as in graph.yaml — full-run tracing without code. See
yaml-workflows.md.
observability:
db: ./data/traces.db
export:
- type: webhook
url: http://obs:8001/obs/ingest
Validate, compile, export¶
teff validate flow.yaml # flow-specific validation (auto-detected)
teff graph flow.yaml # compiled topology as YAML
teff graph flow.yaml --mermaid # compiled topology as Mermaid
teff -f flow.yaml # run it
from teff.flow.compiler import load_flow, compile_flow_file
graph, tools, initial, reducers = load_flow("flow.yaml")
graph2 = compile_flow_file("flow.yaml") # just the graph
from teff.flow.compiler import build_flow_to_yaml
text = build_flow_to_yaml("flow.yaml", output="graph.yaml") # compile artifact
The compiled graph.yaml round-trips through teff validate and the low-level
loaders — the authoring layer never produces anything a hand-written
graph.yaml could not express.
See also¶
graph.yamlreference — the low-level format, key by key.- Choosing your abstraction — when to use which layer.
FlowAPI — the programmatic twin of every idiom.teff.flow.compiler— the compiler itself.