---
title: <Supervisor>
description: Boss plans, workers run in parallel, boss reviews and re-delegates failures.
---

```ts
// Props
import { Supervisor } from "smithers-orchestrator";

type SupervisorProps = {
  id?: string;                                  // default: "supervisor"
  boss: AgentLike;
  workers: Record<string, AgentLike>;           // { coder, tester, ... }
  planOutput: OutputTarget;                     // { tasks: [{ id, workerType, instructions }] }
  workerOutput: OutputTarget;
  reviewOutput: OutputTarget;                   // { allDone: boolean, retriable: string[] }
  finalOutput: OutputTarget;
  maxIterations?: number;                       // default: 3
  maxConcurrency?: number;                      // default: 5
  useWorktrees?: boolean;                       // default: false
  skipIf?: boolean;
  children: string | ReactNode;                 // goal/prompt for the boss
};
```

```tsx
export default smithers(() => (
  <Workflow name="build-feature">
    <Supervisor
      boss={boss}
      workers={{ coder, tester }}
      planOutput={outputs.plan}
      workerOutput={outputs.workerResult}
      reviewOutput={outputs.review}
      finalOutput={outputs.final}
      maxIterations={3}
      maxConcurrency={4}
    >
      Build the user authentication module with tests.
    </Supervisor>
  </Workflow>
));
```

## Notes

- Generated node ids: `{id}-plan`, `{id}-loop`, `{id}-worker-{type}`, `{id}-review`, `{id}-final`.
- Workers run with `continueOnFail`; a single failure does not abort the cycle.
- With `useWorktrees`, each worker runs in `.worktrees/{prefix}-worker-{type}` on branch `worker/{prefix}-worker-{type}`.

{/* GENERATED:COMPONENT-SOURCE START (regenerate: pnpm docs:components) */}
## Source

The `<Supervisor>` implementation and the files it imports, straight from the package source. This section is generated; edit the source, not this block.

<CodeGroup>
```js Supervisor.js
// @smithers-type-exports-begin
/** @typedef {import("./SupervisorProps.ts").SupervisorProps} SupervisorProps */
// @smithers-type-exports-end

import React from "react";
import { Sequence } from "./Sequence.js";
import { Task } from "./Task.js";
import { Parallel } from "./Parallel.js";
import { Loop } from "./Ralph.js";
import { Worktree } from "./Worktree.js";
import { useOptionalSmithersContext } from "./useOptionalSmithersContext.js";
/**
 * <Supervisor> — Boss plans, delegates to parallel workers, reviews, re-delegates failures.
 *
 * Composes: Sequence → [plan Task, Loop(until allDone) [Parallel worker Tasks, review Task], final Task]
 * @param {SupervisorProps} props
 */
export function Supervisor(props) {
    if (props.skipIf)
        return null;
    const prefix = props.id ?? "supervisor";
    const maxIterations = props.maxIterations ?? 3;
    const maxConcurrency = props.maxConcurrency ?? 5;
    const useWorktrees = props.useWorktrees ?? false;
    const workerNames = Object.keys(props.workers);
    const ctx = useOptionalSmithersContext();
    const latestReview = ctx?.latest?.(props.reviewOutput, `${prefix}-review`);
    const latestPlan = ctx?.latest?.(props.planOutput, `${prefix}-plan`);
    const allDone = latestReview?.allDone === true;
    // Build a worker Task element for each worker type.
    // At render time the runtime resolves which tasks are active based on
    // the plan output; here we declare one slot per worker type.
    const workerElements = workerNames.map((workerType) => {
        const workerId = `${prefix}-worker-${workerType}`;
        const workerTask = React.createElement(Task, {
            key: workerId,
            id: workerId,
            output: props.workerOutput,
            agent: props.workers[workerType],
            continueOnFail: true,
            // `deps` resolves the plan into the worker's prompt (`needs` alone is
            // cache-context only and injects nothing).
            needs: { plan: `${prefix}-plan` },
            deps: { plan: props.planOutput },
            label: `Worker: ${workerType}`,
            children: (d) => `Execute tasks assigned to worker type "${workerType}". Refer to the plan for your specific instructions.\n\nPlan:\n${JSON.stringify(d.plan ?? "(no plan)")}`,
        });
        if (useWorktrees) {
            return React.createElement(Worktree, {
                key: workerId,
                path: `.worktrees/${workerId}`,
                branch: `worker/${workerId}`,
            }, workerTask);
        }
        return workerTask;
    });
    // Parallel worker execution
    const parallelWorkers = React.createElement(Parallel, { maxConcurrency }, ...workerElements);
    // Boss review Task — depends on the plan and every worker, and resolves all of
    // them into its prompt via `deps`. `depsOptional` omits workers that failed
    // (continueOnFail) rather than deferring the review forever.
    const reviewNeeds = { plan: `${prefix}-plan` };
    const reviewDeps = { plan: props.planOutput };
    for (const workerType of workerNames) {
        const workerId = `${prefix}-worker-${workerType}`;
        reviewNeeds[workerId] = workerId;
        reviewDeps[workerId] = props.workerOutput;
    }
    const reviewTask = React.createElement(Task, {
        id: `${prefix}-review`,
        output: props.reviewOutput,
        agent: props.boss,
        needs: reviewNeeds,
        deps: reviewDeps,
        depsOptional: true,
        label: "Supervisor review",
        children: (d) => {
            const workerResults = workerNames
                .map((workerType) => {
                    const workerId = `${prefix}-worker-${workerType}`;
                    return `### ${workerType}\n${workerId in d ? JSON.stringify(d[workerId]) : "(no result — this worker failed)"}`;
                })
                .join("\n\n");
            return `Review worker results. Set allDone to true if all tasks are satisfactory. List retriable task IDs in retriable[] if any need re-doing.\n\nPlan:\n${JSON.stringify(d.plan ?? "(no plan)")}\n\nWorker results:\n${workerResults}`;
        },
    });
    // Loop body: parallel workers then review
    const loopBody = React.createElement(Sequence, null, parallelWorkers, reviewTask);
    // Loop: repeat until boss says allDone (runtime resolves `until` reactively)
    const delegateLoop = React.createElement(Loop, {
        id: `${prefix}-loop`,
        until: allDone,
        maxIterations,
        onMaxReached: "return-last",
    }, loopBody);
    // Boss plan Task
    const planTask = React.createElement(Task, {
        id: `${prefix}-plan`,
        output: props.planOutput,
        agent: props.boss,
        label: "Supervisor plan",
        children: props.children,
    });
    // Final summary Task. The review lives inside the loop, so fold the plan and
    // the most recent review into the prompt via `latest` (the reader that
    // resolves the newest iteration's rows); Sequence ordering gates it after the
    // loop, and `needs` alone is cache-context only and injects nothing.
    const finalTask = React.createElement(Task, {
        id: `${prefix}-final`,
        output: props.finalOutput,
        agent: props.boss,
        needs: { review: `${prefix}-review`, plan: `${prefix}-plan` },
        label: "Supervisor summary",
        children: () => `Summarize the overall results from all delegation cycles.\n\nPlan:\n${JSON.stringify(latestPlan ?? "(no plan)")}\n\nFinal review:\n${JSON.stringify(latestReview ?? "(no review)")}`,
    });
    return React.createElement(Sequence, null, planTask, delegateLoop, finalTask);
}
```

```js Sequence.js
import React from "react";
/** @typedef {import("./SequenceProps.ts").SequenceProps} SequenceProps */

/**
 * @param {SequenceProps} props
 */
export function Sequence(props) {
    if (props.skipIf)
        return null;
    // Sequence carries only a display label; pass a sanitized bag (align with
    // the sanitizing structural components) so control props don't leak through.
    // `label` names the phase group in run views (graph, the Claude /workflows
    // mirror) and is preserved in the persisted frame XML.
    const next = props.label === undefined ? {} : { label: props.label };
    return React.createElement("smithers:sequence", next, props.children);
}
```

```js Task.js
// @smithers-type-exports-begin
/**
 * @template D
 * @typedef {import("./InferDeps.ts").InferDeps<D>} InferDeps
 */
/** @typedef {import("./OutputTarget.ts").OutputTarget} OutputTarget */
// @smithers-type-exports-end

import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { markdownComponents } from "../markdownComponents.js";
import { zodSchemaToJsonExample } from "../zod-to-example.js";
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
import { SmithersContext } from "@smithers-orchestrator/react-reconciler/context";
import { AspectContext } from "../aspects/AspectContext.js";
import { AntigravityAgent } from "@smithers-orchestrator/agents/AntigravityAgent";
import { ClaudeCodeAgent } from "@smithers-orchestrator/agents/ClaudeCodeAgent";
import { GeminiAgent } from "@smithers-orchestrator/agents/GeminiAgent";
import { PiAgent } from "@smithers-orchestrator/agents/PiAgent";
/** @typedef {import("@smithers-orchestrator/agents/AgentLike").AgentLike} AgentLike */
/** @typedef {import("./DepsSpec.ts").DepsSpec} DepsSpec */
/**
 * @template Row, Output, D
 * @typedef {import("./TaskProps.ts").TaskProps<Row, Output, D>} TaskProps
 */

/**
 * Reverse the HTML-entity escaping that renderToStaticMarkup applies to text
 * (& < > " '). Without this, a prompt like `a < b && c` reaches the agent as
 * `a &lt; b &amp;&amp; c`, and injected JSON schema hints (`"key"`) arrive as
 * `&quot;key&quot;`, both of which corrupt the agent-facing text.
 * `&amp;` is decoded last so an escaped literal (`&amp;lt;`, meaning the source
 * text `&lt;`) is not double-decoded into `<`.
 * @param {string} html
 * @returns {string}
 */
function decodeHtmlEntities(html) {
    return html
        .replace(/&lt;/g, "<")
        .replace(/&gt;/g, ">")
        .replace(/&quot;/g, '"')
        .replace(/&#x27;/g, "'")
        .replace(/&#39;/g, "'")
        .replace(/&amp;/g, "&");
}
/**
 * Render a prompt React node to plain markdown text.
 *
 * If the prompt is a React element (e.g. a compiled MDX component), we inject
 * `markdownComponents` via the standard MDX `components` prop so that
 * renderToStaticMarkup outputs clean markdown instead of HTML. The static
 * render entity-escapes all text, so we decode the entities back to literal
 * characters before handing the prompt to the agent.
 * @param {unknown} prompt
 * @returns {string}
 */
export function renderPromptToText(prompt) {
    if (prompt == null)
        return "";
    if (typeof prompt === "string")
        return prompt;
    if (typeof prompt === "number")
        return String(prompt);
    try {
        let element;
        if (React.isValidElement(prompt)) {
            // Inject markdown components into the element so MDX components
            // render fragments instead of HTML tags.
            element = React.cloneElement(prompt, {
                components: markdownComponents,
            });
        }
        else {
            element = React.createElement(React.Fragment, null, prompt);
        }
        return decodeHtmlEntities(renderToStaticMarkup(element))
            .replace(/\n{3,}/g, "\n\n")
            .trim();
    }
    catch (err) {
        const result = String(prompt ?? "");
        if (result === "[object Object]") {
            throw new SmithersError("MDX_PRELOAD_INACTIVE", `MDX prompt could not be rendered — the prompt resolved to [object Object] instead of a React component.\n\n` +
                `This usually means the MDX preload is not active. Common causes:\n` +
                `  • bunfig.toml uses [run] preload instead of top-level preload (the [run] section doesn't apply to dynamic imports)\n` +
                `  • bunfig.toml is not in the current working directory\n` +
                `  • mdxPlugin() is not registered in the preload script\n` +
                `  • The MDX file is imported without a default import (use: import MyPrompt from "./prompt.mdx")\n\n` +
                `Original error: ${err instanceof Error ? err.message : String(err)}`);
        }
        return result;
    }
}
/**
 * @param {unknown} value
 * @returns {value is import("zod").ZodObject<import("zod").ZodRawShape>}
 */
function isZodObject(value) {
    return Boolean(value && typeof value === "object" && "shape" in value);
}
/**
 * @param {DepsSpec | undefined} deps
 * @param {Record<string, string> | undefined} needs
 * @returns {string[] | undefined}
 */
function deriveDepNodeIds(deps, needs) {
    if (!deps)
        return undefined;
    const ids = new Set();
    for (const key of Object.keys(deps)) {
        const nodeId = needs?.[key] ?? key;
        if (nodeId)
            ids.add(nodeId);
    }
    return ids.size > 0 ? [...ids] : undefined;
}
/**
 * @param {string[] | undefined} dependsOn
 * @param {string[] | undefined} depNodeIds
 * @returns {string[] | undefined}
 */
function mergeDependsOn(dependsOn, depNodeIds) {
    const merged = new Set();
    for (const id of dependsOn ?? [])
        merged.add(id);
    for (const id of depNodeIds ?? [])
        merged.add(id);
    return merged.size > 0 ? [...merged] : undefined;
}
/**
 * @param {any} ctx
 * @param {DepsSpec | undefined} deps
 * @param {Record<string, string> | undefined} needs
 * @param {boolean | undefined} depsOptional
 * @returns {Record<string, unknown> | null}
 */
function resolveDeps(ctx, deps, needs, depsOptional) {
    if (!deps)
        return Object.create(null);
    const keys = Object.keys(deps);
    if (keys.length === 0)
        return Object.create(null);
    const resolved = Object.create(null);
    for (const key of keys) {
        const target = deps[key];
        const nodeId = needs?.[key] ?? key;
        const value = ctx.outputMaybe(target, { nodeId });
        if (value === undefined) {
            // Optional deps mode: omit unresolved keys instead of deferring the
            // whole task. Used when the task is already gated by `needs`/`dependsOn`
            // and upstream tasks may legitimately fail (continueOnFail) without
            // producing an output row.
            if (depsOptional)
                continue;
            return null;
        }
        resolved[key] = value;
    }
    return resolved;
}
/**
 * @param {AgentLike} agent
 * @param {string[] | undefined} allowTools
 * @returns {AgentLike}
 */
function applyCliToolAllowlist(agent, allowTools) {
    if (!allowTools) {
        return agent;
    }
    if (agent instanceof ClaudeCodeAgent) {
        const opts = { ...agent.opts };
        if (allowTools.length === 0) {
            return new ClaudeCodeAgent({
                ...opts,
                allowedTools: [],
                tools: "",
            });
        }
        return new ClaudeCodeAgent({
            ...opts,
            allowedTools: [...allowTools],
        });
    }
    if (agent instanceof PiAgent) {
        const opts = { ...agent.opts };
        if (allowTools.length === 0) {
            return new PiAgent({
                ...opts,
                tools: [],
                noTools: true,
            });
        }
        return new PiAgent({
            ...opts,
            tools: [...allowTools],
            noTools: false,
        });
    }
    if (agent instanceof GeminiAgent) {
        const opts = { ...agent.opts };
        return new GeminiAgent({
            ...opts,
            allowedTools: [...allowTools],
        });
    }
    if (agent instanceof AntigravityAgent) {
        const opts = { ...agent.opts };
        return new AntigravityAgent({
            ...opts,
            allowedTools: [...allowTools],
        });
    }
    return agent;
}
/**
 * @param {unknown} ctx
 * @param {string[] | undefined} allowTools
 * @returns {string[] | undefined}
 */
function resolveCliToolAllowlist(ctx, allowTools) {
    if (allowTools !== undefined) {
        return allowTools;
    }
    const cliAgentToolsDefault = ctx && typeof ctx === "object"
        ? ctx.__smithersRuntime?.cliAgentToolsDefault
        : undefined;
    return cliAgentToolsDefault === "explicit-only" ? [] : undefined;
}
/**
 * @template Row, Output, D
 * @param {TaskProps<Row, Output, D>} props
 * @returns {React.ReactElement | null}
 */
export function Task(props) {
    const { children, agent, fallbackAgent, deps, depsOptional, ...rest } = props;
    const taskContext = props.smithersContext ?? SmithersContext;
    const ctx = React.useContext(taskContext);
    const aspectCtx = React.useContext(AspectContext);
    const depNodeIds = deriveDepNodeIds(deps, rest.needs);
    if (deps && !ctx) {
        throw new SmithersError("CONTEXT_OUTSIDE_WORKFLOW", "Task deps require a workflow context. Build the workflow with createSmithers().");
    }
    const resolvedDeps = deps ? resolveDeps(ctx, deps, rest.needs, depsOptional) : undefined;
    if (deps && resolvedDeps == null) {
        // Deps not yet available — component defers until upstream tasks complete.
        // This is normal reactive behavior; the task will re-render once deps are
        // ready. Record the deferral so the engine can distinguish a transient wait
        // from a permanent one: a deferral that survives to quiescence means a
        // dependency that can never resolve (e.g. a deps key that maps to a node id
        // no task produces), which would otherwise be a silent skip.
        ctx?.recordDeferredDep?.(props.id, depNodeIds ?? []);
        return null;
    }
    // Build aspect metadata to attach to the task element so the engine can
    // enforce budgets and track metrics at execution time.
    const aspectMeta = aspectCtx ? buildAspectMeta(aspectCtx) : undefined;
    const agentChain = Array.isArray(agent)
        ? fallbackAgent
            ? [...agent, fallbackAgent]
            : agent
        : agent && fallbackAgent
            ? [agent, fallbackAgent]
            : agent;
    const effectiveAllowTools = resolveCliToolAllowlist(ctx, rest.allowTools);
    const restrictedAgentChain = Array.isArray(agentChain)
        ? agentChain.map((entry) => applyCliToolAllowlist(entry, effectiveAllowTools))
        : agentChain
            ? applyCliToolAllowlist(agentChain, effectiveAllowTools)
            : agentChain;
    const nextDependsOn = mergeDependsOn(rest.dependsOn, depNodeIds);
    const childValue = typeof children === "function" && (agent || deps)
        ? children(resolvedDeps ?? Object.create(null))
        : children;
    if (agent) {
        // Auto-inject `schema` prop into React element children when output is a ZodObject
        let childElement = childValue;
        const schemaForInjection = props.outputSchema ??
            (isZodObject(props.output) ? props.output : undefined);
        if (React.isValidElement(childValue) && schemaForInjection) {
            childElement = React.cloneElement(childValue, {
                schema: zodSchemaToJsonExample(schemaForInjection),
            });
        }
        const prompt = renderPromptToText(childElement);
        return React.createElement("smithers:task", {
            ...rest,
            dependsOn: nextDependsOn,
            waitAsync: rest.async === true,
            agent: restrictedAgentChain,
            __smithersKind: "agent",
            ...aspectMeta,
        }, prompt);
    }
    if (typeof children === "function" && !deps) {
        const nextProps = {
            ...rest,
            dependsOn: nextDependsOn,
            waitAsync: rest.async === true,
            __smithersKind: "compute",
            __smithersComputeFn: children,
            ...aspectMeta,
        };
        return React.createElement("smithers:task", nextProps, null);
    }
    const nextProps = {
        ...rest,
        dependsOn: nextDependsOn,
        waitAsync: rest.async === true,
        __smithersKind: "static",
        __smithersPayload: childValue,
        __payload: childValue,
        ...aspectMeta,
    };
    return React.createElement("smithers:task", nextProps, null);
}
/**
 * Build the __aspects metadata object from the current AspectContext.
 * This is attached to the smithers:task element props so the engine can read
 * budgets and tracking config at execution time.
 * @param {{
 *     tokenBudget?: unknown;
 *     latencySlo?: unknown;
 *     tracking?: unknown;
 *     accumulator?: unknown;
 * }} aspectCtx
 * @returns {{ __aspects: Record<string, unknown> }}
 */
function buildAspectMeta(aspectCtx) {
    return {
        __aspects: {
            tokenBudget: aspectCtx.tokenBudget,
            latencySlo: aspectCtx.latencySlo,
            tracking: aspectCtx.tracking,
            accumulator: aspectCtx.accumulator,
        },
    };
}
```

```js Parallel.js
import React from "react";
/** @typedef {import("./ParallelProps.ts").ParallelProps} ParallelProps */

/**
 * @param {ParallelProps} props
 */
export function Parallel(props) {
    if (props.skipIf)
        return null;
    // Align prop sanitization with other structural components. `label` names
    // the phase group in run views (graph, the Claude /workflows mirror).
    const next = {
        maxConcurrency: props.maxConcurrency,
        subtreeConcurrency: props.subtreeConcurrency,
        id: props.id,
        ...(props.label === undefined ? {} : { label: props.label }),
    };
    return React.createElement("smithers:parallel", next, props.children);
}
```

```js Ralph.js
// @smithers-type-exports-begin
/** @typedef {import("./RalphProps.ts").RalphProps} RalphProps */
// @smithers-type-exports-end

import React from "react";
/** @typedef {import("./LoopProps.ts").LoopProps} LoopProps */

/**
 * @param {LoopProps} props
 */
export function Loop(props) {
    if (props.skipIf)
        return null;
    // Sanitize to the loop's host props (align with other structural components);
    // key/skipIf are React/control props and children are passed separately.
    const next = {
        id: props.id,
        until: props.until,
        maxIterations: props.maxIterations,
        onMaxReached: props.onMaxReached,
        continueAsNewEvery: props.continueAsNewEvery,
    };
    return React.createElement("smithers:ralph", next, props.children);
}
/** @deprecated Use `Loop` instead. */
export const Ralph = Loop;
```

```js Worktree.js
import React from "react";
import { WORKTREE_EMPTY_PATH_ERROR } from "@smithers-orchestrator/graph/constants";
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
/** @typedef {import("./WorktreeProps.ts").WorktreeProps} WorktreeProps */

/**
 * @param {WorktreeProps} props
 */
export function Worktree(props) {
    if (typeof props.path !== "string" || props.path.trim() === "") {
        throw new SmithersError("WORKTREE_EMPTY_PATH", WORKTREE_EMPTY_PATH_ERROR);
    }
    if (props.skipIf)
        return null;
    const next = { id: props.id, path: props.path, branch: props.branch, baseBranch: props.baseBranch };
    return React.createElement("smithers:worktree", next, props.children);
}
```

```js useOptionalSmithersContext.js
import React from "react";
import { SmithersContext } from "@smithers-orchestrator/react-reconciler/context";

/**
 * Read the workflow context when React is rendering the component, but allow
 * direct structural expansion tests to call composite wrappers as plain
 * functions. In that direct-call path React throws the standard invalid hook
 * error; treating it as "no context yet" preserves the static element shape.
 */
export function useOptionalSmithersContext() {
    try {
        return React.useContext(SmithersContext);
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        if (/Invalid hook call|dispatcher\.useContext|useContext/i.test(message)) {
            return null;
        }
        throw error;
    }
}
```

```ts SupervisorProps.ts
import type React from "react";
import type { AgentLike } from "@smithers-orchestrator/agents/AgentLike";
import type { OutputTarget } from "./OutputTarget.ts";

export type SupervisorProps = {
	id?: string;
	/** Agent that plans, delegates, and reviews worker results. */
	boss: AgentLike;
	/** Map of worker type names to agents (e.g., { coder, tester, docs }). */
	workers: Record<string, AgentLike>;
	/** Output schema for the boss's plan. Must include `tasks: Array<{ id, workerType, instructions }>`. */
	planOutput: OutputTarget;
	/** Output schema for individual worker results. */
	workerOutput: OutputTarget;
	/** Output schema for the boss's review. Must include `allDone: boolean` and `retriable: string[]`. */
	reviewOutput: OutputTarget;
	/** Output schema for the final summary. */
	finalOutput: OutputTarget;
	/** Max delegate-review cycles (default 3). */
	maxIterations?: number;
	/** Max parallel workers (default 5). */
	maxConcurrency?: number;
	/** Whether each worker gets its own git worktree (default false). */
	useWorktrees?: boolean;
	skipIf?: boolean;
	/** Goal/prompt for the boss agent. */
	children: string | React.ReactNode;
};
```
</CodeGroup>
{/* GENERATED:COMPONENT-SOURCE END */}
