---
title: <Task>
description: Executable node; runs an agent, compute callback, or emits a static value.
---

```ts
import { Task } from "smithers-orchestrator";

type TaskProps = {
  id: string;
  output: z.ZodObject | Table | string;
  outputSchema?: z.ZodObject; // inferred when output is a Zod schema
  agent?: AgentLike | AgentLike[]; // array = [primary, ...fallbacks]
  fallbackAgent?: AgentLike;
  dependsOn?: string[];
  needs?: Record<string, string>;
  deps?: Record<string, OutputTarget>; // typed render-time upstream outputs
  depsOptional?: boolean; // render even when some deps are unresolved (skip them)
  fork?: string; // start from another task's final agent session snapshot
  allowTools?: string[]; // CLI-agent tool allowlist
  key?: string;
  skipIf?: boolean;
  needsApproval?: boolean; // pause for human before executing
  async?: boolean; // with needsApproval: let unrelated flow continue
  timeoutMs?: number;
  retries?: number; // default Infinity with exponential backoff
  noRetry?: boolean;
  retryPolicy?: { backoff?: "fixed" | "linear" | "exponential"; initialDelayMs?: number };
  continueOnFail?: boolean;
  cache?: { by?: (ctx) => unknown; version?: string; key?: string; ttlMs?: number; scope?: "run" | "workflow" | "global" };
  label?: string;
  meta?: Record<string, unknown>;
  scorers?: ScorersMap;
  memory?: {
    recall?: { namespace?: string; query?: string; topK?: number };
    remember?: { namespace?: string; key?: string };
    threadId?: string;
  };
  heartbeatTimeoutMs?: number; // fail if no heartbeat in window
  heartbeatTimeout?: number; // alias of heartbeatTimeoutMs
  hijack?: boolean; // request an immediate hijack handoff as soon as the task starts running
  onHijackExit?: "complete" | "reopen"; // what Smithers should do after a hijacked session exits
  children?:
    | string
    | Row
    | (() => Row | Promise<Row>)
    | ReactNode
    | ((deps) => Row | ReactNode);
};
```

`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.

<Warning>
**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`.
</Warning>

```tsx
import { CodexAgent } from "smithers-orchestrator";

const analysisAgent = new CodexAgent({
  model: "gpt-5.6-terra",
  instructions: "You are a careful software analyst.",
});

const reviewAgent = new CodexAgent({
  model: "gpt-5.6-sol",
  instructions: "You are a senior code reviewer.",
});

<Task id="analyze" output={outputs.analysis} agent={analysisAgent}>
  {`Analyze: ${ctx.input.repoPath}`}
</Task>

<Task id="review" output={outputs.review} agent={reviewAgent} deps={{ analyze: outputs.analysis }}>
  {(deps) => `Review: ${deps.analyze.summary}`}
</Task>
```

## 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.

```tsx
// Compute task: an async function body runs real work, no agent involved.
<Task id="run-tests" output={outputs.testResult}>
  {async () => {
    const proc = Bun.spawn(["bun", "test"], { stdout: "pipe", stderr: "pipe" });
    const stdout = await new Response(proc.stdout).text();
    const code = await proc.exited;
    return { passed: code === 0, log: stdout.slice(-4000) };
  }}
</Task>

// Compute task reading an upstream output via deps; still just a function.
<Task id="count" output={outputs.count} deps={{ analyze: outputs.analysis }}>
  {async (deps) => ({ issues: deps.analyze.issues.length })}
</Task>

// Static task: a literal value, persisted as-is.
<Task id="config" output={outputs.config}>
  {{ region: "us-east-1", retries: 3 }}
</Task>
```

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`:

```tsx
// Upstream task id is "parse-summary", but you want to call it `summary` locally.
<Task id="parse-summary" output={outputs.summary}>{/* ... */}</Task>

<Task id="report" output={outputs.report}
  needs={{ summary: "parse-summary" }}   // map the key to the real task id
  deps={{ summary: outputs.summary }}>
  {(deps) => deps.summary.text}
</Task>
```

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:

```tsx
<Task id="config" output={outputs.config}>{/* runs once, outside the loop */}</Task>

<Loop id="work" until={done}>
  <Task id="step" output={outputs.step}
    needs={{ config: "config" }}
    deps={{ config: outputs.config }}>
    {(deps) => useRegion(deps.config.region)}
  </Task>
</Loop>
```

## 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.

```tsx
import { CodexAgent } from "smithers-orchestrator";

const planner = new CodexAgent({ model: "gpt-5.6-sol" });
const implementer = new CodexAgent({ model: "gpt-5.6-luna", config: { model_reasoning_effort: "medium" } });

const PLAN = "plan" as const;
const IMPLEMENT = "implement" as const;
const VERIFY = "verify" as const;

<Task id={PLAN} agent={planner} output={outputs.plan}>
  Make a plan.
</Task>

<Task id={IMPLEMENT} agent={implementer} fork={PLAN} output={outputs.patch}>
  Implement the plan.
</Task>

<Task id={VERIFY} agent={implementer} fork={IMPLEMENT} output={outputs.result}>
  Run tests and fix failures.
</Task>
```

`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:

```tsx
import { CodexAgent } from "smithers-orchestrator";

const researcher = new CodexAgent({ model: "gpt-5.6-luna", config: { model_reasoning_effort: "medium" } });
const implementer = new CodexAgent({ model: "gpt-5.6-luna", config: { model_reasoning_effort: "medium" } });

<Task id="investigate" agent={researcher} output={outputs.investigation}>
  Understand the bug and identify possible fixes.
</Task>

<Parallel>
  <Task id="minimal-fix" agent={implementer} fork="investigate" output={outputs.patch}>
    Try the minimal fix.
  </Task>
  <Task id="refactor-fix" agent={implementer} fork="investigate" output={outputs.patch}>
    Try the refactor fix.
  </Task>
</Parallel>
```

`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).
- `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.
