ask step requests a value only a human can supply — a choice between options, a confirmation before something irreversible, a detail no tool exposes — and binds the answer to the step id like any other result.
infer gate or an agent step.
Input
Field names are snake_case in a plan file, like every other step. The camelCase spellings (
outputSchema, whenUnanswered) still load, and any authoring command that rewrites the file normalizes them.
Result
{{E0.answer.channel}}. answered is false when the fallback was used, and a reason field then says which of the two ways it got there:
"declined"— a person saw the question and chose not to answer."unavailable"— nobody could be asked at all.
exit gate or a decide step:
When nobody can answer
This is the field that matters, and the reasonask is safe to use in a plan you also run in CI.
Whether a human is reachable is a property of where the plan runs, not of the plan:
A plan that only works on the first four is not portable, and portability is most of why a plan beats a prose runbook. So the unattended behaviour is declared in the plan, statically visible, and reviewable — never inferred:
when_unanswered: fail(the default) — the step fails and the run stops. The right choice when proceeding without the answer would be wrong.when_unanswered: default— fall back todefaultand carry on, withanswered: falserecording that it happened.
fail is the default deliberately: an author who never considered the headless case finds out the first time it runs there, instead of a plan quietly proceeding on a value nobody supplied.
A default is checked against output_schema at load time when it contains no templates, and after rendering when it does — a template’s type isn’t knowable until it resolves.
Answer schemas
output_schema must be a flat object whose properties are primitives (string, number, integer, boolean) or enums. No nested objects, no arrays.
This is MCP’s elicitation/create constraint, and graph enforces it for every host, not just MCP. A schema only a raw-JSON prompt could satisfy would make the plan silently unusable from an MCP client — exactly the host-specific trap when_unanswered exists to close. Nested data belongs in a tool result, not in a form a person fills in one field at a time.
Two practical consequences:
- Describe every property. The
descriptionis the field label the user sees. - Prefer an enum over free text when the options are known. Terminals render it as a choice list and clients render it as a picker, and neither can then produce a value the plan has to defend against.
output_schema before it becomes a result, whatever host produced it — a hand-typed terminal answer and a client’s form submission are equally untrusted. A non-conforming answer fails the step rather than propagating.
Where ask can appear
Top level, and inside a decide branch or a map/reduce body, where the question can reference {{item}}, {{index}}, and {{accumulator}}:
map sets concurrency above 1, because two prompts at once are unanswerable. That makes an ask inside a concurrent map a bottleneck by construction: prefer asking once about the whole list.
Like every control step, ask never reaches a tool registry and is never seen by an execution gate — it makes no tool call. Its result still counts toward steps_executed.
The terminal prompt
graph plan run walks the schema field by field on stderr, so stdout still carries only the deliverable:
3 becomes a number for an integer field and the string "3" for a string one), y/yes/true and n/no/false both work for booleans, and enum values match case-insensitively. A value that can’t be coerced is re-prompted up to three times, then the question is declined. A blank line on a required field declines immediately.
There is no prompt at all when stdin or stderr is not a terminal, or when GRAPH_EVENTS=jsonl has claimed stderr for machine-readable events. In those cases the ask is unavailable, which is the honest input to when_unanswered.
From an MCP client
graph mcp serve turns an ask into an elicitation/create request back to the client, issued from inside the tools/call that is running the plan.
It is capability-gated: a client that didn’t advertise elicitation at initialize time is never sent a request, and the ask resolves as unavailable immediately. A client that advertises support and then errors or times out is also unavailable — an unanswerable question is a plan-declared condition, not a server failure. decline and cancel both come back as "declined".