Authoring in YAML¶
A graph is data. YAML is its native form — what a workflow package stores on disk, what the Architect writes, and what you edit by hand. Everything the Python API can express, YAML can express.
name: content_pipeline
description: Research a topic, then explain it.
inputs:
- name: topic
type: string
required: true
description: What to research.
nodes:
- id: researcher
kind: base
instructions: Research {topic} and produce exactly 5 key bullet points.
- id: writer
kind: base
instructions: Write a clear, 2-paragraph explanation from the research notes above.
depends_on: [researcher]
outputs: [writer]
Load it with load_graph, or let a package carry it:
Top-level keys¶
| Key | Required | Means |
|---|---|---|
name | yes | The graph's identifier. |
nodes | yes | The list of nodes. Order does not matter — depends_on defines the DAG. |
description | no | What the workflow is for. |
inputs | no | Declared inputs. See below. |
outputs | no | Node ids whose outputs the graph returns. Superseded by an output node if one exists. |
functions | no | A Python sidecar file, resolved relative to this file. |
fail_fast | no | Stop the whole run at the first node failure instead of continuing down live branches. |
strict_inputs | no | Refuse a run that supplies a key no input declares. |
Declaring inputs¶
inputs:
- name: topic
type: string
required: true
description: What to research.
- name: max_items
type: integer
required: false
type is string, integer, number, boolean, array, or object.
A declared input that no step names is reported by the validator — the run would otherwise go green while the model answers as though it had been passed nothing. Interpolate it somewhere, or drop the declaration.
Node keys¶
Every node takes id (required) and kind (defaults to base). The rest depends on the kind — see Node kinds for which fields each one actually uses.
Instructions¶
instructions is the current field. The older trio is still read when instructions is absent, which is why you will see it in existing packages:
purpose: You are a careful technical writer.
goal: Write a 2-paragraph explanation of {topic}.
expected_result: Two paragraphs, no bullet points.
Prefer instructions in new graphs.
Wiring¶
depends_on: [researcher, scout] # waits for both; receives both outputs
when: "len(nodes.scout) > 0" # run only if truthy
on_error: fallback_node # reroute on failure
writes: draft # store output as {draft} / vars.draft
policy:
retries: 2
Tools¶
- id: fetch
kind: tool
tools: [http]
tool_args:
url: "{source_url}"
method: GET
tool_settings:
http:
timeout: 30
secrets: [API_TOKEN]
tool_args are the call arguments; tool_settings are the author-configured settings keyed by tool name. A ${NAME} in tool_args resolves only if the node declared NAME in secrets — see State & secrets.
Structured output¶
- id: extract
kind: base
instructions: Extract the invoice fields from {document}.
output_schema: my_package.models:Invoice
mode: json
output_schema is an import path, module:Attr, resolved and checked by validation. Without one, a node asked for an object returns JSON as a string.
Per-node model¶
Omit both to use the run's provider.
Control flow in YAML¶
Router¶
- id: triage
kind: router
instructions: "Route this support ticket by urgency: {ticket}"
routes:
urgent: escalate
routine: reply
default: reply
repair: true
- id: escalate
kind: base
depends_on: [triage] # every target MUST depend on the router
instructions: Draft an escalation for {ticket}.
Deterministic form, no LLM call:
- id: gate
kind: router
depends_on: [check]
cases:
- when: "contains(lower(nodes.check), 'yes')"
to: approve
default: reject
Declare routes or cases, never both.
Loop¶
functions: helpers.py
nodes:
- id: polish
kind: loop
max_iterations: 4
until: tagline_is_short # a name helpers.py defines
body:
- id: draft
kind: base
instructions: "Write a tagline. {feedback}"
until is either a function name from the sidecar or plain English. Which one it is, is a lookup — see Python in a graph.
Map¶
- id: per_item
kind: map
over: inputs.items
item_var: item
concurrency: 4
body:
- id: handle
kind: base
instructions: "Process one item: {item}"
Nested bodies¶
loop, map and subgraph take a body: — a full list of nodes with the same schema, nested. Body nodes may only depend on their siblings; a dependency pointing outside the body is refused when the graph loads.
Two YAML details that bite¶
Braces need quoting. A value starting with { is a YAML flow mapping, not a string:
instructions: {topic} summary # ✗ YAML parse error
instructions: "{topic} summary" # ✓
instructions: Summary of {topic} # ✓ — brace is not first
Multi-line prose wants a block scalar. Use > to fold or | to keep newlines:
instructions: >
Research {topic} thoroughly and produce exactly five bullet
points, each one sentence long.
Round-tripping¶
YAML and the Python API produce the same Graph, and the loader validates both identically:
from neurosurfer.graph import load_graph_from_dict
load_graph_from_dict(graph.model_dump(mode="json")) # reproduces graph
That is deliberate: there is no second code path, so no class of bug reaches one form and not the other. See Building in Python.
Next¶
- Node kinds — every field, by kind.
- Python in a graph — the sidecar and what your functions receive.
- Control flow — the semantics behind the syntax above.
- Validation — what is checked when this file loads.