docs: agent nodes can seed a child graph agent's state with inputs

Document the inputs map on agent nodes: overlay order over the child's
initial_state, the lone-reference typing rule and the null footgun, the
two load-time/runtime rejections, cross-branch read coverage, and the
fan-out narrowing example.
Dark-Alex-17 committed 2026-09-10 12:48:51 -06:00
1 parent 846c634cb5
commit b57aa2c2db
1 file changed
+116 -3
+116 -3
@@ -200,7 +200,9 @@ parse_input:
...
```
You do not need to (and should not) put `initial_prompt` in `initial_state` as it is overwritten by the dispatcher.
You do not need to (and should not) put `initial_prompt` in `initial_state` as it is overwritten by the dispatcher,
and `inputs.initial_prompt` on a parent's agent node is rejected (see
[Passing state into a child graph agent](#passing-state-into-a-child-graph-agent-inputs)).
---
@@ -242,6 +244,8 @@ research_topic:
agent: deep-researcher # name of an existing Coyote agent
prompt: "Research {{topic}}" # interpolated against state
timeout: 600 # optional, in seconds (default 300)
inputs: # optional; only when the child is a GRAPH agent
max_parallel: "{{inner_width}}" # seeds the child's initial_state (see below)
teammates: false # optional; peer messaging with concurrent siblings (see below)
state_updates:
findings: "{{output}}"
@@ -257,12 +261,111 @@ research_topic:
whole graph fails (no built-in fallback path on agent nodes).
- **`state_updates`:** Map of `state_key: "{{template}}"`. The child agent's
final text is available inside this map as `{{output}}`.
- **`inputs`:** Map of `child_state_key: "{{template}}"`. Each value is
interpolated against the **parent's** state and the resolved map is overlaid
on the child graph agent's `initial_state` before its first node runs. Only
valid when `agent` names a graph agent (one with a `graph.yaml`). See
[Passing state into a child graph agent](#passing-state-into-a-child-graph-agent-inputs).
- **`teammates`:** Opt-in peer messaging. When `true` and this node runs
concurrently with other flagged agent nodes (as branches of one `map`
fan-out, or alongside other `teammates: true` agent nodes in the same
super-step), the sibling sub-agents can message each other during the
run. See [Teammate messaging in fan-outs](#teammate-messaging-in-fan-outs).
### Passing state into a child graph agent (inputs)
`prompt:` is the only thing an agent node hands to a config-only child. When
the child is itself a **graph agent**, `inputs:` lets the parent seed any key
of the child's `initial_state` as well:
```yaml
research:
type: agent
agent: deep-research
prompt: "{{finding}}"
inputs:
max_parallel: "{{inner_width}}" # lone {{key}}: the child gets the value as-is
local_context: "Repo: {{repo}}" # mixed text: the child gets a string
state_updates:
verdict: "{{output}}"
```
**Overlay order.** The child's state is seeded in three layers, later layers
winning:
1. the child's own `initial_state` from its `graph.yaml`;
2. the resolved `inputs` map (keys the child never declared are simply added);
3. `initial_prompt`, which the dispatcher always sets from `prompt:`.
Nothing is validated against the child's YAML, and nothing about `inputs`
persists past the child's executor start: the child sees an ordinary
`initial_state` and runs exactly as if those values had been written in its
own file.
**Typing rule.** Every value is a template against the *parent's* state,
resolved strictly (a missing key fails the node before the child starts, with
`Failed to interpolate inputs.<key> for agent '<agent>'`):
- A value that is exactly one reference (`"{{inner_width}}"`) hands the
parent's JSON value through unchanged: numbers stay numbers, lists stay
lists, objects stay objects. Scripts in the child see the real type;
the child's own templates stringify it the usual way.
- Anything else (`"Repo: {{repo}}"`, `"{{a}}-{{b}}"`) renders to a string.
- **`null` is a value, not an absence.** A lone `"{{k}}"` whose parent value
is `null` overlays the child's default with `null`. Because `state_updates`
cleanup leaves `output: null` behind on the parent, `inputs: {x: "{{output}}"}`
after another node's `state_updates` is a footgun: the child gets
`x: null`, not its own default.
**Two things are rejected**, both at load time by the validator and again at
runtime for graphs running with `validate_before_run: false`:
- `inputs:` on a node whose `agent` has a `config.yaml` but no `graph.yaml`
(`agent node '<id>': `inputs:` requires agent '<name>' to be a graph agent
(no graph.yaml found)`). Presence is what matters: `inputs: {}` on a
config-only agent is still an error, while on a graph agent it is a no-op.
- An input named `initial_prompt` (`agent node '<id>': `inputs.initial_prompt`
is reserved (the dispatcher seeds it from `prompt:`)`). The dispatcher owns
that key; put the text in `prompt:`.
`inputs` values are templated reads like any other, so the
[cross-branch read check](#cross-branch-reads-are-not-visible-gotcha) covers
them: an `inputs` value that references a key a sibling branch writes in the
same super-step is a load-time error.
**Example: narrowing a child's fan-out.** `deep-research` fans out 4-wide
inside each of its runs. An orchestrator that maps over 4 findings and
delegates each to `deep-research` therefore runs 16 concurrent LLM calls,
and without `inputs` nothing on the parent side can change that. With a
templated cap in the child:
```yaml
# child: deep-research/graph.yaml
initial_state:
max_parallel: 4 # the default when run standalone
nodes:
fan_out:
type: map
over: "{{questions}}"
max_concurrency: "{{max_parallel}}"
...
```
the parent can size it per run:
```yaml
# parent
initial_state:
inner_width: 2
nodes:
research:
type: agent
agent: deep-research
prompt: "{{finding}}"
inputs:
max_parallel: "{{inner_width}}" # 4 items × 2-wide = 8 concurrent, not 16
```
---
## script
@@ -1182,6 +1285,16 @@ contain another `map`, or end in an `end` node; see
[Branch subgraph rules](#branch-subgraph-rules) and
[Retrying inside a branch](#retrying-inside-a-branch-with-_next).
**Narrowing a child's fan-out.** When a branch delegates to a graph agent
that has its own `map`, the fan-outs multiply: 4 items × a 4-wide child is
16 concurrent sub-agents. A templated `max_concurrency` sizes *this* graph's
fan-out at run time; to size the child's, give the child a templated cap
(`max_concurrency: "{{max_parallel}}"` with a default in its
`initial_state`) and pass the width from the branch's agent node with
`inputs: {max_parallel: "{{inner_width}}"}`. A lone `{{key}}` input keeps
the parent's number a number, so the child's cap resolves to an integer.
See [Passing state into a child graph agent](#passing-state-into-a-child-graph-agent-inputs).
## Teammate messaging in fan-outs
By default, concurrent sub-agents in a fan-out are mutually invisible:
@@ -1440,8 +1553,8 @@ super-step (or remove the cross-branch reference).
**Caveat (scripts).** The validator scans templated fields of typed
nodes (`prompt`, `query`, `instructions`, `question`, `default`,
`output`, `over`, and `state_updates` values). It does not scan
script bodies, since scripts read state opaquely via `GRAPH_STATE` /
`output`, `over`, `state_updates` values, and agent-node `inputs` values).
It does not scan script bodies, since scripts read state opaquely via `GRAPH_STATE` /
`GRAPH_STATE_FILE` env vars. A Python script in a parallel branch
that does `state.get("sibling_written_key")` will silently see the
**pre**-super-step snapshot. The script's required `state_updates`