Human-authored plans never replan
When you run a plan — viaplan run or as a plan__* tool — you asked for that plan. If a step fails, rewriting your workflow behind your back would be worse than failing, so graph doesn’t. You get a structured, attributable error:
plan run: non-zero exit, message on stderr.- As a plan tool: an
is_errortool result — the agent explains it or works around it visibly (you’ll see any fallback tool calls in the trace).
plan_and_execute replans
Plans authored by the LLM planner are drafts. Defects trigger a replan: the error — including, for bad data references, a digest of the keys that actually exist — feeds back to the planner, which revises the plan. Executed steps are preserved and never re-run; only the broken tail is replaced. After the configured attempts (planning_attempts, default 2), it degrades to an honest error summary instead of raw failure.
One idempotency caveat: “executed steps are preserved” is keyed on top-level step results. Steps inside a decide branch or a map/reduce body are scoped to that step, so a control step that fails partway through counts as not executed — a replan that re-authors it re-runs the branch from its first step (or the map from its first item), duplicating any side effects the successful prefix already had. Keep bodies idempotent, or move side-effecting sequences into a plan__* call the body invokes. The same applies to an agent step, and more sharply: its rounds are not top-level results either, so a replan re-runs the whole loop — and because the calls were chosen by the model rather than written in the plan, you cannot tell from the YAML what will be repeated. Give agents read-only tool sets unless you have a reason not to.
The three failure classes
Template resolution errors are typed (details), and each class routes differently:
The
EmptyData distinction is the one that matters day-to-day: a search returning zero results is an answer (“no active cycle”), not a defect. Solver-mode plans report it gracefully; automation-mode plans surface it as empty_data: true so pipelines can branch on it.
Where validation happens
- Load time (automatically on every load, everywhere): YAML shape, template syntax, forward references (
E0referencingE2), id format, control-step inputs (adecidegate’s arity, amap’s body, anagent’soutput_schemaand budget — inside bodies too), malformed tool names — includingworkbench__*, which is rejected outright: those tools exist only inside the workbench TUI’s chat agent, never in the plan runtime, so a step naming one is always a defect. This layer is static and context-free. - Catalog resolution (
plan validate,plan run, and workbench validation and runs — anywhere the full config context exists): every step tool name, control-step bodies included, is resolved against what is actually loadable —builtin__*against the enabled packs,user__*against[tools].paths,plan__*against the plan catalog (followed transitively through composed plans). Anagentstep’stools:patterns are resolved the same way, wildcards included —linear__*requires[mcp.linear],user__*must match at least one loadable user tool — so a plan that names tools which cannot load fails here rather than mid-agent. MCPserver__toolnames are verified at the server level — the server must be configured under[mcp.*]— because listing a server’s real tools would mean connecting, which validation never does; the individual tool name is still checked at dispatch. An unresolvable tool fails the run before any step executes, so no tool calls or LLM money are spent on a plan that cannot finish. The one soft case: a server that is unconfigured but declared inrequires_serversis a note, not an error, when validating a plan file — the file is portable and its dependency is declared — but the plan is hidden from the catalog and running it by name reports the missing servers. - Input time: JSON Schema validation of inputs, before any step runs.
- Run time: everything that depends on real data — path existence against actual results, tool errors, emptiness, and MCP tool existence within a verified server.
plan_and_execute) skip catalog resolution: the planner draws from the live registry, and execution plus replanning already handle its misfires.