Understanding

Planning

Solves: agreeing on the shape of the work before the agent does it.

Established — a stable convention across independent vendors.

pattern: established code: beta <PlanPreview>: Beta: the code works and has tests, and its API can still change.

watching →emerging →established →contested →fading 13 sightings since Apr 2026 seen in 1 different contexts
Ask an assistant about this pattern Ask ChatGPT ↗Ask Claude ↗

Planning puts the agent's intended steps in front of the person before any of them run. The person edits, accepts, or sends the plan back, and one decision about the whole replaces a decision about each action. If the agent leaves the plan mid-run, it says so against what was agreed.

When to Use It

  • The work is long or consequential enough to review its shape once, up front
  • Approving a plan can replace dozens of per-action approvals
  • Deviations from an agreed plan need to be visible

When Not To

  • Tasks are trivial; asking for a plan on a two-step task is theater
  • You cannot pause execution; a plan that authorizes nothing should not look like consent
  • The person cannot evaluate the steps; invest in the preview, not the ask

Three views

The exchange between the human, the agent and the system the agent acts on; who holds each part of it; and the component that implements it.

01 · the interaction
human agent system codebase · shell · services reads the codebase, schema or account to draft presents the plan, step by step edits or redirects a step accepts the plan runs each step: commands, writes, calls returns what happened at each step surfaces a deviation from the plan
02 · who holds what
human interface agent Plan steps: The steps the AI intends to take, in order, in words you can read 01 Plan steps Edit: You reshape the plan before any work starts 02 Edit Accept: You approve the whole plan at once 03 Accept Reject & revise: You send the plan back with a note on what to change 04 Reject & revise Deviation note: The AI tells you when it leaves the plan mid-run 05 Deviation note
human interface agent Plan steps: The steps the AI intends to take, in order, in words you can read 01 Plan steps Edit: You reshape the plan before any work starts 02 Edit Accept: You approve the whole plan at once 03 Accept Reject & revise: You send the plan back with a note on what to change 04 Reject & revise Deviation note: The AI tells you when it leaves the plan mid-run 05 Deviation note
  1. 01 Plan steps The steps the AI intends to take, in order, in words you can read
  2. 02 Edit You reshape the plan before any work starts
  3. 03 Accept You approve the whole plan at once
  4. 04 Reject & revise You send the plan back with a note on what to change
  5. 05 Deviation note The AI tells you when it leaves the plan mid-run
03 · the component · code: beta

vendor presets are recreations, not vendor assets

THE LIVE PLAYGROUND NEEDS A WIDER SCREEN — IT'S WAITING ON DESKTOP. THE ANATOMY ABOVE AND THE CODE BELOW ARE THE SAME COMPONENT.

Do / Don't

Do

For consequential work, make the plan block: nothing runs until the person accepts it.

Don't

Show a step list that authorizes nothing and let it read as consent.

A good-looking plan is not a good plan, and a narrated plan is not an approved one. Decide whether your step list blocks or narrates, and say which.

Do

Keep plans reviewable: a handful of steps a human can actually judge.

Don't

Emit forty micro-steps nobody reads, or three steps too coarse to mean anything.

Step lists too fine to read or too coarse to review both defeat the point of externalizing intent.

Do

Surface deviations against the approved plan as they happen.

Don't

Silently execute a different plan than the one the human accepted.

The plan the human approved is not always the plan that runs — deviation notes keep the contract honest.

Every pair cites a documented tension or principle boundary — no free-floating taste.

Get the Code

Namespaced install (add the registry to components.json once):

{
  "registries": {
    "@agentsandhumans": "https://agentsandhumans.ai/api/registry/{name}.json"
  }
}
npx shadcn@latest add @agentsandhumans/plan-preview

Or install directly from the endpoint:

npx shadcn@latest add https://agentsandhumans.ai/api/registry/plan-preview.json

Dependencies: react · Files: patterns/plan-preview/plan-preview-core.tsx · patterns/plan-preview/plan-preview.tsx · patterns/tokens.css

patterns/plan-preview/plan-preview-core.tsx
/**
 * PlanPreview — headless core.
 * Pattern: showing intended steps for review and edit before the agent acts.
 * Anatomy: Steps · Edit · Accept · Reject-revise · Deviation note
 *
 * The config's `gate` flag captures the Claude Code vs. Codex divergence:
 * gate=true → the plan blocks execution until accepted (plan as contract);
 * gate=false → the plan is a live trace and Accept/Revise are absent.
 */
import * as React from 'react';
import {
  createEvent,
  type EventDraft,
  type InteractionEventHandler,
  type PlanAcceptedPayload,
  type PlanProposedPayload,
  type PlanRevisedPayload,
  type PlanSnapshot,
} from '@/lib/interaction-contract';

export type { PlanSnapshot } from '@/lib/interaction-contract';

export type PlanState = 'proposed' | 'editing' | 'accepted' | 'revising' | 'executing';
export type StepStatus = 'pending' | 'active' | 'done' | 'skipped';

export interface PlanStep {
  id: string;
  text: string;
  status: StepStatus;
}

export interface PlanPreviewConfig {
  /** Does the plan block execution until accepted? */
  gate: boolean;
  /** Can the human edit steps before accepting? */
  allowEdit: boolean;
}

export const defaultConfig: PlanPreviewConfig = { gate: true, allowEdit: true };

/**
 * Lifecycle (gate=true):
 *   proposed ⇄ editing → accepted → executing
 *   proposed → revising
 *   accepted → editing (an edit makes a new version; Done without an edit returns to accepted)
 * Execution starts only on START, which the host sends through `start()` or the
 * controlled `state="executing"` prop. ACCEPT never starts it.
 */
export type PlanEvent =
  | { type: 'ACCEPT' }
  | { type: 'REVISE'; direction?: string }
  | { type: 'EDIT' }
  | { type: 'CANCEL_EDIT' } // leave editing (the Edit toggle); edits made so far stay
  | { type: 'UPDATE_STEP'; id: string; text: string }
  | { type: 'START' } // host only: run the accepted plan
  | { type: 'ADVANCE' } // execution progress: the current step is done, the next one is active
  | { type: 'DEVIATE'; note: string };

export interface MachineState {
  state: PlanState;
  planId: string;
  version: number;
  steps: PlanStep[];
  /** The last snapshot the person accepted; null until the first accept. */
  accepted: PlanSnapshot | null;
  revisionDirection: string | null;
  deviations: string[];
}

export function planSnapshot(s: Pick<MachineState, 'planId' | 'version' | 'steps'>): PlanSnapshot {
  return Object.freeze({
    planId: s.planId,
    version: s.version,
    steps: Object.freeze(s.steps.map((st) => Object.freeze({ id: st.id, text: st.text }))),
  });
}

export function initialPlanState(
  steps: Array<Pick<PlanStep, 'id' | 'text'>>,
  config: PlanPreviewConfig,
  planId: string,
  version = 1,
): MachineState {
  return {
    // A non-gating plan is already executing when it appears: that's the point
    state: config.gate ? 'proposed' : 'executing',
    planId,
    version,
    steps: steps.map((s, i) => ({ id: s.id, text: s.text, status: i === 0 && !config.gate ? 'active' : 'pending' })),
    accepted: null,
    revisionDirection: null,
    deviations: [],
  };
}

/** True while the plan on screen is the version the person accepted. */
function isAcceptedVersion(s: MachineState): boolean {
  return s.accepted !== null && s.accepted.version === s.version;
}

export function reducer(s: MachineState, e: PlanEvent): MachineState {
  switch (e.type) {
    case 'ACCEPT':
      return s.state === 'proposed' || s.state === 'editing'
        ? { ...s, state: 'accepted', accepted: planSnapshot(s) }
        : s;
    case 'REVISE':
      return s.state === 'proposed' ? { ...s, state: 'revising', revisionDirection: e.direction ?? null } : s;
    case 'EDIT':
      return s.state === 'proposed' || s.state === 'accepted' ? { ...s, state: 'editing' } : s;
    case 'CANCEL_EDIT':
      if (s.state !== 'editing') return s;
      return { ...s, state: isAcceptedVersion(s) ? 'accepted' : 'proposed' };
    case 'UPDATE_STEP': {
      if (s.state !== 'editing') return s;
      const target = s.steps.find((st) => st.id === e.id);
      if (!target || target.text === e.text) return s;
      return {
        ...s,
        // The first change to an accepted plan makes a new version, which needs its own accept
        version: isAcceptedVersion(s) ? s.version + 1 : s.version,
        steps: s.steps.map((st) => (st.id === e.id ? { ...st, text: e.text } : st)),
      };
    }
    case 'START': {
      if (s.state !== 'accepted') return s;
      const first = s.steps.findIndex((st) => st.status === 'pending');
      return {
        ...s,
        state: 'executing',
        steps: s.steps.map((st, i) => (i === first ? { ...st, status: 'active' } : st)),
      };
    }
    case 'ADVANCE': {
      if (s.state !== 'executing') return s;
      const idx = s.steps.findIndex((st) => st.status === 'pending' || st.status === 'active');
      if (idx === -1) return s;
      return {
        ...s,
        steps: s.steps.map((st, i) =>
          i === idx ? { ...st, status: 'done' } : i === idx + 1 ? { ...st, status: 'active' } : st,
        ),
      };
    }
    case 'DEVIATE':
      return s.state === 'executing' ? { ...s, deviations: [...s.deviations, e.note] } : s;
  }
}

/**
 * The contract events one transition produces (spec 2026-10-04 §1). `prev` is
 * null on mount. Every event carries the plan id as its correlationId.
 */
export function planEvents(
  prev: MachineState | null,
  next: MachineState,
  config: PlanPreviewConfig = defaultConfig,
): EventDraft[] {
  const correlationId = next.planId;
  if (prev === null) {
    const payload: PlanProposedPayload = {
      planId: next.planId,
      version: next.version,
      steps: planSnapshot(next).steps,
      gate: config.gate,
    };
    return [{ type: 'plan.proposed', actor: 'agent', correlationId, payload }];
  }
  const out: EventDraft[] = [];
  if (next.version > prev.version) {
    const payload: PlanRevisedPayload = { planId: next.planId, version: next.version, reason: 'edited' };
    out.push({ type: 'plan.revised', actor: 'human', correlationId, payload });
  }
  if (prev.state !== 'revising' && next.state === 'revising') {
    const payload: PlanRevisedPayload = {
      planId: next.planId,
      version: next.version,
      reason: 'requested',
      direction: next.revisionDirection,
    };
    out.push({ type: 'plan.revised', actor: 'human', correlationId, payload });
  }
  if (next.accepted && next.accepted !== prev.accepted) {
    const payload: PlanAcceptedPayload = next.accepted;
    out.push({ type: 'plan.accepted', actor: 'human', correlationId, payload });
  }
  return out;
}

interface PlanPreviewContext extends MachineState {
  config: PlanPreviewConfig;
  send: (e: PlanEvent) => void;
  /** Host only: run the accepted plan. Does nothing unless the state is `accepted`. */
  start: () => void;
}

const Ctx = React.createContext<PlanPreviewContext | null>(null);

export function usePlanPreview(): PlanPreviewContext {
  const ctx = React.useContext(Ctx);
  if (!ctx) throw new Error('PlanPreview parts must be used inside <PlanPreview.Root>');
  return ctx;
}

export interface RootProps {
  steps: Array<Pick<PlanStep, 'id' | 'text'>>;
  /** Stable id for this plan; carried on every snapshot. Defaults to a generated id. */
  planId?: string;
  /** Version of the plan as proposed. Defaults to 1. */
  version?: number;
  config?: Partial<PlanPreviewConfig>;
  /**
   * Controlled execution: pass `executing` to start the accepted plan (the same
   * as calling `start()`). It has no effect until the person has accepted.
   */
  state?: PlanState;
  /** Called with the frozen plan the person accepted, once per accepted version. */
  onAccept?: (plan: PlanSnapshot) => void;
  onRevise?: (direction: string | null) => void;
  /** One agent run; stamped on every contract event. Defaults to a generated id. */
  runId?: string;
  /** Contract events: plan.proposed, plan.revised, plan.accepted. Correlation id: the plan id. */
  onEvent?: InteractionEventHandler;
  children: React.ReactNode;
}

function Root({
  steps,
  planId,
  version,
  config: partial,
  state: controlled,
  onAccept,
  onRevise,
  runId,
  onEvent,
  children,
}: RootProps) {
  const config = React.useMemo(() => ({ ...defaultConfig, ...partial }), [partial]);
  const fallbackId = React.useId();
  const [machine, send] = React.useReducer(reducer, undefined, () =>
    initialPlanState(steps, config, planId ?? fallbackId, version),
  );
  const start = React.useCallback(() => send({ type: 'START' }), []);

  React.useEffect(() => {
    if (controlled === 'executing' && machine.state === 'accepted') start();
  }, [controlled, machine.state, start]);

  const prevAccepted = React.useRef<PlanSnapshot | null>(machine.accepted);
  const prevState = React.useRef<PlanState>(machine.state);
  React.useEffect(() => {
    if (machine.accepted && machine.accepted !== prevAccepted.current) onAccept?.(machine.accepted);
    prevAccepted.current = machine.accepted;
    if (prevState.current !== machine.state && machine.state === 'revising') onRevise?.(machine.revisionDirection);
    prevState.current = machine.state;
  }, [machine.accepted, machine.state, machine.revisionDirection, onAccept, onRevise]);

  const fallbackRun = React.useId();
  const run = runId ?? fallbackRun;
  const emit = React.useRef(onEvent);
  emit.current = onEvent;
  const prevMachine = React.useRef<MachineState | null>(null);
  React.useEffect(() => {
    const drafts = planEvents(prevMachine.current, machine, config);
    prevMachine.current = machine;
    for (const d of drafts) emit.current?.(createEvent(run, d));
  }, [machine, config, run]);

  const value = React.useMemo(() => ({ ...machine, config, send, start }), [machine, config, start]);

  return (
    <Ctx.Provider value={value}>
      <div
        role="group"
        aria-label="Agent plan"
        data-pattern="plan-preview"
        data-part="root"
        data-state={machine.state}
        data-gate={config.gate}
        data-version={machine.version}
      >
        {children}
      </div>
    </Ctx.Provider>
  );
}

function Steps({ children }: { children: (step: PlanStep, index: number) => React.ReactNode }) {
  const { steps, state } = usePlanPreview();
  return (
    <ol data-part="steps" data-state={state}>
      {steps.map((s, i) => (
        <li key={s.id} data-part="step" data-status={s.status}>
          {children(s, i)}
        </li>
      ))}
    </ol>
  );
}

/** Inline editor for a step — renders children only while editing is possible. */
function StepEditor({ step }: { step: PlanStep }) {
  const { state, config, send } = usePlanPreview();
  if (!config.allowEdit || state !== 'editing') return null;
  return (
    <input
      data-part="step-editor"
      aria-label={`Edit step: ${step.text}`}
      value={step.text}
      onChange={(e) => send({ type: 'UPDATE_STEP', id: step.id, text: e.target.value })}
    />
  );
}

type TriggerProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'>;

/** Absent entirely when the plan is a trace, not a gate. */
function Accept(props: TriggerProps) {
  const { state, config, send } = usePlanPreview();
  if (!config.gate) return null;
  return (
    <button
      type="button"
      data-part="accept"
      disabled={state !== 'proposed' && state !== 'editing'}
      onClick={() => send({ type: 'ACCEPT' })}
      {...props}
    />
  );
}

function Revise({ direction, ...props }: TriggerProps & { direction?: string }) {
  const { state, config, send } = usePlanPreview();
  if (!config.gate) return null;
  return (
    <button
      type="button"
      data-part="revise"
      disabled={state !== 'proposed'}
      onClick={() => send({ type: 'REVISE', direction })}
      {...props}
    />
  );
}

function Edit(props: TriggerProps) {
  const { state, config, send } = usePlanPreview();
  if (!config.allowEdit || !config.gate) return null;
  return (
    <button
      type="button"
      data-part="edit"
      aria-pressed={state === 'editing'}
      disabled={state !== 'proposed' && state !== 'editing' && state !== 'accepted'}
      onClick={() => send({ type: state === 'editing' ? 'CANCEL_EDIT' : 'EDIT' })}
      {...props}
    />
  );
}

/** How departures from the approved plan surface mid-run. */
function DeviationNote() {
  const { deviations, state } = usePlanPreview();
  if (deviations.length === 0) return null;
  return (
    <div data-part="deviation" data-state={state} role="status" aria-live="polite">
      {deviations.map((d, i) => (
        <p key={i}>{d}</p>
      ))}
    </div>
  );
}

export const PlanPreview = Object.assign(Root, { Steps, StepEditor, Accept, Revise, Edit, DeviationNote });
patterns/plan-preview/plan-preview.tsx
/**
 * PlanPreview — styled reference implementation with vendor presets.
 * The gate/trace divergence is the preset: Claude Code's plan blocks execution
 * (gate: true); Codex's plan narrates it (gate: false).
 */
import * as React from 'react';
import {
  PlanPreview as Core,
  usePlanPreview,
  type PlanPreviewConfig,
  type PlanSnapshot,
  type PlanState,
  type PlanStep,
} from './plan-preview-core';
import type { InteractionEventHandler } from '@/lib/interaction-contract';

export interface PlanPreviewPreset {
  id: string;
  label: string;
  copy: { title: string; accept: string; revise: string; edit?: string };
  machineConfig: Partial<PlanPreviewConfig>;
}

export const presets: Record<'neutral' | 'anthropic' | 'openai', PlanPreviewPreset> = {
  neutral: {
    id: 'neutral',
    label: 'Neutral',
    copy: { title: 'Proposed plan', accept: 'Accept plan', revise: 'Request changes', edit: 'Edit' },
    machineConfig: { gate: true, allowEdit: true },
  },
  anthropic: {
    id: 'anthropic',
    label: 'Claude Code Plan Mode (recreation)',
    copy: { title: "Claude's plan", accept: 'Yes, and approve the plan', revise: 'No, keep planning', edit: 'Edit plan' },
    machineConfig: { gate: true, allowEdit: true },
  },
  openai: {
    id: 'openai',
    label: 'Codex task plan (recreation)',
    copy: { title: 'Task plan', accept: '', revise: '' }, // no gate: accept/revise absent
    machineConfig: { gate: false, allowEdit: false },
  },
};

export interface PlanPreviewProps {
  preset?: keyof typeof presets;
  steps: Array<Pick<PlanStep, 'id' | 'text'>>;
  planId?: string;
  version?: number;
  /** Pass `executing` to run the accepted plan. Accepting never starts it. */
  state?: PlanState;
  /** Receives the frozen plan the person accepted: `{ planId, version, steps }`. */
  onAccept?: (plan: PlanSnapshot) => void;
  onRevise?: (direction: string | null) => void;
  /** One agent run; stamped on every contract event. */
  runId?: string;
  /** Interaction Contract events (see the Interaction Contract docs page). */
  onEvent?: InteractionEventHandler;
}

/** Says which version is accepted, and when an edit has made a new one that needs accepting. */
function VersionNote() {
  const { state, version, accepted, config } = usePlanPreview();
  if (!config.gate) return null;
  let text = '';
  if (state === 'accepted') text = `Version ${version} accepted. Nothing runs until the host starts it.`;
  else if (accepted && accepted.version !== version) text = `Version ${version} changed after acceptance. Accept it again before it runs.`;
  else if (state === 'executing') text = `Running version ${version}.`;
  return (
    <p data-part="version-note" role="status" aria-live="polite" hidden={!text}>
      {text}
    </p>
  );
}

export function PlanPreview({ preset = 'neutral', steps, planId, version, state, onAccept, onRevise, runId, onEvent }: PlanPreviewProps) {
  const p = presets[preset];
  return (
    <Core
      steps={steps}
      planId={planId}
      version={version}
      state={state}
      config={p.machineConfig}
      onAccept={onAccept}
      onRevise={onRevise}
      runId={runId}
      onEvent={onEvent}
    >
      <div className="ah-plan" data-preset={p.id}>
        <p style={{ margin: '0 0 .6rem', fontSize: '.75rem', textTransform: 'uppercase', letterSpacing: '.08em', opacity: 0.6 }}>
          {p.copy.title}
        </p>
        <Core.Steps>
          {(step) => (
            <>
              <span>{step.text}</span>
              <Core.StepEditor step={step} />
            </>
          )}
        </Core.Steps>
        <div className="ah-actions">
          {p.copy.accept && <Core.Accept>{p.copy.accept}</Core.Accept>}
          {p.copy.revise && <Core.Revise>{p.copy.revise}</Core.Revise>}
          {p.copy.edit && <Core.Edit>{p.copy.edit}</Core.Edit>}
        </div>
        <VersionNote />
        <Core.DeviationNote />
      </div>
    </Core>
  );
}

Give this to your coding agent (shadcn MCP: npx shadcn@latest mcp init --client claude):

Add the @agentsandhumans registry to components.json:
{
  "registries": {
    "@agentsandhumans": "https://agentsandhumans.ai/api/registry/{name}.json"
  }
}
Then install plan-preview (npx shadcn@latest add @agentsandhumans/plan-preview) and wire it into the flow where your agent agreeing on the shape of the work before the agent does it.

Field Notes

SEP 14 · Clipped · via Maggie Appleton ContextDelegationPlanning

Maggie Appleton says agents start work knowing too little about you

Shared ground is what two people know before one asks the other for help: the goal, the history, who matters. An agent has almost none, so it guesses. Appleton wants a screen that shows what it understood before it acts, so you can fix it.

AUG 28 · Clipped · via Maggie Appleton DelegationHuman HandoffPlanning

Maggie Appleton says design, not the model, now sets the pace

Appleton's argument shifts the pressure: model capability is no longer the slow part, design decisions are. The patterns teams name now will be the defaults agentic products ship with, and reversing bad defaults in production is expensive.

Documented sightings (8) — anatomy-mapped observations from vendor materials

A dedicated planning interface helps developers decompose tasks, assign them to parallel agent sessions, and clarify execution order and dependencies before work begins.

arXiv · Sep 29, 2026

The LLM planned multi-step queries across corpora to answer reporter questions, decomposing intent into structured retrieval steps.

arXiv · Sep 28, 2026

Developers concentrate supervisory effort in the planning stage to reduce downstream intervention needs.

arXiv · Sep 22, 2026

The piece centers on how humans and agents negotiate shared plans as collaborative artifacts rather than one-way instructions.

Maggie Appleton · Sep 14, 2026

Trace analysis reveals planning failures where agents lose track of multi-step task constraints across 12 real-world desktop applications.

arXiv · Sep 2, 2026
Anthropic

Plan is a gate: nothing executes until the human accepts. Editable before acceptance; deviations surface as plan updates.

Anthropic docs · Apr 20, 2026
OpenAI

Plan is a trace: step list updates as the task runs, but execution never waits for approval — informative, not authorizing.

OpenAI docs · Apr 20, 2026

The agent autonomously sequences hypothesis generation, training runs, and result interpretation as a self-directed research plan.

GitHub · Aug 29, 2026

Side by Side

Anatomy part AnthropicOpenAI
Plan steps researched plan presented before any editlive step list during cloud run
Edit human edits or comments before accept—
Accept explicit plan approval gate—
Reject & revise reject with direction—
Deviation note plan updates surfaced mid-runsteps update as work proceeds

What Vendors Call It

Chart unlocks as the corpus grows — n = 0 mentions across 0 quarters.

  • Anthropic Plan mode
  • OpenAI task planning / plan tool
  • Cognition (Devin) Devin's plan

Tensions & Failure Modes

  • Plans are cheap: a good-looking plan is not a good plan
  • Granularity: step lists too coarse to review, too fine to read
  • Staleness: the plan the human approved is not always the plan that runs

What we are learning from the community

Readers who build with Planning add what they found: questions, use cases, critiques, other ways to solve it. An editor reads each one before it shows here. Nothing here changes the pattern's status on its own.

This pattern has no contributions yet. The form below sends the first one.

Add to this pattern

A post, a comment, a thread or a repo. Leave it empty to write it here.

Or the name you post under. It shows beside your contribution.

We store a one-way hash of it, not the address.

Up to 2,000 characters. Leave it empty if the link says it all.

The Story So Far

  1. Sep 28, 2026 drift: new → too-early (30d sightings 2 → 1)
  2. Aug 30, 2026 Seeded from SPEC v1.5 Phase 0: Anthropic-first corpus with OpenAI counterparts
  3. Aug 27, 2026 comparison Codex shows you the plan. Claude Code makes you approve it. The same transparency artifact — a step list — plays two different roles depending on whether execution waits for you.

Related Patterns