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.