Skip to main content
// 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;
};
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.
// @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);
}