> ## Documentation Index
> Fetch the complete documentation index at: https://smithers-feat-claude-workflow-mirror.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# <Runbook>

> Sequential steps with risk classification; safe auto-runs, risky/critical gate on approval.

```ts theme={null}
// Props
import { Runbook } from "smithers-orchestrator";

type RunbookProps = {
  id?: string; // used as step-id prefix; defaults to "runbook" when omitted
  steps: RunbookStep[];
  defaultAgent?: AgentLike;
  stepOutput: OutputTarget;
  approvalRequest?: Partial<ApprovalRequest>;
  onDeny?: "fail" | "skip"; // default "fail"
  skipIf?: boolean;
};

type RunbookStep = {
  id: string;
  agent?: AgentLike;
  command?: string;
  risk: "safe" | "risky" | "critical"; // critical adds `elevated: true` to approval meta
  label?: string;
  output?: OutputTarget;
};
```

```tsx theme={null}
export default smithers(() => (
  <Workflow name="deploy-runbook">
    <Runbook
      defaultAgent={ops}
      stepOutput={outputs.stepResult}
      steps={[
        { id: "health-check", command: "curl -f https://api.example.com/health", risk: "safe" },
        { id: "backup-db", command: "pg_dump prod > backup.sql", risk: "risky" },
        { id: "run-migration", command: "npx prisma migrate deploy", risk: "critical" },
        { id: "smoke-test", command: "npm run test:smoke", risk: "safe" },
      ]}
    />
  </Workflow>
));
```

## Notes

* Each step depends on the previous via `needs`; execution order is guaranteed.
* Critical steps set `elevated: true` in approval metadata for stronger auth UIs.
* Approval output is stored at `{prefix}-{step.id}-approval-decision`.

## Source

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

<CodeGroup>
  ```js Runbook.js theme={null}
  // @smithers-type-exports-begin
  /** @typedef {import("./RunbookProps.ts").RunbookProps} RunbookProps */
  /** @typedef {import("./RunbookStep.ts").RunbookStep} RunbookStep */
  // @smithers-type-exports-end

  import React from "react";
  import { Sequence } from "./Sequence.js";
  import { Task } from "./Task.js";
  import { Approval } from "./Approval.js";
  /**
   * <Runbook> — Sequential steps with risk classification.
   *
   * Safe steps auto-execute. Risky and critical steps require human approval first.
   * Composes: Sequence of [Approval? → Task] per step, chained via `needs`.
   * @param {RunbookProps} props
   */
  export function Runbook(props) {
      if (props.skipIf)
          return null;
      const prefix = props.id ?? "runbook";
      const onDeny = props.onDeny ?? "fail";
      const children = [];
      let previousStepId;
      for (let i = 0; i < props.steps.length; i++) {
          const step = props.steps[i];
          const stepId = `${prefix}-${step.id}`;
          const agent = step.agent ?? props.defaultAgent;
          const output = step.output ?? props.stepOutput;
          const label = step.label ?? step.id;
          // Build needs: each step depends on the previous step's completion
          const needs = previousStepId
              ? { previousStep: previousStepId }
              : undefined;
          if (step.risk === "safe") {
              // Safe: plain Task, auto-executes
              children.push(React.createElement(Task, {
                  key: stepId,
                  id: stepId,
                  output,
                  agent,
                  needs,
                  label: `[safe] ${label}`,
                  children: step.command ?? `Execute step: ${label}`,
              }));
              previousStepId = stepId;
          }
          else {
              // Risky or critical: Approval gate then Task
              const approvalId = `${stepId}-approval`;
              const isCritical = step.risk === "critical";
              const approvalTitle = props.approvalRequest?.title ??
                  `Approve ${isCritical ? "CRITICAL" : "risky"} step: ${label}`;
              const approvalSummary = props.approvalRequest?.summary ??
                  (isCritical
                      ? `CRITICAL step requires elevated approval. Command: ${step.command ?? label}`
                      : `Risky step requires approval before execution. Command: ${step.command ?? label}`);
              const approvalMeta = {
                  stepId: step.id,
                  risk: step.risk,
                  ...props.approvalRequest?.metadata,
              };
              if (isCritical) {
                  approvalMeta.elevated = true;
              }
              children.push(React.createElement(Approval, {
                  key: approvalId,
                  id: approvalId,
                  output: `${approvalId}-decision`,
                  request: {
                      title: approvalTitle,
                      summary: approvalSummary,
                      metadata: approvalMeta,
                  },
                  onDeny: onDeny === "skip" ? "skip" : "fail",
                  needs,
                  label: `Approve: ${label}`,
              }));
              children.push(React.createElement(Task, {
                  key: stepId,
                  id: stepId,
                  output,
                  agent,
                  needs: { approval: approvalId },
                  label: `[${step.risk}] ${label}`,
                  children: step.command ?? `Execute step: ${label}`,
              }));
              previousStepId = stepId;
          }
      }
      return React.createElement(Sequence, null, ...children);
  }
  ```

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

  /**
   * @param {SequenceProps} props
   */
  export function Sequence(props) {
      if (props.skipIf)
          return null;
      // Sequence carries no host props of its own; pass an empty bag (align with
      // the sanitizing structural components) so control props don't leak through.
      return React.createElement("smithers:sequence", {}, props.children);
  }
  ```

  ```js Task.js theme={null}
  // @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
   */

  /**
   * 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.
   * No HTML tag stripping or entity decoding needed.
   * @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 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
   * @returns {Record<string, unknown> | null}
   */
  function resolveDeps(ctx, deps, needs) {
      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)
              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, ...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) : 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 Approval.js theme={null}
  // @smithers-type-exports-begin
  /** @typedef {import("./ApprovalDecision.ts").ApprovalDecision} ApprovalDecision */
  /** @typedef {import("./ApprovalRanking.ts").ApprovalRanking} ApprovalRanking */
  /** @typedef {import("./ApprovalRequest.ts").ApprovalRequest} ApprovalRequest */
  /** @typedef {import("./ApprovalSelection.ts").ApprovalSelection} ApprovalSelection */
  // @smithers-type-exports-end

  import React from "react";
  import { z } from "zod";
  import { SmithersContext } from "@smithers-orchestrator/react-reconciler/context";
  import { getTaskRuntime } from "@smithers-orchestrator/driver/task-runtime";
  import { SmithersDb } from "@smithers-orchestrator/db/adapter";
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
  /** @typedef {import("./ApprovalAutoApprove.ts").ApprovalAutoApprove} ApprovalAutoApprove */
  /** @typedef {import("./ApprovalMode.ts").ApprovalMode} ApprovalMode */
  /** @typedef {import("./ApprovalOption.ts").ApprovalOption} ApprovalOption */
  /**
   * @template Row, Output
   * @typedef {import("./ApprovalProps.ts").ApprovalProps<Row, Output>} ApprovalProps
   */

  export const approvalDecisionSchema = z.object({
      approved: z.boolean(),
      // `note` is omitted entirely when no note was provided, so the default
      // decision schema must accept an absent key (optional) as well as the
      // legacy null/string shapes.
      note: z.string().nullable().optional(),
      decidedBy: z.string().nullable(),
      decidedAt: z.string().datetime().nullable(),
  });
  export const approvalSelectionSchema = z.object({
      selected: z.string(),
      notes: z.string().nullable(),
  });
  export const approvalRankingSchema = z.object({
      ranked: z.array(z.string()),
      notes: z.string().nullable(),
  });
  /**
   * @param {unknown} value
   * @returns {value is import("zod").ZodObject<import("zod").ZodRawShape>}
   */
  function isZodObject(value) {
      return Boolean(value && typeof value === "object" && "shape" in value);
  }
  /**
   * @template T
   * @param {unknown} value
   * @returns {T | null}
   */
  function parseJson(value) {
      if (typeof value !== "string" || value.length === 0) {
          return null;
      }
      try {
          return JSON.parse(value);
      }
      catch {
          return null;
      }
  }
  /**
   * @param {ApprovalMode} mode
   * @returns {import("zod").ZodObject<import("zod").ZodRawShape>}
   */
  function defaultSchemaForMode(mode) {
      switch (mode) {
          case "select":
              return approvalSelectionSchema;
          case "rank":
              return approvalRankingSchema;
          default:
              return approvalDecisionSchema;
      }
  }
  /**
   * @param {{ status?: string | null; note?: string | null; decidedBy?: string | null; decidedAtMs?: number | null } | undefined | null} approval
   * @param {import("zod").ZodObject<import("zod").ZodRawShape>} outputSchema
   * @returns {Record<string, unknown>}
   */
  function buildDecisionPayload(approval, outputSchema) {
      const base = {
          approved: approval?.status === "approved",
          decidedBy: approval?.decidedBy ?? null,
          decidedAt: approval?.decidedAtMs != null ? new Date(approval.decidedAtMs).toISOString() : null,
      };
      if (typeof approval?.note === "string") {
          return { ...base, note: approval.note };
      }
      if (outputSchema.safeParse(base).success) {
          return base;
      }
      return { ...base, note: null };
  }
  /**
   * @param {ApprovalMode | undefined} mode
   * @returns {"select" | "rank" | "decision"}
   */
  function normalizeMode(mode) {
      switch (mode) {
          case "select":
              return "select";
          case "rank":
              return "rank";
          default:
              return "decision";
      }
  }
  /**
   * @param {ApprovalOption[] | undefined} options
   * @returns {ApprovalOption[] | undefined}
   */
  function normalizeOptions(options) {
      return options?.map((option) => ({
          key: option.key,
          label: option.label,
          ...(option.summary ? { summary: option.summary } : {}),
          ...(option.metadata ? { metadata: option.metadata } : {}),
      }));
  }
  /**
   * @param {ApprovalAutoApprove[keyof ApprovalAutoApprove]} callback
   * @param {import("@smithers-orchestrator/driver").SmithersCtx<unknown> | null} ctx
   * @returns {boolean | undefined}
   */
  function evaluateBooleanCallback(callback, ctx) {
      if (typeof callback !== "function") {
          return undefined;
      }
      return Boolean(/** @type {(ctx: import("@smithers-orchestrator/driver").SmithersCtx<unknown> | null) => boolean} */ (callback)(ctx));
  }
  /**
   * @template Row
   * @param {ApprovalProps<Row>} props
   * @returns {React.ReactElement | null}
   */
  export function Approval(props) {
      if (props.skipIf)
          return null;
      const smithersContext = props.smithersContext ?? SmithersContext;
      const ctx = React.useContext(smithersContext);
      const mode = props.mode ?? "approve";
      const approvalMode = normalizeMode(mode);
      const options = normalizeOptions(props.options);
      const outputSchema = props.outputSchema ??
          (isZodObject(props.output) ? props.output : defaultSchemaForMode(mode));
      if ((mode === "select" || mode === "rank") && (!options || options.length === 0)) {
          throw new SmithersError("APPROVAL_OPTIONS_REQUIRED", `Approval ${props.id} requires options when mode="${mode}".`);
      }
      const conditionMet = props.autoApprove
          ? evaluateBooleanCallback(props.autoApprove.condition, ctx)
          : undefined;
      const revertOnMet = props.autoApprove
          ? evaluateBooleanCallback(props.autoApprove.revertOn, ctx)
          : undefined;
      const autoApprove = props.autoApprove
          ? {
              ...(typeof props.autoApprove.after === "number" ? { after: props.autoApprove.after } : {}),
              audit: props.autoApprove.audit !== false,
              ...(conditionMet !== undefined ? { conditionMet } : {}),
              ...(revertOnMet !== undefined ? { revertOnMet } : {}),
          }
          : undefined;
      const requestMeta = {
          ...(props.request.summary ? { requestSummary: props.request.summary } : {}),
          ...(options ? { approvalOptions: options } : {}),
          ...(props.allowedScopes?.length ? { approvalAllowedScopes: props.allowedScopes } : {}),
          ...(props.allowedUsers?.length ? { approvalAllowedUsers: props.allowedUsers } : {}),
          ...(autoApprove ? { approvalAutoApprove: autoApprove } : {}),
          ...props.request.metadata,
          ...props.meta,
      };
      /**
     * @returns {Promise<Row>}
     */
      const computeDecision = async () => {
          const runtime = getTaskRuntime();
          if (!runtime) {
              throw new SmithersError("APPROVAL_OUTSIDE_TASK", "Approval decisions can only be resolved while a Smithers task is executing.");
          }
          const adapter = new SmithersDb(runtime.db);
          const approval = await adapter.getApproval(runtime.runId, props.id, runtime.iteration);
          const decision = parseJson(approval?.decisionJson);
          if (approvalMode === "select") {
              return {
                  selected: typeof decision?.selected === "string" ? decision.selected : "",
                  notes: typeof decision?.notes === "string"
                      ? decision.notes
                      : approval?.note ?? null,
              };
          }
          if (approvalMode === "rank") {
              return {
                  ranked: Array.isArray(decision?.ranked)
                      ? decision.ranked.filter((value) => typeof value === "string")
                      : [],
                  notes: typeof decision?.notes === "string"
                      ? decision.notes
                      : approval?.note ?? null,
              };
          }
          return buildDecisionPayload(approval, outputSchema);
      };
      return React.createElement("smithers:task", {
          id: props.id,
          key: props.key,
          output: props.output,
          outputSchema,
          dependsOn: props.dependsOn,
          needs: props.needs,
          needsApproval: true,
          waitAsync: props.async === true,
          approvalMode,
          approvalOnDeny: props.onDeny,
          approvalOptions: options,
          approvalAllowedScopes: props.allowedScopes,
          approvalAllowedUsers: props.allowedUsers,
          approvalAutoApprove: autoApprove,
          timeoutMs: props.timeoutMs,
          heartbeatTimeoutMs: props.heartbeatTimeoutMs,
          heartbeatTimeout: props.heartbeatTimeout,
          retries: props.retries,
          retryPolicy: props.retryPolicy,
          continueOnFail: props.continueOnFail,
          cache: props.cache,
          label: props.label ?? props.request.title,
          meta: Object.keys(requestMeta).length > 0 ? requestMeta : undefined,
          __smithersKind: "compute",
          __smithersComputeFn: computeDecision,
      });
  }
  ```

  ```ts RunbookProps.ts theme={null}
  import type { AgentLike } from "@smithers-orchestrator/agents/AgentLike";
  import type { ApprovalRequest } from "./ApprovalRequest.ts";
  import type { RunbookStep } from "./RunbookStep.ts";
  import type { OutputTarget } from "./OutputTarget.ts";

  export type RunbookProps = {
  	id?: string;
  	/** Ordered steps to execute. */
  	steps: RunbookStep[];
  	/** Default agent for steps that don't specify one. */
  	defaultAgent?: AgentLike;
  	/** Default output schema for step results. */
  	stepOutput: OutputTarget;
  	/** Template for approval requests on risky/critical steps. */
  	approvalRequest?: Partial<ApprovalRequest>;
  	/** Behavior when a risky/critical step is denied: "fail" (default) or "skip". */
  	onDeny?: "fail" | "skip";
  	skipIf?: boolean;
  };
  ```

  ```ts RunbookStep.ts theme={null}
  import type { AgentLike } from "@smithers-orchestrator/agents/AgentLike";
  import type { OutputTarget } from "./OutputTarget.ts";

  export type RunbookStep = {
  	/** Unique step identifier. */
  	id: string;
  	/** Agent for this step (falls back to `defaultAgent`). */
  	agent?: AgentLike;
  	/** Shell command or instruction for the step. */
  	command?: string;
  	/** Risk classification: safe auto-executes, risky/critical require approval. */
  	risk: "safe" | "risky" | "critical";
  	/** Human-readable label for the step. */
  	label?: string;
  	/** Per-step output schema override. */
  	output?: OutputTarget;
  };
  ```
</CodeGroup>
