---
title: HTTP API
description: "Every route of the Orchestrator's HTTP API, by access band, and the types of its bodies and answers."
---

# HTTP API

The Orchestrator's HTTP API: what the `jr2` CLI, the Console, a webhook and the Harness call. Each route is in one access band, by the credential it asks for ([ADR-0014](decisions/0014-observation-is-open-run-state-is-not.md)). Send a token as `Authorization: Bearer <token>`. An error answer is `{ error }`, with the status the route gives. A path segment `:name` is a parameter; `:name{regex}` is a parameter that must match the regex.

Bands: [Open](#band-open), [Authenticated](#band-authenticated), [Instance token only](#band-instanceonly), [Publish token only](#band-publishonly). The types the bodies and answers name are in [Types](#types).

<a id="band-open"></a>

## Open

No token. Structure and observation: no run context, and no control.

<a id="get-healthz"></a>

### `GET /healthz`

- Summary: Liveness, and what this instance is: its Kit version and image content hash.
- Response: { ok: true, version, hash? }
- Stream: no
- Decisions: [ADR-0009](decisions/0009-cli-and-instance-interface.md), [ADR-0019](decisions/0019-one-converging-command-against-the-current-context.md)

<a id="get-readyz"></a>

### `GET /readyz`

- Summary: Readiness.
- Response: { ready: true }
- Stream: no
- Decisions: [ADR-0009](decisions/0009-cli-and-instance-interface.md)

<a id="get-workflows"></a>

### `GET /workflows`

- Summary: The Workflows a run can start, each with its origin, and each Chart wrapper's named Charts in its entry. A named Chart that cannot run now says why.
- Response: [`WorkflowListing`](#type-workflowlisting)[]
- Stream: no
- Decisions: [ADR-0014](decisions/0014-observation-is-open-run-state-is-not.md), [ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)

<a id="get-assembly"></a>

### `GET /assembly`

- Summary: The current Assembly and the Assemblies that live runs hold.
- Response: [`AssemblyView`](#type-assemblyview)
- Stream: no
- Decisions: [ADR-0069](decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)

<a id="get-workflows-wrapper-chart-schema"></a>

### `GET /workflows/:wrapper/chart-schema`

- Summary: The Chart grammar, narrowed to the Open body of one Chart wrapper.
- Response: JSON Schema
- Stream: no
- Decisions: [ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)

<a id="get-workflows-name-runs-runid-machine"></a>

### `GET /workflows/:name/runs/:runId/machine`

- Summary: The Machine that a run binds, with each bound Chart compiled in, which the Console draws.
- Response: [`MachineDoc`](#type-machinedoc)
- Stream: no
- Decisions: [ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)

<a id="get"></a>

### `GET /`

- Summary: The Console page to a browser; a 404 to a JSON caller.
- Response: text/html (the Console shell)
- Stream: no
- Decisions: [ADR-0032](decisions/0032-the-console-unlocks-with-the-instance-token.md)

<a id="get-workflows-name"></a>

### `GET /workflows/:name`

- Summary: The Console page to a browser; to a JSON caller, the workflow and its declared input schema.
- Response: { name, machineId, input: JSON Schema | null }, or text/html
- Stream: no
- Decisions: [ADR-0032](decisions/0032-the-console-unlocks-with-the-instance-token.md), [ADR-0033](decisions/0033-a-machine-declares-the-input-that-starts-it.md)

<a id="get-workflows-name-machine"></a>

### `GET /workflows/:name/machine`

- Summary: The registered template Machine's structure, which the Console draws.
- Response: [`MachineDoc`](#type-machinedoc)
- Stream: no
- Decisions: [ADR-0014](decisions/0014-observation-is-open-run-state-is-not.md)

<a id="get-workflows-name-runs"></a>

### `GET /workflows/:name/runs`

- Summary: The live runs of one workflow, as observations: identity and state value, no context.
- Response: [`RunObservation`](#type-runobservation)[]
- Stream: no
- Decisions: [ADR-0014](decisions/0014-observation-is-open-run-state-is-not.md)

<a id="get-workflows-name-events"></a>

### `GET /workflows/:name/events`

- Summary: The activity of every run of one workflow, as observations, on one connection.
- Response: SSE: runs, status, emit (type only), retry, gone
- Stream: yes: Server-Sent Events
- Decisions: [ADR-0014](decisions/0014-observation-is-open-run-state-is-not.md), [ADR-0022](decisions/0022-observation-is-a-level-triggered-feed.md)

<a id="get-workflows-name-runs-runid-events"></a>

### `GET /workflows/:name/runs/:runId/events`

- Summary: One live run's activity, as observations, until it settles.
- Response: SSE: status, emit (type only), retry
- Stream: yes: Server-Sent Events
- Decisions: [ADR-0014](decisions/0014-observation-is-open-run-state-is-not.md), [ADR-0022](decisions/0022-observation-is-a-level-triggered-feed.md)

<a id="get-assets-style-css"></a>

### `GET /assets/style.css`

- Summary: The Console's stylesheet.
- Response: text/css
- Stream: no
- Decisions: [ADR-0032](decisions/0032-the-console-unlocks-with-the-instance-token.md)

<a id="get-assets-elk-js"></a>

### `GET /assets/elk.js`

- Summary: The Console's graph layout library (the elkjs bundle).
- Response: text/javascript
- Stream: no
- Decisions: [ADR-0032](decisions/0032-the-console-unlocks-with-the-instance-token.md)

<a id="get-assets-file"></a>

### `GET /assets/:file{[\w.-]+\.ts}`

- Summary: A Console source module, with its types erased.
- Response: text/javascript
- Stream: no
- Decisions: [ADR-0034](decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md)

<a id="get-assets-components-file"></a>

### `GET /assets/components/:file{[\w.-]+\.ts}`

- Summary: A Console view module, with its types erased.
- Response: text/javascript
- Stream: no
- Decisions: [ADR-0034](decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md)

<a id="get-assets-vendor-file"></a>

### `GET /assets/vendor/:file`

- Summary: Preact's browser ESM, which the Console's import map names.
- Response: text/javascript
- Stream: no
- Decisions: [ADR-0034](decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md)

<a id="band-authenticated"></a>

## Authenticated

Any token jr2 minted: the Instance token, or a Sandbox token. A Sandbox token reaches only what its own Sandbox owns.

<a id="get-agents-instanceid-surface"></a>

### `GET /agents/:instanceId/surface`

- Summary: An Agent's Menu and this Turn's Allowed picks. A Sandbox token reads only its own Sandbox's.
- Response: [`AgentSurfaceView`](#type-agentsurfaceview)
- Stream: no
- Decisions: [ADR-0013](decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md), [ADR-0029](decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)

<a id="post-agents-instanceid-events"></a>

### `POST /agents/:instanceId/events`

- Summary: Deliver an Agent's pick into the state that invoked it.
- Body: { type, ...payload }, checked against the pick's input schema
- Response: [`AgentDeliveryReceipt`](#type-agentdeliveryreceipt); 409 { refused: "outstanding", outstanding?, allowed }
- Stream: no
- Decisions: [ADR-0013](decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md), [ADR-0024](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md), [ADR-0029](decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)

<a id="get-agents-instanceid-deliveries-deliveryid"></a>

### `GET /agents/:instanceId/deliveries/:deliveryId`

- Summary: Long-poll the answer to an outstanding pick; 503 means ask again.
- Response: [`AgentDeliveryReceipt`](#type-agentdeliveryreceipt)
- Stream: no
- Decisions: [ADR-0013](decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md), [ADR-0024](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)

<a id="post-sandboxes-name-fetch"></a>

### `POST /sandboxes/:name/fetch`

- Summary: Ask the node cache to fetch one Repo that this Sandbox mounts. A Sandbox token asks only for its own.
- Body: { identity }
- Response: [`FetchAnswer`](#type-fetchanswer): { fetched } | { stale, asOf }
- Stream: no
- Decisions: [ADR-0053](decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md)

<a id="band-instanceonly"></a>

## Instance token only

The Instance token alone: run control, starting a run, and full run state. A Sandbox token is refused.

<a id="put-workflows-wrapper-charts-name"></a>

### `PUT /workflows/:wrapper/charts/:name`

- Summary: Admit and keep a runtime Chart under a name. Its Workflow is `<wrapper>/<name>`.
- Body: the Chart text alone
- Response: 201 { name, origin, hash } when new, 200 when it replaced one; 422 { error, report } when refused
- Stream: no
- Decisions: [ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)

<a id="delete-workflows-wrapper-charts-name"></a>

### `DELETE /workflows/:wrapper/charts/:name`

- Summary: Remove a runtime Chart. A live run of it keeps its own text.
- Response: 204
- Stream: no
- Decisions: [ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)

<a id="post-workflows-wrapper-charts-name-runs"></a>

### `POST /workflows/:wrapper/charts/:name/runs`

- Summary: Start a run of a named Chart, as `POST /workflows/<wrapper>%2F<name>/runs` does.
- Body: the Chart wrapper's input, without the Chart
- Response: 201 { runId, instanceId }; 409 when the named Chart cannot run now
- Stream: no
- Decisions: [ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)

<a id="post-workflows-name-runs"></a>

### `POST /workflows/:name/runs`

- Summary: Start a run of a registered workflow. A `dispatch` from another Orchestrator sends the header `jr2-dispatched-by`, and a repeated start with its key answers the run that it started.
- Body: the workflow's input, checked against its declared input schema
- Response: 201 { runId, instanceId }; 200 { runId, instanceId, existing: true } for a repeated dispatch; 422 { error, report } when the door refuses a Chart in the input
- Stream: no
- Decisions: [ADR-0033](decisions/0033-a-machine-declares-the-input-that-starts-it.md), [ADR-0058](decisions/0058-a-harness-answers-the-orchestrator-alone.md), [ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md), [ADR-0071](decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md)

<a id="get-runs"></a>

### `GET /runs`

- Summary: The live runs, with their full status.
- Response: [`RunStatus`](#type-runstatus)[]
- Stream: no
- Decisions: [ADR-0009](decisions/0009-cli-and-instance-interface.md), [ADR-0014](decisions/0014-observation-is-open-run-state-is-not.md)

<a id="get-repos"></a>

### `GET /repos`

- Summary: The Repos as the cluster reports them.
- Response: { dataPlane, repos: [`RepoStatus`](#type-repostatus)[] }
- Stream: no
- Decisions: [ADR-0048](decisions/0048-the-orchestrator-boots-without-its-repos.md), [ADR-0051](decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md)

<a id="get-runs-resolve"></a>

### `GET /runs/resolve`

- Summary: The run ids that start with a prefix (`?prefix=`, at least 4 characters).
- Response: { prefix, runIds: string[], truncated }
- Stream: no
- Decisions: [ADR-0009](decisions/0009-cli-and-instance-interface.md)

<a id="get-runs-runid"></a>

### `GET /runs/:runId`

- Summary: One run's status, live or settled, with its open Gates.
- Response: [`RunStatus`](#type-runstatus) & { gates: [`GateView`](#type-gateview)[] }
- Stream: no
- Decisions: [ADR-0009](decisions/0009-cli-and-instance-interface.md), [ADR-0011](decisions/0011-workflow-defined-events.md)

<a id="post-runs-runid-gates-gate-events"></a>

### `POST /runs/:runId/gates/:gate/events`

- Summary: Deliver an event to an open Gate of a run.
- Body: { type, ...payload }, checked against the Gate's schema for that event
- Response: { ok: true }; 409 { error, acceptsNow } when the run does not take it now
- Stream: no
- Decisions: [ADR-0011](decisions/0011-workflow-defined-events.md), [ADR-0013](decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)

<a id="get-runs-runid-events"></a>

### `GET /runs/:runId/events`

- Summary: One run's full activity until it settles; a settled run's final status, once.
- Response: SSE: status, emit, retry, admission, pick, placing, placed, child-error, effect, gone
- Stream: yes: Server-Sent Events
- Decisions: [ADR-0022](decisions/0022-observation-is-a-level-triggered-feed.md), [ADR-0023](decisions/0023-the-harness-prints-the-conversation.md), [ADR-0064](decisions/0064-a-workspace-waits-for-capacity.md), [ADR-0067](decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)

<a id="post-runs-runid-events"></a>

### `POST /runs/:runId/events`

- Summary: Cancel a run: it ends, and its Workspaces go unless `keep` is true.
- Body: { type: "CANCEL", keep?: boolean }
- Response: { ok: true }
- Stream: no
- Decisions: [ADR-0002](decisions/0002-duplex-streaming-actor.md), [ADR-0025](decisions/0025-cancel-ends-a-run-stop-parks-it.md)

<a id="band-publishonly"></a>

## Publish token only

The publish token alone, which `jr2 up` reads from the instance Secret. The Instance token is refused, because a publish puts code into the Orchestrator.

<a id="put-assembly"></a>

### `PUT /assembly`

- Summary: Publish the next Assembly. `jr2 up` calls it, and nothing else. A refused Assembly leaves the current one current.
- Body: the Assembly as a tar stream, with the headers `jr2-kit-version` and `jr2-assembly-hash`
- Response: 200 with the Assembly, its Workflows, the held Assemblies and the ones collected; 400, 409 or 422 with the reason
- Stream: no
- Decisions: [ADR-0069](decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)

## Types

<a id="type-workflowlisting"></a>

### `WorkflowListing`

```ts
type WorkflowListing = {
  name: string;
  origin: "machine" | ChartOrigin;
  wrapper?: {
    body: string;
    oneShot: boolean;
    charts: Array<{ name: string; origin: ChartOrigin; unrunnable?: string }>;
  };
  unrunnable?: string;
};
```

One entry of `GET /workflows` ([ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)): a Workflow and where it came from. A Chart wrapper's
entry carries its Open body, whether its door takes a Chart (`oneShot`), and its named Charts.

**Fields**

- `name`: `string`
- `origin`: `"machine" | ChartOrigin`
- `wrapper` (optional): `object`

  - `body`: `string`
  - `oneShot`: `boolean`
  - `charts`: `Array<{ name: string; origin: ChartOrigin; unrunnable?: string; }>`
- `unrunnable` (optional): `string`

<a id="type-assemblyview"></a>

### `AssemblyView`

```ts
type AssemblyView = {
  kitVersion: string;
  current: { hash: string; kitVersion: string; workflows: string[] } | null;
  held: HeldAssembly[];
};
```

`GET /assembly`'s body.

**Fields**

- `kitVersion`: `string`
- `current`: `{ hash: string; kitVersion: string; workflows: string[]; } | null`
- `held`: `HeldAssembly[]`

<a id="type-machinedoc"></a>

### `MachineDoc`

```ts
type MachineDoc = MachineBodyDoc & {
  workflow: string;
  opaqueStates?: string[];
  chartsNotDrawn?: Array<{ path: string; src: string; reason: string }>;
};
```

The serialized structure of a workflow's Machine.

**Fields**: each field of [`MachineBodyDoc`](#type-machinebodydoc), and these:

- `workflow`: `string`
- `opaqueStates` (optional): `string[]`

  States whose child list may be incomplete — see `opaqueStates`. Computed once here, on
  the server, so the notice reaches whoever renders the doc without every consumer re-walking it
  to discover the diagram is lying. Absent on the nested `MachineBodyDoc`s: it is a whole-doc
  question, and the root's walk already covers them.
- `chartsNotDrawn` (optional): `Array<{ path: string; src: string; reason: string; }>`

  A run's doc only (`runMachine`, [ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)): each bound Chart the doc could not draw, so its Open
  body shows the slot's own subgraph — the body's path in the run, its slot, and why (a stored
  Chart the door no longer admits is drift, D-B). Absent when every bound Chart is drawn.

<a id="type-runobservation"></a>

### `RunObservation`

```ts
type RunObservation = {
  runId: string;
  workflow: string;
  status: string;
  value: unknown;
  children: RunChild[];
  bound?: true;
  chartHashes?: string[];
  dispatchedBy?: Omit<DispatchedBy, "effect">;
  dispatched?: Array<{ slot: string; run: string }>;
};
```

A run as an UNAUTHENTICATED observer may see it: which run, of what workflow, and where in the
Machine it is. That is the whole set — enough to light up a state in the Console, and nothing
more.

What is absent is the point. `context` is the workflow's working data (branch names, ticket
bodies, review verdicts, Harness endpoints) and `instanceId`/`fault` name live infrastructure and
leak error text; all of it is STATE, which [ADR-0013](decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) guards behind the Instance token. `value` is
a tree of state KEYS — it is structure, and structure is already public (`/workflows/:name/machine`
serves the whole Machine). So an observer learns nothing here it could not read from the Machine
doc, except which states are lit.

`children` extends that to the child machines (which is where a run mostly lives), and it does NOT
widen the line: a [`RunChild`](#type-runchild) is keys and ids by construction, with no context field at any
depth. The one thing it adds is the spawn ids, which are the workflow's own labels for its
parallel work (`"F-1"`) — the same class of thing as a state key.

**Fields**

- `runId`: `string`
- `workflow`: `string`
- `status`: `string`
- `value`: `unknown`
- `children`: `RunChild[]`
- `bound` (optional): `true`

  The run binds a Chart ([ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)): the Console draws `GET /workflows/:name/runs/:runId/machine`.
  Structure, like `value`.
- `chartHashes` (optional): `string[]`

  The hashes of the Charts it binds now (see `RunStatus.chartHashes`). Structure: a hash
  of a Chart's text, which `runMachine` already serves on the open band.
- `dispatchedBy` (optional): `Omit<DispatchedBy, "effect">`

  The run that dispatched this one, its slot, and the chain's depth ([ADR-0071](decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md)). Routing — two
  ids, a slot name and a number — the class of thing `runId` and `value` are, so [ADR-0014](decisions/0014-observation-is-open-run-state-is-not.md)'s line
  is unmoved. The idempotency key is not: it names the caller's ledger, which is the caller's.
- `dispatched` (optional): `Array<{ slot: string; run: string; }>`

  The runs it dispatched, by slot ([ADR-0071](decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md)): the same routing as `dispatchedBy`, the other
  direction.

<a id="type-runstatus"></a>

### `RunStatus`

```ts
type RunStatus = RunRecord & {
  status: string;
  value: unknown;
  context: unknown;
  children: RunChild[];
  waiting: RunWaiting[];
  held: RunHeld[];
  fault?: string;
  reason?: string;
  assembly?: string;
  bound?: true;
  chartHashes?: string[];
  output?: unknown;
  dispatched?: Dispatched[];
};
```

A run's current observable state. `fault` carries the error message when status is "error"
(e.g. a gate invoked with a name outside the workflow's manifest — [ADR-0011](decisions/0011-workflow-defined-events.md)). A run that ends
"failed" ([ADR-0073](decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md)) has no `fault`: its `output` is the Failure's `{ reason, detail? }`.

**Fields**: each field of [`RunRecord`](#type-runrecord), and these:

- `status`: `string`
- `value`: `unknown`
- `context`: `unknown`
- `children`: `RunChild[]`

  The live child machines beneath the root — where most of a run actually is.
- `waiting`: `RunWaiting[]`

  Every Workspace waiting for capacity, and why ([ADR-0064](decisions/0064-a-workspace-waits-for-capacity.md)). Behind the token: the reason is
  the scheduler's or the quota's own words, which can name nodes and taints, so `observe`
  never carries it — the open band sees only that a Workspace is in `placing`.
- `held`: `RunHeld[]`

  Every pick of this run that is outstanding ([ADR-0024](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)): the Agent, its pick, and since when —
  the Console's "held since". There is no default time bound on a held Turn; this is how a
  human sees one. Behind the token, like `context`: a pick names what the Agent did.
- `fault` (optional): `string`
- `reason` (optional): `string`

  Why the HOST set this status, for the statuses the Machine did not choose — today `drifted`
  ([ADR-0030](decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md)). Distinct from `fault`, which is a running actor's own error; this is the store's
  account of a run it declined to resume, and until it was surfaced here nothing on any HTTP
  route could read it.
- `assembly` (optional): `string`

  The hash of the Assembly the run started under ([ADR-0069](decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)). Absent for a run no Assembly
  started.
- `bound` (optional): `true`

  The run binds at least one Chart ([ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)): its Machine is the bound one, `runMachine`.
- `chartHashes` (optional): `string[]`

  The hashes of the Charts the run binds now, distinct and sorted ([ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)). A run can bind a
  second Chart in its life (a planner whose first Chart reaches a ceiling picks again), so these,
  not the run id, say which bound Machine `runMachine` draws. Absent when it binds none.
- `output` (optional): `unknown`

  The Machine's output, for a run that is no longer live ([ADR-0071](decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md)): what a `dispatch` with
  `wait` settles with, and what `GET /runs/<id>` serves its foreign caller. Never for a live run,
  whose Machine has none yet. For a `failed` run, the Failure's `{ reason, detail? }` ([ADR-0073](decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md)),
  not a value of the Machine's output type. Behind the token, like `context`: it is the
  workflow's data.
- `dispatched` (optional): `Dispatched[]`

  The runs this run's `dispatch` Effects started, oldest first ([ADR-0071](decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md)): the chain's other
  direction, read off the caller's own ledger rows. A foreign dispatch's run is a run of the
  Orchestrator its target names. Absent when it started none.

<a id="type-repostatus"></a>

### `RepoStatus`

```ts
type RepoStatus = {
  key: string;
  url: string;
  identity?: string;
  bound: boolean;
  lastAttached?: string;
  nodes: RepoNodeState[];
};
```

One `Repo` resource as the instance reports it (`GET /repos`).

**Fields**

- `key`: `string`

  The cache key: the resource's name, the hostPath leaf, `/repos/<key>`.
- `url`: `string`

  The Binding's own spelling — what the cache clones.
- `identity` (optional): `string`
- `bound`: `boolean`

  A registered Machine binds it — never evicted by `jr2 gc`.
- `lastAttached` (optional): `string`

  When a run last attached it — the eviction clock for a Repo nothing binds. Absent while only
  the boot has stated it: a bound Repo no run has attached yet.
- `nodes`: `RepoNodeState[]`

<a id="type-gateview"></a>

### `GateView`

```ts
type GateView = {
  gate: string;
  path: string[];
  accepts: Array<{ name: string; description?: string; input: unknown }>;
  acceptsNow: string[];
  meta?: Record<string, unknown>;
};
```

One open gate as external callers discover it (`GET /runs/:id` — [ADR-0011](decisions/0011-workflow-defined-events.md)): the accepted
events with their input schemas as JSON Schema (what drives a form or a `jr2 send` prompt),
plus the workflow-supplied `meta` (what a UI renders and a webhook translator matches on).

**Fields**

- `gate`: `string`
- `path`: `string[]`

  The invoking state's actor path below the run root — where the gate lives in the Machine
  (what the Console's "parked here" pin resolves against its scope tree), stable whether the
  id was authored ("F-12") or derived. Never parse `gate` for this.
- `accepts`: `Array<{ name: string; description?: string; input: unknown; }>`
- `acceptsNow`: `string[]`

  The names of `accepts` the run would take from this Gate NOW, payload-blind ([ADR-0011](decisions/0011-workflow-defined-events.md),
  [ADR-0029](decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)): a send of another name is refused 409. `accepts` stays whole — it drives the forms.
- `meta` (optional): `Record<string, unknown>`

<a id="type-agentsurfaceview"></a>

### `AgentSurfaceView`

```ts
type AgentSurfaceView = {
  instanceId: string;
  runId: string;
  sandbox?: string;
  menu: SurfaceEvent[];
  allowed: string[];
};
```

One live agent surface, as the Harness reads it through its Custodian (`GET /agents/:iid/surface`
— [ADR-0013](decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md), [ADR-0029](decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)). Two answers, because they change at different rates:

  - `menu` is the Agent's MENU — every pick any state invoking this Agent offers, in a fixed
    order. The Harness presents it as the tools block, which is then byte-identical for every
    Turn of the conversation and never breaks the provider's prompt cache.
  - `allowed` is this Turn's ALLOWED PICKS — the invoking state's derived set, narrowed to what its
    guards would accept now, in Menu order. The Harness states it in the Briefing's Turn part,
    after the Frame's prompt ([ADR-0062](decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)). Enforced at delivery, not here.

`sandbox` is the Sandbox that may deliver here. It is not a secret from that pod (the pod IS the
sandbox), and serving it lets a caller fail loudly on a surface that is not its own.

**Fields**

- `instanceId`: `string`
- `runId`: `string`
- `sandbox` (optional): `string`
- `menu`: `SurfaceEvent[]`
- `allowed`: `string[]`

<a id="type-agentdeliveryreceipt"></a>

### `AgentDeliveryReceipt`

```ts
type AgentDeliveryReceipt = {
  outstanding?: true;
  since?: string;
  answered?: true;
  straightBack?: true;
  answer?: PickAnswer;
  stopped?: true;
  delivered: true;
  event: string;
  moved: boolean;
  turnComplete: boolean;
  allowed: string[];
  deliveryId: string;
};
```

The answer to one Agent delivery (`POST /agents/:iid/events`) — a receipt that DESCRIBES ITSELF
([ADR-0024](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)). The Harness renders it as prose, because a bare `deliveryId` told the Agent nothing
about whether it was finished, and the model answered that silence by calling again.

`turnComplete` is the HINT, never the guarantee (the abort on invocation end is): it is read off
the registration table right after delivery, so it says whether the state that asked for this
turn has stopped waiting. It fails conservatively — anything that made the read unreliable reads
`false`, which is today's behavior, never a false claim.

`deliveryId` keeps the outcome ADDRESSABLE after the fact ([ADR-0013](decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)): a pick the workflow is still
running when the POST answers is OUTSTANDING (`outstanding: true`), and its answer is read at
`GET /agents/:iid/deliveries/:id` — the same shape, `answered: true`, once the workflow is back at
Rest in the Agent's state or has left it ([ADR-0024](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)).

**Fields**

- `outstanding` (optional): `true`

  The workflow is running the pick: the answer comes on the delivery's poll. `allowed` is empty:
  no other pick is taken until it answers.
- `since` (optional): `string`

  ISO time the pick was delivered — with `outstanding`.
- `answered` (optional): `true`

  This receipt is a pick's answer, read at Rest (or once the Turn left the Agent's state): an outstanding
  pick's, a reply inside the pick's own send, or a guard straight back with no reply.
- `straightBack` (optional): `true`

  With `answered`: a guard straight back ([ADR-0024](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)) — the pick entered a state below the Agent's
  and came back inside its own send, nothing replying. The one answer that says "moved; nothing
  ran"; an outstanding pick's answer never carries it.
- `answer` (optional): [`PickAnswer`](#type-pickanswer)

  The last reply since the pick ([ADR-0024](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)); absent with `answered`: no reply came.
- `stopped` (optional): `true`

  With `answered` and `turnComplete`: something other than the pick's route left the Agent's
  state while the pick's work ran, and that exit stopped the work before it finished. `answer`,
  if any, is an earlier link's reply, kept ([ADR-0024](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)).
- `delivered`: `true`
- `event`: `string`

  The event delivered — the Agent reads its own pick back, by name.
- `moved`: `boolean`

  A transition accepted it. False means the pick was well-formed, arrived, and moved nothing —
  every transition for it was guarded false in the current state ([ADR-0029](decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)). Distinct from
  `turnComplete`: a pick can move the Machine WITHIN the invoking state, which is
  `moved: true, turnComplete: false`. Before this existed the two were indistinguishable, so an
  Agent whose pick a guard rejected was told the workflow was "still in the state that asked",
  and its only move was to call again.
- `turnComplete`: `boolean`

  The invoking state stopped waiting: this Agent's turn is over.
- `allowed`: `string[]`

  The Allowed picks read AFTER the delivery ([ADR-0029](decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)): what this Turn may pick now — narrowed by
  a pick that moved the Machine within the invoking state, empty once the Turn is over. The
  Harness renders it ("Allowed now: `finish`"); it does not pre-check against it.
- `deliveryId`: `string`

<a id="type-fetchanswer"></a>

### `FetchAnswer`

```ts
type FetchAnswer = { fetched: string } | { stale: string; asOf: string | null };
```

What one ask settles as. `fetched` is the landing's timestamp; `stale` is git's own words (or
this port's, when the budget ran out) beside the time the cache's objects are as of — `null`
when nothing has ever fetched it, which the program prints as "an unknown time".

**Forms**: it is one of these objects.

- Form 1: `fetched`

  - `fetched`: `string`
- Form 2: `stale`, `asOf`

  - `stale`: `string`
  - `asOf`: `string | null`

<a id="type-chartorigin"></a>

### `ChartOrigin`

```ts
type ChartOrigin = "file" | "runtime";
```

Where a named Chart came from ([ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)): the Assembly's own file, or a `PUT` at run time.

<a id="type-heldassembly"></a>

### `HeldAssembly`

```ts
type HeldAssembly = { hash: string; kitVersion: string; runs: string[] };
```

An Assembly the process holds ([ADR-0069](decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)), as `GET /assembly` lists it: the current one, and
every one a live run holds, each with the runs that hold it.

**Fields**

- `hash`: `string`
- `kitVersion`: `string`
- `runs`: `string[]`

<a id="type-machinebodydoc"></a>

### `MachineBodyDoc`

```ts
type MachineBodyDoc = {
  id: string;
  root: MachineStateDoc;
  transitions: MachineTransitionDoc[];
  events: MachineEventDoc[];
  seats: MachineSeat[];
  warnings: string[];
};
```

One Machine's structure, independent of what NAMES it (a `workflow` at the root, a `src` below).

**Fields**

- `id`: `string`

  Machine id.
- `root`: [`MachineStateDoc`](#type-machinestatedoc)
- `transitions`: `MachineTransitionDoc[]`
- `events`: `MachineEventDoc[]`

  The events THIS Machine declares, and only this one ([ADR-0049](decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md)): a nested Machine's ride its
  own `MachineBodyDoc`. Empty for a Machine not built by `jr2Setup`.
- `seats`: `MachineSeat[]`

  Who may send what, per state ([ADR-0015](decisions/0015-authoring-surface-absorbs-the-mechanism.md)): one entry per party on each state that invokes one —
  the audience rule's answer for this Machine, as `jr2 up` prints it. Empty for a Machine not
  built by `jr2Setup`.
- `warnings`: `string[]`

  What the build found legal and suspect ([ADR-0015](decisions/0015-authoring-surface-absorbs-the-mechanism.md)) — a mode nothing enters, a pick a Gate above
  takes from the Agent below, one name written for two parties on one path.

<a id="type-runchild"></a>

### `RunChild`

```ts
type RunChild = { id: string; src: string; status: string; value: unknown; children: RunChild[] };
```

One live child MACHINE of a run, and its own children below it. A workflow's root machine is
usually a coordinator — `coding` spawns a `featureWorkspace` per feature and the real work happens
in there — so the root's `value` alone says almost nothing about where a run IS. This is the rest.

Context-free BY CONSTRUCTION: there is no context field to forget to redact, which is what lets
`observe()` pass the whole tree through to unauthenticated observers untouched. `src` is the join
key ([`ChildMachineDoc`](#type-childmachinedoc)); `id` is the spawn id (`"F-1"`), which is how two live instances of
the same child machine are told apart.

**Fields**

- `id`: `string`
- `src`: `string`
- `status`: `string`
- `value`: `unknown`
- `children`: `RunChild[]`

<a id="type-dispatchedby"></a>

### `DispatchedBy`

```ts
type DispatchedBy = { run: string; slot: string; effect: string; depth: number; instance?: string };
```

Which run dispatched a run ([ADR-0071](decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md)): the caller's run id, the slot of its `dispatch` Effect, and
that Effect's ledger row id, which is the idempotency key — a start that carries a key some run
already carries answers that run. `depth` is the dispatched run's depth in the chain, one below
its caller, stated by the caller and recorded as sent: one meaning on the wire, on the binding
and on the row. `instance` is set for a foreign caller, whose Orchestrator this one cannot
verify: opaque text, never resolved.

**Fields**

- `run`: `string`
- `slot`: `string`
- `effect`: `string`
- `depth`: `number`
- `instance` (optional): `string`

<a id="type-runrecord"></a>

### `RunRecord`

```ts
type RunRecord = { runId: string; workflow: string; instanceId: string; dispatchedBy?: DispatchedBy };
```

The serializable identity of a run — what reconcile sees and what restore rebuilds from.
`dispatchedBy` is the run that started it through a `dispatch` Effect, and this run's depth in
that chain, as the caller sent it ([ADR-0071](decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md)); absent for a run a human or a client started.

**Fields**

- `runId`: `string`
- `workflow`: `string`
- `instanceId`: `string`
- `dispatchedBy` (optional): [`DispatchedBy`](#type-dispatchedby)

<a id="type-runwaiting"></a>

### `RunWaiting`

```ts
type RunWaiting = { child: string; on: "node" | "quota"; message: string; since: string };
```

One Workspace of a run waiting for capacity ([ADR-0064](decisions/0064-a-workspace-waits-for-capacity.md)): its Sandbox has no node. `child` is the
Workspace's actor path below the run root — the [`RunChild`](#type-runchild) ids down to it, joined by `/`,
and `""` when the run's root IS the Workspace. `on` is what it waits for: a node from the
scheduler, or room under the cluster owner's quota; `message` is the scheduler's or the API
server's own words; `since` (ISO) is when the wait began, kept across a changed reason and a
restart.

**Fields**

- `child`: `string`
- `on`: `"node" | "quota"`
- `message`: `string`
- `since`: `string`

<a id="type-runheld"></a>

### `RunHeld`

```ts
type RunHeld = { agent: string; pick: string; since: string };
```

One outstanding pick, as `RunStatus.held` reports it: the Agent slot, its pick, and the ISO time
it was delivered.

**Fields**

- `agent`: `string`
- `pick`: `string`
- `since`: `string`

<a id="type-dispatched"></a>

### `Dispatched`

```ts
type Dispatched = { slot: string; run: string; at: string };
```

One run a `dispatch` Effect of this run started ([ADR-0071](decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md)): its slot, the run, and when the
ledger row was written.

**Fields**

- `slot`: `string`
- `run`: `string`
- `at`: `string`

<a id="type-reponodestate"></a>

### `RepoNodeState`

```ts
type RepoNodeState = {
  node: string;
  present: boolean;
  synced: boolean;
  attempted?: "Probe" | "Clone" | "Fetch";
  lastAttempt?: string;
  lastFetched?: string;
  lastError?: string;
};
```

One node's view of a Repo, as the cache agent reports it on the resource's status.

**Fields**

- `node`: `string`
- `present`: `boolean`

  A clone exists on this node.
- `synced`: `boolean`

  The last attempt — a probe, a clone, or a fetch — succeeded.
- `attempted` (optional): `"Probe" | "Clone" | "Fetch"`

  Which of the three the last attempt was. A failed Probe is `jr2 status`'s signal for a Repo
  no pod on the node mounts yet; only a failed Clone fails a Sandbox waiting on that node.
- `lastAttempt` (optional): `string`
- `lastFetched` (optional): `string`

  The last successful clone or fetch.
- `lastError` (optional): `string`

  git's own words; absent when synced.

<a id="type-surfaceevent"></a>

### `SurfaceEvent`

```ts
type SurfaceEvent = {
  name: string;
  description?: string;
  input: unknown;
  semantics: EventSemantics;
  briefing?: string;
};
```

One pick on the surface. `briefing` marks a pick whose parameter is a Chart: jr2's text on how a
Chart runs (`CHART_BRIEFING`, [ADR-0070](decisions/0070-a-machine-lends-its-parts-to-a-chart.md)), which the Harness says in the Turn part when the pick is
Allowed, so only the Turns that may write a Chart carry it.

**Fields**

- `name`: `string`
- `description` (optional): `string`
- `input`: `unknown`
- `semantics`: [`EventSemantics`](authoring-surface/type-eventsemantics.md)
- `briefing` (optional): `string`

<a id="type-pickanswer"></a>

### `PickAnswer`

```ts
type PickAnswer =
  | { kind: "event"; event: string; sender?: { party: "agent" | "gate"; id: string }; payload: Record<string, unknown> }
  | { kind: "output"; actor: string; output: unknown }
  | { kind: "answer"; value: unknown };
```

What an outstanding pick's answer is ([ADR-0024](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)): the LAST reply from below the Agent's state since
the pick — a party's event (a Gate's answer, a nested Agent's pick) with its payload, or a child's
output (a child Machine, an Effect stand-in) — named by who sent it. A child's error is no reply (a
defect, [ADR-0024](decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)). Absent where nothing replied: the answer carries no reply, and says nothing of
whether work ran. Plain data; the Harness writes the words. `answer`: the value the Machine's
`answer` action set ([ADR-0067](decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)), which replaces the default.

**Forms**: it is one of these objects.

- Form 1: `kind`, `event`, `sender`, `payload`

  - `kind`: `"event"`
  - `event`: `string`
  - `sender` (optional): `object`

    - `party`: `"agent" | "gate"`
    - `id`: `string`
  - `payload`: `Record<string, unknown>`
- Form 2: `kind`, `actor`, `output`

  - `kind`: `"output"`
  - `actor`: `string`
  - `output`: `unknown`
- Form 3: `kind`, `value`

  - `kind`: `"answer"`
  - `value`: `unknown`

<a id="type-machinestatedoc"></a>

### `MachineStateDoc`

```ts
type MachineStateDoc = {
  id: string;
  key: string;
  type: "atomic" | "compound" | "parallel" | "final" | "history";
  initial?: string;
  invoke: Array<{ id: string; src: string; effect?: EffectDoc; fail?: true }>;
  tags: string[];
  description?: string;
  states: MachineStateDoc[];
  children: ChildMachineDoc[];
  opaqueActions?: boolean;
};
```

One state of the Machine; `states` nests in document order.

**Fields**

- `id`: `string`

  Unique state id (custom `id:` respected).
- `key`: `string`

  Relative key — the segment that appears in a run's `status.value`.
- `type`: `"atomic" | "compound" | "parallel" | "final" | "history"`
- `initial` (optional): `string`

  Initial child state id (compound only).
- `invoke`: `Array<{ id: string; src: string; effect?: EffectDoc; fail?: true; }>`

  Each invoke's id and slot; an Effect's carries its plain-data parts ([ADR-0067](decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)) — what it runs,
  where, and for how long, read without running anything. `fail`: the slot is a `fail()`, so this
  state can fail the run ([ADR-0073](decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md)).
- `tags`: `string[]`
- `description` (optional): `string`
- `states`: `MachineStateDoc[]`
- `children`: `ChildMachineDoc[]`

  The child MACHINES this state runs — invoked ones (a subset of `invoke`, which stays the
  complete invocation list) plus spawned ones (which appear nowhere else). See
  [`ChildMachineDoc`](#type-childmachinedoc). A promise/callback actor has no state tree, so it is never here.
- `opaqueActions` (optional): `boolean`

  This state runs an `enqueueActions` closure, so THIS STATE'S `children` MAY BE INCOMPLETE.

  Spawning is an action, and `spawnChild` is the only static trace it leaves (see
  `spawnedSrcs`). A spawn issued as `enqueue.spawnChild(...)` inside a closure resolves at
  runtime — it can be conditional, looped, or have a computed `src` — so no amount of static
  analysis recovers it, and the subgraph beneath it would simply be absent from the diagram while
  the machine ran correctly. That silence is the hazard, not the omission: it once erased the
  whole feature pipeline of a multi-stage Machine with no signal at all. So the doc carries the
  fact it cannot see, and callers say so out loud (`opaqueStates`, surfaced on the page).

<a id="type-machinetransitiondoc"></a>

### `MachineTransitionDoc`

```ts
type MachineTransitionDoc = {
  source: string;
  targets: string[];
  event: string;
  label: string;
  guard?: string;
  description?: string;
  kind: "event" | "always" | "after" | "done" | "error";
};
```

One transition of the Machine, id-addressed at both ends.

**Fields**

- `source`: `string`

  Source state id.
- `targets`: `string[]`

  Target state ids; empty = targetless (self/internal) transition.
- `event`: `string`

  The raw event descriptor (`""` for always/eventless).
- `label`: `string`

  Display label, precomputed here so the renderer stays dumb.
- `guard` (optional): `string`

  Normalized guard name, if guarded.
- `description` (optional): `string`

  xstate's own transition `description`, as authored: what this writing does, where one name
  has writings that differ ([ADR-0015](decisions/0015-authoring-surface-absorbs-the-mechanism.md)'s "one def, one meaning").
- `kind`: `"event" | "always" | "after" | "done" | "error"`

<a id="type-machineeventdoc"></a>

### `MachineEventDoc`

```ts
type MachineEventDoc = {
  name: string;
  description?: string;
  audience?: EventAudience;
  input: unknown;
};
```

One event a Machine declares — its Vocabulary entry ([ADR-0011](decisions/0011-workflow-defined-events.md)), as the doc carries it. Same
shape a Gate's `accepts` serves, because it is the same thing seen statically: the name, what a
caller sends (JSON Schema), and who may send it.

**Fields**

- `name`: `string`
- `description` (optional): `string`
- `audience` (optional): [`EventAudience`](authoring-surface/type-eventaudience.md)

  Absent: each writing's state decides who may send it ([ADR-0015](decisions/0015-authoring-surface-absorbs-the-mechanism.md)) — see the body's `seats`.
- `input`: `unknown`

  The def's input schema as JSON Schema — what a form or a `jr2 send` prompt generates from.

<a id="type-machineseat"></a>

### `MachineSeat`

```ts
type MachineSeat = { state: string; party: "agent" | "gate"; agent?: string; names: string[] };
```

One party's seat on one state: the state's id, the party, and the names it may send there —
for an Agent the derived set its guards narrow; for a Gate its accepted set, unless the Gate's
input names `accepts:` itself.

**Fields**

- `state`: `string`
- `party`: `"agent" | "gate"`
- `agent` (optional): `string`
- `names`: `string[]`

<a id="type-eventsemantics"></a>

### `EventSemantics`

```ts
type EventSemantics = "ack" | "poll";
```

How the party that sends an event gets its answer.

- `"ack"`, the default: jr2 delivers the event and answers the sender. When the event starts
  work inside the Agent's state, the answer comes when that work is done.
- `"poll"`: reserved. `createMachine` refuses it: ``machine "<id>": event "<name>" is `poll`,
  which is reserved but NOT IMPLEMENTED (ADR-0013). Only `ack` events can be delivered today;
  nothing drains a steer inbox, and degrading it to `ack` would hand the Agent a lying tool
  contract.``

**See also**

- [The Menu](../concepts/the-menu.md)
- [ADR-0006: The agent picks from a Machine-defined, schema-backed menu; it does not drive the workflow](decisions/0006-agent-control-surface.md)

<a id="type-effectdoc"></a>

### `EffectDoc`

```ts
type EffectDoc =
  | { kind: "command"; argv: string[]; timeoutMs: number; slot?: string; secrets?: string[]; pin?: true }
  | {
      kind: "http";
      method: string;
      target: string;
      secrets: string[];
      timeoutMs: number;
      idempotent: boolean;
      file?: true;
    }
  | { kind: "push"; sign: boolean; squash: boolean; force: boolean; pin: boolean; timeoutMs: number; slot?: string }
  | { kind: "dispatch"; workflow: string; wait: boolean; target?: string; timeoutMs: number };
```

An Effect slot's parts, as the doc carries them ([ADR-0067](decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)): a command's argv (an argv slot reads
`<name>`), its timeout, and the Repo Slot whose Worktree is its cwd, when it names one.

**Forms**: it is one of these objects.

- Form 1: `kind`, `argv`, `timeoutMs`, `slot`, `secrets`, `pin`

  - `kind`: `"command"`
  - `argv`: `string[]`
  - `timeoutMs`: `number`
  - `slot` (optional): `string`
  - `secrets` (optional): `string[]`
  - `pin` (optional): `true`
- Form 2: `kind`, `method`, `target`, `secrets`, `timeoutMs`, `idempotent`, `file`

  - `kind`: `"http"`
  - `method`: `string`
  - `target`: `string`
  - `secrets`: `string[]`
  - `timeoutMs`: `number`
  - `idempotent`: `boolean`
  - `file` (optional): `true`
- Form 3: `kind`, `sign`, `squash`, `force`, `pin`, `timeoutMs`, `slot`

  - `kind`: `"push"`
  - `sign`: `boolean`
  - `squash`: `boolean`
  - `force`: `boolean`
  - `pin`: `boolean`
  - `timeoutMs`: `number`
  - `slot` (optional): `string`
- Form 4: `kind`, `workflow`, `wait`, `target`, `timeoutMs`

  - `kind`: `"dispatch"`
  - `workflow`: `string`
  - `wait`: `boolean`
  - `target` (optional): `string`
  - `timeoutMs`: `number`

<a id="type-childmachinedoc"></a>

### `ChildMachineDoc`

```ts
type ChildMachineDoc = {
  src: string;
  label: string;
  via: "invoke" | "spawn";
  machine?: MachineBodyDoc;
  recursive?: true;
};
```

A child Machine reached from a state — the pipeline a workflow delegates to, which is most of
what it DOES. Attached to the state that runs it, so the renderer nests it where it belongs.

`src` is the JOIN KEY: it is exactly the `src` a live child actor reports (`RunChild.src`), for
both kinds — a named actor keeps its name (`"body"`, `"worker"`, `"featureWorkspace"`), an inline
machine object gets xstate's generated key (`"xstate.invoke.0.wrapper.running"`) on both sides.
That is what lets the page hang a run's live child state under the right subgraph without the two
halves agreeing on anything but this string. Since [ADR-0049](decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md) every kit wrapper names its child, so
the generated key is an author's shape only.

**Fields**

- `src`: `string`

  The join key — matches a live `RunChild.src` exactly.
- `label`: `string`

  Display name: the invoke id (`"body"`) or the spawned actor's name (`"featureWorkspace"`).
- `via`: `"invoke" | "spawn"`

  How the state reaches it: `invoke` binds it to the state's lifetime, `spawn` outlives it.
- `machine` (optional): [`MachineBodyDoc`](#type-machinebodydoc)

  The child's own structure. Absent iff `recursive`.
- `recursive` (optional): `true`

  The machine is already an ancestor of this point — the body is up there, not repeated here.

<a id="type-eventaudience"></a>

### `EventAudience`

```ts
type EventAudience = "agent" | "external" | "any";
```

Who may send an event: `"agent"` (an Agent, as a pick from its Menu), `"external"` (the caller of
a Gate), or `"any"` (both).

Leave it unset in most cases. Then the position of each `on` key decides: the event belongs to
the party that its state, or the nearest state above it, invokes. An `on` key on a state that
invokes no party, with no such state above it, belongs to both. Set an audience only to give an
event to the other party too (`"any"`). Or set it to keep an event on the root from one party
(`"agent"` or `"external"`).

**See also**

- [The Menu](../concepts/the-menu.md)
- [Gates](../concepts/gates.md)
- [ADR-0015: Workflows are authored through `jr2Setup`; vocabulary and agent menus derive from the machine](decisions/0015-authoring-surface-absorbs-the-mechanism.md)
