Skip to main content
output={outputs.someKey} is the canonical schema path. outputs comes from the same createSmithers(...) call as the workflow, and each value is the exact Zod schema object you registered. When a Task renders, Smithers uses that object as the output target, infers outputSchema, passes it to native structured-output agents, validates the returned row, and persists it to the matching output table. You only need outputSchema={...} when output is a custom Drizzle table or string key and Smithers cannot infer the Zod schema.
Do not name an output field iteration, runId, or nodeId. Smithers persists every node output alongside those internal columns, so an output schema that declares one collides and the workflow fails to load (bunx smithers-orchestrator graph errors before it runs). This bites loop counters especially; name the field round, pass, or attempt instead of iteration.

Three task modes: agent, compute, static

The children shape decides what a Task does. No agent prop is needed for the compute and static modes.
  • Agent (agent={...}, children is a prompt string or (deps) => string): runs the agent, parses its output against the schema, persists.
  • Compute (no agent, children is a function returning a value): runs your code. The function may be async and is awaited, so do real work here (run a test command, call an API, read a file). Whatever it returns is validated against output and persisted, exactly like an agent’s parsed JSON.
  • Static (no agent, children is a literal value): persists the value as-is.
A compute or static Task is a first-class node: it persists, resumes, gates downstream deps, and works inside <Loop>, <Branch>, <Parallel>, and <Sequence> just like an agent Task. The only thing it cannot do is be a fork source (no agent session). Throwing inside a compute function fails the task and triggers its retry policy.

Dependencies: dependsOn, needs, deps

Three props express upstream dependencies. They compose.
  • dependsOn: string[]: ordering only. The task waits until every listed task id is terminal. No values are passed in.
  • needs: Record<string, string>: named dependencies. Each value is an upstream task id; each key is the name that value is looked up under.
  • deps: Record<string, OutputTarget>: typed render-time outputs. The task gates on each dependency and passes the resolved rows to a children callback via {(deps) => ...}.
  • depsOptional: boolean: tolerate unresolved deps. By default a task with deps defers until every dependency has an output row. With depsOptional, the task renders as soon as it can, and any dependency that has no row (for example an upstream continueOnFail task that failed) is simply absent from the deps object. Read those values defensively: deps.summary?.text, because an unresolved dep is missing, not present-but-undefined.
The detail that bites people: a deps key is treated as the upstream task’s id. deps={{ analyze: outputs.analysis }} depends on a task whose id is analyze. If your dep name differs from the upstream id, remap it with needs:
Without the needs remap, deps={{ summary: ... }} depends on a node id summary that no task produces. The dependency can never resolve, and the run fails with DEPENDENCY_DEADLOCK naming the stuck task. (Previously this hung or finished while silently skipping the task.) needs alone works too when you don’t need the typed children callback.

Across a loop boundary

A task inside a <Loop> can depend on a task outside the loop. The upstream is resolved at its own iteration, not the loop’s, so deps/needs reach it from any iteration:

Fork

Every agent task produces a reusable session snapshot. Use fork to start a new task from any previous task’s context. <Task id={B} fork={A}> means:
  • B depends on A and cannot run until A has completed.
  • B starts from a copy of A’s final agent session context, then submits its own prompt into that copy.
  • B produces its own output and its own session snapshot. A is never mutated.
fork is immutable. It does not continue or mutate the source task; it copies the conversation into a fresh, independent session. Multiple tasks may fork the same source safely, and a forked task may itself be forked.
VERIFY forks IMPLEMENT, which forked PLAN, so VERIFY sees the whole plan → implement conversation. Parallel branches: fork the same source from sibling tasks; each gets its own copy and they never affect each other:
fork composes with dependsOn, needs, deps, Sequence, Parallel, Branch, and Loop. Inside a loop, fork resolves to the latest completed session snapshot for that task id; there is no iteration selector and no ambiguity.

Error cases

  • TASK_FORK_SOURCE_NOT_FOUND: fork points to a task id not present in the graph (including a source that exists only in an unselected <Branch>).
  • TASK_FORK_CYCLE: fork creates a cycle, directly or indirectly.
  • TASK_FORK_SESSION_UNAVAILABLE: the forking task is not an agent task, or the source completed but produced no usable session snapshot (e.g. a compute/static source, or a source that was skipped/cancelled).
  • TASK_FORK_SOURCE_NOT_COMPLETE: the source exists but has not completed; the forked task waits and does not run.

Notes

  • Three modes by children shape: agent (with agent), compute (function, no agent), static (value, no agent).
  • memory overrides the nearest <Memory> provider for this task. Recall and primer failures log a warning and run the original prompt.
  • fork requires an agent task; the source must be an agent task with a session snapshot. Forking copies the conversation into a new session and never reuses a native session id.
  • When outputSchema is set, JSON is extracted from agent text; schema-validation retries don’t consume retries.
  • Auth errors short-circuit retries; non-idempotent tool reuse warns on the next attempt.