---
url: >-
  https://adk.nht.io/api/@nhtio/adk/batteries/orchestration/interfaces/PlanStore.md
description: >-
  The one contract for plans and runs — not a second store. **Every method is
  async**; nothing here is sync-or-async. A durable plan store is I/O by nature,
  the lifecycle transition is inherently a round-trip, and the cell seam is
  async too, so a uniform `Promise` surface is one less thing for eleven work
  packages to get inconsistently right.
---

# Interface: PlanStore

Defined in: [src/batteries/orchestration/store.ts:22](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L22)

The one contract for plans and runs — not a second store. **Every method is async**; nothing
here is sync-or-async. A durable plan store is I/O by nature, the lifecycle transition is
inherently a round-trip, and the cell seam is async too, so a uniform `Promise` surface is one
less thing for eleven work packages to get inconsistently right.

The store commits; it does not validate. Deciding "is an evaluator wired", "is this tool on the
tier-C allowlist", or "does this reference taint a call arg" requires battery knowledge a BYO
store has no access to, and putting it here would force every store implementor to reimplement
the validator. So the split is: **the battery validates, the store commits.**

## Methods

### appendOps()

```ts
appendOps(
   planId: string,
   ops: PlanOp[],
expectedRevision?: number): Promise<AppendResult>;
```

Defined in: [src/batteries/orchestration/store.ts:69](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L69)

Append ops. **Rejects unless the plan is `editable`**, checked in the SAME COMMIT as the
append — this is what makes `reviewable` and `executable` actually frozen, at the only
boundary that can enforce it. `transition()` cannot enforce this because it is not on the
append path: without the check, ops could change a frozen plan's content and digest while its
stored state stayed frozen, and in the `executable` case the `ApprovalRecord` would remain
bound to the PRIOR digest — making the plan executable with content that was never approved.
It returns `not_editable` with the actual state rather than throwing, so a stale writer learns
what happened.

`expectedRevision`, when given, additionally rejects if the log has moved (optimistic
concurrency for a single author); omit it for the multi-writer CRDT case, where convergence
is the concurrency story.

#### Parameters

| Parameter           | Type                                          |
| ------------------- | --------------------------------------------- |
| `planId`            | `string`                                      |
| `ops`               | [`PlanOp`](../types/type-aliases/PlanOp.md)\[] |
| `expectedRevision?` | `number`                                      |

#### Returns

`Promise`<[`AppendResult`](../type-aliases/AppendResult.md)>

***

### appendRunEvents()

```ts
appendRunEvents(
   planId: string,
   runId: string,
events: RunEvent[]): Promise<void>;
```

Defined in: [src/batteries/orchestration/store.ts:139](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L139)

Ordered, atomic-as-a-batch, and scoped to the claimed run. Takes an ARRAY and commits it
atomically as a batch, because the commit protocol depends on it: `node_settled` + every
`edge_taken` + the new `frontier_snapshot` are ONE commit. A backend that cannot do this is
not conforming.

#### Parameters

| Parameter | Type                                              |
| --------- | ------------------------------------------------- |
| `planId`  | `string`                                          |
| `runId`   | `string`                                          |
| `events`  | [`RunEvent`](../types/type-aliases/RunEvent.md)\[] |

#### Returns

`Promise`<`void`>

***

### claimRun()

```ts
claimRun(
   planId: string,
   expectedDigest: string,
resumeRunId?: string): Promise<ClaimRunResult>;
```

Defined in: [src/batteries/orchestration/store.ts:131](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L131)

Claim the plan's ONE run, or RE-ENTER it to resume. Conditional and durable: requires the
plan `executable` at `expectedDigest`. Without `resumeRunId` it starts a run and succeeds
only if none has ever been claimed. With `resumeRunId` it re-enters that specific run and
succeeds only if that run exists and is not already settled — this is the contractual
difference between a permitted re-entry and a prohibited second run, which a start-only
operation could not express.

"Settled" here means COMPLETED, and the distinction is load-bearing. `aborted` and `halted`
are stopping points, not endings: the interruption taxonomy classifies a turn abort as
resumable with the frontier intact at the same digest, so a store that treats every
`run_settled` as terminal makes `resumeRunId` answer `run_already_settled` for exactly the
cases it exists to serve. A conforming store must admit re-entry after `aborted`/`halted`
and refuse it after `completed`.

This — not the optional lock — is what enforces "one plan, at most one run, ever": the lock
seam is an availability measure a deployment may omit, so the invariant cannot rest on it.
The executor MUST claim before invoking any node.

#### Parameters

| Parameter        | Type     |
| ---------------- | -------- |
| `planId`         | `string` |
| `expectedDigest` | `string` |
| `resumeRunId?`   | `string` |

#### Returns

`Promise`<[`ClaimRunResult`](../type-aliases/ClaimRunResult.md)>

***

### clonePlan()

```ts
clonePlan(
   sourcePlanId: string,
   newPlanId: string,
atRevision?: number): Promise<CreateResult>;
```

Defined in: [src/batteries/orchestration/store.ts:53](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L53)

Mint `newPlanId` in `editable`, seeded with the folded state of `sourcePlanId` at
`atRevision` (default: its current revision), carrying provenance
`{parent, parentDigest, parentRevision}`. Inherits NO approval and NO run — a clone is cold
by construction. Atomic: either the clone exists complete, or not at all.

#### Parameters

| Parameter      | Type     |
| -------------- | -------- |
| `sourcePlanId` | `string` |
| `newPlanId`    | `string` |
| `atRevision?`  | `number` |

#### Returns

`Promise`<[`CreateResult`](../type-aliases/CreateResult.md)>

***

### createPlan()

```ts
createPlan(planId: string, meta?: {
  label?: string;
  provenance?: InstantiatedFrom;
}): Promise<CreateResult>;
```

Defined in: [src/batteries/orchestration/store.ts:35](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L35)

Mint a new plan in `editable` at **revision 0 with a genuinely EMPTY op log**. Rejects a
duplicate id.

Bounds are **genesis content, not an op**: the fold starts from `DEFAULT_PLAN_BOUNDS` and
`set_bounds` ops override it. An earlier draft said the log "begins with an implied
`set_bounds`", which was incoherent — a `PlanOp` requires `opId`/`actorId`/`lamport`/`at` and
`createPlan` has none of those to mint, and it left `readOps`, `throughRevision` and
`rawDiff` ambiguous about whether revision 0 contained an op. So: `readOps` on a fresh plan
returns `[]`, revision 0 is the empty fold, and the first authoring op makes revision 1. The
fold's *seed* is a constant; the log is exclusively authored ops.

#### Parameters

| Parameter          | Type                                                                                                    | Description                                                                                                                                                                                                                                                           |
| ------------------ | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `planId`           | `string`                                                                                                | -                                                                                                                                                                                                                                                                     |
| `meta?`            | { `label?`: `string`; `provenance?`: [`InstantiatedFrom`](../types/interfaces/InstantiatedFrom.md); } | -                                                                                                                                                                                                                                                                     |
| `meta.label?`      | `string`                                                                                                | -                                                                                                                                                                                                                                                                     |
| `meta.provenance?` | [`InstantiatedFrom`](../types/interfaces/InstantiatedFrom.md)                                           | Instantiation lineage, when the plan comes from a template. The store persists it and `readProvenance` returns it — without this parameter `instantiate()` had nowhere to write it, since `PlanOp` cannot carry provenance and `clonePlan` writes only clone lineage. |

#### Returns

`Promise`<[`CreateResult`](../type-aliases/CreateResult.md)>

***

### list()

```ts
list(filter?: {
  state?: PlanState;
}): Promise<PlanSummary[]>;
```

Defined in: [src/batteries/orchestration/store.ts:170](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L170)

Summaries of every plan, optionally filtered by lifecycle state.

#### Parameters

| Parameter       | Type                                                               |
| --------------- | ------------------------------------------------------------------ |
| `filter?`       | { `state?`: [`PlanState`](../types/type-aliases/PlanState.md); } |
| `filter.state?` | [`PlanState`](../types/type-aliases/PlanState.md)                  |

#### Returns

`Promise`<[`PlanSummary`](../types/interfaces/PlanSummary.md)\[]>

***

### readApproval()

```ts
readApproval(planId: string): Promise<ApprovalRecord | undefined>;
```

Defined in: [src/batteries/orchestration/store.ts:167](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L167)

The approval record persisted with the `reviewable → executable` transition, if the plan is
`executable`.

#### Parameters

| Parameter | Type     |
| --------- | -------- |
| `planId`  | `string` |

#### Returns

`Promise`<[`ApprovalRecord`](../types/interfaces/ApprovalRecord.md) | `undefined`>

#### Remarks

**The DIGEST on this record is bound; the AUTHORITY SET is only as trustworthy as the caller
that wrote it.** `transition` proves the plan is at `expectedDigest` before it commits, so a
record whose `digest` disagrees with the plan cannot be persisted. It does NOT — and cannot —
check that `authoritySet` matches the plan's reachable claims: recomputing that set requires
walking the graph and knowing what a `call` node is, which is battery knowledge a BYO store
does not have. That check lives in approvePlan, which recomputes the set and asserts
set-equality BEFORE calling `transition`.

So a caller that bypasses `approvePlan` and calls `transition` directly can persist a record
claiming an authority set the plan never had. **That forged set grants nothing**: the executor
never reads `authoritySet`, and what a run may actually invoke is bounded by the tier-C
`InvocableTools` allowlist enforced at freeze. The damage is to the AUDIT TRAIL — anyone
reading this record back is told the operator approved something they did not.

Treat `approvePlan` as the only supported way to reach `executable`. If you display or audit
this record, recompute the authority set from the plan with `computeAuthoritySet` rather than
trusting the stored copy.

***

### readOps()

```ts
readOps(planId: string, opts?: {
  sinceLamport?: number;
  throughRevision?: number;
}): Promise<PlanOp[]>;
```

Defined in: [src/batteries/orchestration/store.ts:77](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L77)

Read the op log. `sinceLamport` filters by clock; `throughRevision` bounds it to a REVISION
PREFIX, which is what `rawPlan({revision})` and `rawDiff(a, b)` need — a Lamport value is not
a revision selector, and without this a conforming store could not serve a historical view at
all. Rejects a revision the log never reached rather than silently returning everything.

#### Parameters

| Parameter               | Type                                                           |
| ----------------------- | -------------------------------------------------------------- |
| `planId`                | `string`                                                       |
| `opts?`                 | { `sinceLamport?`: `number`; `throughRevision?`: `number`; } |
| `opts.sinceLamport?`    | `number`                                                       |
| `opts.throughRevision?` | `number`                                                       |

#### Returns

`Promise`<[`PlanOp`](../types/type-aliases/PlanOp.md)\[]>

***

### readProvenance()

```ts
readProvenance(planId: string): Promise<PlanProvenance | undefined>;
```

Defined in: [src/batteries/orchestration/store.ts:83](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L83)

Clone lineage, for `RawPlanView.provenance`. `undefined` for a plan that is not a clone.

#### Parameters

| Parameter | Type     |
| --------- | -------- |
| `planId`  | `string` |

#### Returns

`Promise`<[`PlanProvenance`](../types/type-aliases/PlanProvenance.md) | `undefined`>

***

### readRunEvents()

```ts
readRunEvents(planId: string, runId?: string): Promise<RunEvent[]>;
```

Defined in: [src/batteries/orchestration/store.ts:142](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L142)

The claimed run's event log. `runId` omitted reads the plan's only run.

#### Parameters

| Parameter | Type     |
| --------- | -------- |
| `planId`  | `string` |
| `runId?`  | `string` |

#### Returns

`Promise`<[`RunEvent`](../types/type-aliases/RunEvent.md)\[]>

***

### readState()

```ts
readState(planId: string): Promise<{
  digest: string;
  revision: number;
  state: PlanState;
}>;
```

Defined in: [src/batteries/orchestration/store.ts:110](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L110)

The plan's lifecycle state NOW, alongside its current digest and revision. `state` is not a
`PlanOp`, so it cannot be folded from the log; it lives here, and a historical revision has
none to report. `readState` exists because `list({state})` needs a durable, race-safe state
read.

#### Parameters

| Parameter | Type     |
| --------- | -------- |
| `planId`  | `string` |

#### Returns

`Promise`<{
`digest`: `string`;
`revision`: `number`;
`state`: [`PlanState`](../types/type-aliases/PlanState.md);
}>

***

### transition()

```ts
transition(planId: string, t: TransitionRequest): Promise<TransitionResult>;
```

Defined in: [src/batteries/orchestration/store.ts:102](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/store.ts#L102)

The ONE atomic lifecycle operation, and deliberately a NARROW one: it proves the plan is in
`expected.state` at `expectedDigest`, checks the target is a legal successor, and applies it —
persisting `approval` in the SAME COMMIT for `reviewable → executable`. It returns the losing
outcome rather than throwing, so a caller that lost can read what actually happened.

A store does NOT evaluate policy. It cannot: deciding "is an evaluator wired", "is this tool
on the tier-C allowlist", or "does this reference taint a call arg" requires battery knowledge
a BYO store has no access to, and putting it here would force every store implementor to
reimplement the validator. So the split is: **the battery validates, the store commits.**

The digest is what makes that safe rather than racy: the battery validates content at digest
D and the store commits only if the plan is still at D, so a concurrent edit invalidates the
transition instead of slipping past an already-passed check. A direct caller bypassing
`freezePlan` can still reach `transition` — that is a misuse the docs name, not a hole the
store can close, exactly as a direct `appendOps` caller can bypass the authoring tools.

#### Parameters

| Parameter | Type                                                        |
| --------- | ----------------------------------------------------------- |
| `planId`  | `string`                                                    |
| `t`       | [`TransitionRequest`](../type-aliases/TransitionRequest.md) |

#### Returns

`Promise`<[`TransitionResult`](../type-aliases/TransitionResult.md)>
