# Plans (/docs/plans)



Plans are one half of the **cross-session state**
[thematic island](/docs/concept-clusters#what-lives-outside-the-cluster-pattern) —
paired with [memory](/docs/memory). Plans span turns within or
across sessions; memory spans sessions. Both outlive a single
session arc; neither fits the runtime-lifecycle cluster.

A `Plan` is the variable surface for work that spans more than one
turn: the steps the agent committed to, which session executed
each one, what evidence each one produced, and the
revision-by-revision history of how the plan changed. The
substrate doesn't pick the steps — the application's planner does
— but it owns the persistence shape, the dependency-aware step
picker, and the revision audit trail. See [Lineage](/docs/lineage)
for how plans join sessions, and [Subagents](/docs/subagents) for
how a step's `suggestedAgent` routes to a spec.

Ships from `@pleach/core/plans`. `PlanManager` is the orchestrator;
`Plan`, `PlanStep`, and `PlanRevision` are the wire shapes.

```typescript
import {
  PlanManager,
  type Plan,
  type PlanStep,
  type PlanRevision,
} from "@pleach/core/plans";
```

<SourceMeta source="{ label: &#x22;src/plans/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/plans&#x22; }" />

## `Plan` shape [#plan-shape]

| Field              | Type         | Notes                                           |
| ------------------ | ------------ | ----------------------------------------------- |
| `planId`           | string       | 10-char id, assigned at create                  |
| `title`            | string       | Human-readable                                  |
| `objective`        | string       | High-level goal                                 |
| `status`           | enum         | `active` / `completed` / `paused` / `abandoned` |
| `steps`            | `PlanStep[]` | Ordered                                         |
| `createdAt`        | ISO-8601     | UTC                                             |
| `createdInSession` | string       | Session that created the plan                   |
| `createdInChat`    | string       | Chat that created the plan                      |
| `updatedAt`        | ISO-8601     | Bumped on every revision                        |
| `revision`         | number       | Monotonic; bumped on every mutation             |
| `tags`             | string\[]    | Optional categorization                         |

`status` flips to `completed` automatically when every step is
`completed` or `skipped` — `PlanManager.updateStep` moves the
record from the `active` namespace to the `completed` namespace at
that boundary.

## `PlanStep` shape [#planstep-shape]

| Field                | Type                   | Notes                                                     |
| -------------------- | ---------------------- | --------------------------------------------------------- |
| `stepId`             | string                 | Stable across revisions — `step_` + 8-char id             |
| `title`              | string                 | Short label                                               |
| `description`        | string                 | What this step should accomplish                          |
| `status`             | enum                   | `pending` / `active` / `completed` / `skipped` / `failed` |
| `executedInSession`  | string \| undefined    | Session that ran this step                                |
| `executedInChat`     | string \| undefined    | Chat that ran this step                                   |
| `suggestedAgent`     | string \| undefined    | Recommended spec name                                     |
| `suggestedTools`     | string\[] \| undefined | Expected tool calls                                       |
| `dependsOn`          | string\[] \| undefined | Other `stepId`s that must complete first                  |
| `completionEvidence` | object \| undefined    | `canvasCards`, `jobIds`, `summary`                        |
| `updatedAt`          | ISO-8601               | Bumped on every step mutation                             |

`stepId` stability is the load-bearing invariant — `revisePlan`
preserves `completionEvidence`, `executedInSession`, and
`executedInChat` for steps whose `stepId` survives the revision,
even if their position or surrounding steps change. Steps without
a passed-in `stepId` get a new one and start clean.

## `PlanManager` methods [#planmanager-methods]

| Method                                          | Returns            | Effect                                                                                       |
| ----------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------- |
| `createPlan(input)`                             | `Plan`             | Assigns `planId`, stamps revision 1, writes the initial `PlanRevision` snapshot              |
| `updateStep(planId, stepId, update, sessionId)` | `Plan`             | Mutates one step; auto-flips plan to `completed` when all steps are `completed` or `skipped` |
| `revisePlan(planId, revision, sessionId)`       | `Plan`             | Replace `title`, `objective`, or the full step list; rejects revisions to `completed` plans  |
| `getActivePlans()`                              | `Plan[]`           | Up to 50                                                                                     |
| `getCompletedPlans()`                           | `Plan[]`           | Up to 50                                                                                     |
| `getPlan(planId)`                               | `Plan \| null`     | Checks active first, then completed                                                          |
| `getRevisions(planId)`                          | `PlanRevision[]`   | Sorted ascending by revision                                                                 |
| `getNextStep(plan)`                             | `PlanStep \| null` | First `pending` step whose `dependsOn` are all `completed` or `skipped`                      |
| `buildActivePlanSection(plans)`                 | string             | Renders an active-plan summary block for system-prompt injection                             |

`getNextStep` is pure; the rest read or write through the
`BaseStore` the manager was constructed with.

## Revision history [#revision-history]

Every mutation writes a `PlanRevision` to a per-plan namespace
alongside the plan itself.

| Field               | Type     | Notes                                                      |
| ------------------- | -------- | ---------------------------------------------------------- |
| `revision`          | number   | Matches `Plan.revision` at the time of the snapshot        |
| `snapshot`          | `Plan`   | `structuredClone` of the plan at this revision             |
| `changeDescription` | string   | Free-text; `updateStep` writes `Step "<title>" → <status>` |
| `sessionId`         | string   | Which session made the change                              |
| `timestamp`         | ISO-8601 | UTC                                                        |

Revisions are append-only. `getRevisions(planId)` returns them
sorted ascending so a UI can render the plan's evolution as a
timeline without re-sorting.

## Example [#example]

```typescript
import { PlanManager } from "@pleach/core/plans";
import { MyStore } from "./my-store";

const plans = new PlanManager(new MyStore(), orgId, userId);

const plan = await plans.createPlan({
  title:     "Q3 vendor evaluation",
  objective: "Pick a vendor for the queue rewrite.",
  steps: [
    { title: "List candidates",  description: "Pull from prior research." },
    { title: "Run benchmarks",   description: "p99 + cost.",
      dependsOn: [] /* filled in after createPlan assigns stepIds */ },
    { title: "Write recommendation", description: "1-pager." },
  ],
  sessionId: session.id,
  chatId:    chat.id,
});

// Pick what to do next — dependency-aware.
const next = plans.getNextStep(plan);
if (next) {
  await plans.updateStep(
    plan.planId,
    next.stepId,
    { status: "active", executedInSession: session.id, executedInChat: chat.id },
    session.id,
  );
}
```

## Revising mid-flight [#revising-mid-flight]

The reader can see how `revisePlan` carries `stepId`s forward so
completed work survives a re-plan.

```typescript
const current = await plans.getPlan(plan.planId);
if (!current) throw new Error("plan vanished");

const revised = await plans.revisePlan(
  current.planId,
  {
    steps: [
      // Pass existing stepIds back in to preserve their evidence + executedIn* fields.
      ...current.steps.filter((s) => s.status === "completed"),
      { title: "Cross-check sources", description: "Verify two corroborating refs." },
      { title: "Write recommendation", description: "1-pager." },
    ],
    changeDescription: "Inserted cross-check after benchmark results came in.",
  },
  session.id,
);
```

Steps without a `stepId` get a fresh one and start `pending`. Steps
whose `stepId` survives the revision keep `completionEvidence`,
`executedInSession`, and `executedInChat` even if their position
shifts.

## Walking revision history [#walking-revision-history]

The snippet renders a plan's evolution by reading revisions ascending.

```typescript
const history = await plans.getRevisions(plan.planId);

for (const rev of history) {
  console.log(
    `r${rev.revision}`,
    rev.changeDescription,
    `(${rev.snapshot.steps.length} steps)`,
  );
}
```

`changeDescription` is the free-text label `updateStep` and
`revisePlan` write; the snapshot is a full `structuredClone` of the
plan at that revision, so a UI can diff one snapshot against the
next without re-querying.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Agents" href="/docs/agents" description="The persistent per-spec profile a step's `suggestedAgent` resolves against." />

  <Card title="Subagents" href="/docs/subagents" description="Steps with a `suggestedAgent` route to subagent specs." />

  <Card title="Lineage" href="/docs/lineage" description="Plans persist across chats — lineage joins the sessions that executed each step." />

  <Card title="Query" href="/docs/query" description="Aggregate readers project plan + revision history into a review dashboard." />

  <Card title="Subpath exports" href="/docs/subpath-exports" description="`@pleach/core/plans` ships PlanManager + the public types." />
</Cards>
