# CLI workflow runs

Workflow commands operate recorded runs and durable step journals. A run receipt records acceptance; it does not establish live agent progress or task completion.


## run

Operate durable workflow runs (cotal-lang programs) from the terminal.

```bash
cotal run start --file <program> [--timeout <dur>] [--local --admit-read <channels> --admit-publish <channels>] [--on <instance>]
cotal run resume <runId> [--local --file <program>] [--on <instance>]
cotal run [--endpoint <ep>] [--json] [--on <instance>]
cotal run --page --on <instance> [--limit <n>] [--cursor <cursor>] [--endpoint <ep>]
cotal run show <runId> [--endpoint <ep>] [--limit <n>] [--json] [--on <instance>]
cotal run answer <runId> <stepKey> [--value <json>] [--artifact <ref>] [--endpoint <ep>] [--local --by <who>] [--on <instance>]
cotal run amend <runId> <stepKey> [--value <json>] [--artifact <ref>] [--endpoint <ep>] [--local --by <who>] [--on <instance>]
cotal run revoke <runId> --local --by <who> --reason <text> [--endpoint <ep>]
cotal run check <runId> <program> [--endpoint <ep>]
```

`ps` (or `list`), `journal` and `migrate` still work as compatibility aliases of bare `run`, `show`
and `check`.

Every verb also takes `--space <s>`, `--server <url>` and `--creds <path>` to pick the mesh and the
credential. A hosted verb refuses a raw `--creds` file `UNSUPPORTED`, exit 2, because the file
carries no endpoint-caller triple for the manager call and that route cannot mint one. Its next step
is to run the verb again without `--creds`, in the mesh's project folder or with `--space`, where
the CLI mints the one-shot caller itself.


### Manager selection

Hosted run verbs accept `--on <manager-instance>` to choose one full manager id
as `cotal ps` prints it. A managed seat defaults to the manager that launched
it; an unbound user-auth caller with several managers must choose one. Without
`--on` its call is refused with exit 2 before any request, and the refusal names
each live manager.
`--on` does not apply to local, offline or seat-only run commands, and a failed
pin never falls back to another manager. Rich run reports label an explicit pin
as the requested manager, and the JSON target carries `requestedManagerInstance`.
Follow-up commands printed by the CLI retain the same explicit pin and fill
in the run ID the command named or the manager returned, only when it matches the native
run-token shape. Keep the pin on later status, answer and resume calls.

A run ID that is not 1 to 64 letters, digits, `_` or `-` is refused as `USAGE_INVALID`, exit 1,
on every verb that takes one, before anything is contacted.
A hosted verb the manager refuses reports the outcome and code as other commands do, and with
`--json` as one refusal envelope on stdout. A refusal the manager makes before acting is refused: a
denied credential or a step in the wrong state exits 2, and a program that does not validate is
`SCHEMA_INVALID`, exit 1, like other invalid input. A run or step it holds no record of is `TARGET_NOT_FOUND`, with `cotal run` or
`cotal run show <runId>` as the next step. A manager activates at most 8 runs at once, counting
each `start` or `resume` until its run is recorded. One more is refused `LIFECYCLE_BLOCKED`, exit 2,
before anything is written; the refusal names the runs still activating, and the next step is to
wait until one has activated and run it again. Any other refusal of `start`, `resume`, `answer` or
`amend` may follow a recorded change, so it is unknown, exit 3, and is not retried.

An unpinned hosted verb run by the operator reads the space's service registry first. When it
lists no live manager, as after `cotal down stack-manager`, the verb refuses `TARGET_UNAVAILABLE`
(exit 2) at once, as `ps` does, and names bare `cotal up` as the next step, instead of waiting out
the manager's answer deadline.

A mesh a run verb cannot resolve or connect to is refused before any request under the refusal's
own code, hosted or `--local`, as `ps` refuses it. With no mesh running that is `TARGET_NOT_FOUND`,
exit 2, and with `--json` one refusal envelope on stdout.

`start` hands the program to the mesh's manager, which validates it, mints the run id (the record
never takes a caller-supplied one), drives it in its own process, and answers with the id once the
run is recorded; a program that does not validate is refused with every problem listed, each
control character in it shown as its `\uXXXX` escape. Every verb
that takes a program file refuses a missing one with `TARGET_NOT_FOUND` and one this account cannot
read with `PERMISSION_DENIED`, exit 2, and a folder with `USAGE_INVALID`, exit 1.
A `start` interrupted with Ctrl-C or `SIGTERM` after its request went out reports outcome `unknown`,
because the manager may already have recorded the run, and exits 130 or 143. List the runs with
`cotal run` before starting it again.
`resume` asks the manager to take an existing run back and continue it from its step journal; the
source is the recorded program, so no `--file` is taken. A run recorded `completed` or `failed` is
refused `LIFECYCLE_BLOCKED` (exit 2) and gets no new activation, because nothing is left to continue;
the next step is `cotal run show <runId>`. Neither takes `--endpoint`: the manager records
its runs under its own endpoint, and naming another is refused. Bare `run` lists the run records and
`show` renders one run's durable records; both only inspect. `show` reads the
journal from the manager in pages, each one reply, so a journal larger than the broker's payload
limit stays readable. A record or a journal row too large for one reply refuses `UNSUPPORTED`,
exit 2, and names `cotal run show <run-id> --local`, which reads the run's stream from its
project folder with no reply. A manager from release 0.99.3 or earlier reads no journal page, so
through it both refuse `UNSUPPORTED` and name the same local read. An open pause prints its question
under `Needs an answer`, and only then does Next offer `answer`. A checkpoint with a `to` prints
beside its question whom it escalates to, and once it has escalated, until when that attempt is open. A pause counts as open while its
last journal row is pending, so a settled or expired one is not listed there.
A pause settled with an accepted answer prints its value as JSON plus the recorded answerer,
artifact when present, time, and answer id, then one `amended` line per later amendment, in the
order the store committed them, so the last is the current position. A value longer than 120
characters is cut there, with a line that says so; `--json` carries it whole.
Expired pauses and ordinary steps print no answer line.
A step whose last journal row recorded an error prints its outcome and the recorded message under
`Step errors`, a caught failure included, with each control character shown as its `\uXXXX`
escape. A run that failed records its cause on its status. Its recorded state reads
`failed (<code>)` in the run list and the run view when the error carries a language code, and the run
view prints the message under `Failure`. When a step's error is the run's failure, `Failure` names
that step and the message stays under `Step errors`. A run that failed before its status recorded a
cause shows only `failed`. A failure raised by program code outside any step is recorded in no
journal row. When the status recorded no cause either, the view says so under `Failure`. Either way
it names `cotal service logs stack-manager --space <space>`, because a hosted run's cause is in its
manager's log.
The failure records no line or column in the program: a source position belongs to a problem
`run validate` reports before the program runs.

A run recorded `released` has no recorded reason. Its `Released` section points to the executing
manager's log for a hosted run or the driving process's output for a locally driven run. A journal
read does not establish that execution host or log location, so the view does not guess one.
A journal entry larger than the broker's configured `max_payload`, such as a large checkpoint
question, can release a run with `L5010 Journal append rejected`. Repeating `resume` while the
cause remains can release it again; this view does not change that state or prevent the retry.

A program's `log(...)` lines are not journalled, so `show` does not print them. A hosted
run writes each line to its manager's log as `run <run-id>: log <text>`, which
`cotal service logs stack-manager --space <space>` reads, and a resumed run writes the lines before
its resume point again. `--local` prints them in the terminal that drives the run.
`--json` on the run list or `show` writes one envelope on stdout, as other commands do, and nothing on
stderr, whether the manager answers or `--local` reads. The list puts its rows under `result.items`.
`show` puts the run under `result.view`: its `runId`, `endpoint`, recorded `spec` and `status`,
its `programHash` as the list row carries it, and its rows under `result.view.journal`. A program
record that cannot be read gives `programUnreadable` with its reason, and no `programHash`. A list row carries `runId`, `endpoint`, `state`, `holder`, `epoch`,
`journalHigh`, `forkedFrom`, `startedAt` and `programHash` (the values the program's `run()`
reports; `programHash` is absent for a run with no recorded program), `failure` on a failed run
with its `code` and `message`, and `revoked` or `revocationUnreadable` when the marker says so. A journal row is an `activation` or a `step`. A
step row carries its `step` key, the `effect` kind and its `name`, `state`, `outcome`, the recorded
`status` and `errorCode` once settled, the recorded `errorMessage` of a failed or refused step,
and `startedAt` and `endedAt` in epoch milliseconds. An open
pause adds its `asks`, its `deadlineAt`, and for a checkpoint the `onExpiry` it was armed with and
the `addressee` its `to` names. A checkpoint's escalation is armed with `proceed`, so it never
escalates again. A settled pause adds its `answer` and its `amendments`, as the text view prints
them. A field the
journal does not record is absent: a checkpoint opened before `onExpiry` was recorded carries none.
An unreadable revocation marker makes the result partial, exit 4, with a `REVOCATION_NOT_CHECKED`
warning naming the run. A `run --endpoint <endpoint>` that matches no run lists nothing and says so,
naming the endpoints runs are recorded under, with an `ENDPOINT_HAS_NO_RUNS` warning. The text view is presentation and is not a stable parsing target. With
`--local`, only the list, `show`, `answer` and `amend` take `--json` or `--plain`. Any other local form
refuses either flag `USAGE_INVALID`, exit 1: `start --local` and `resume --local` name the
manager's form, which takes it, and `revoke` and `check`, which have no other form, name the same
command without it.
`run --page`, which is not in release 0.88.0 or earlier, asks one manager for a single bounded
diagnostic JSON page instead of the full list:
`--limit` takes 1 to 16 rows (default 8), and `--cursor` continues a scan from a returned
`nextCursor`. It requires `--on` and refuses `--local`, and a page proves neither absence nor
ownership ([Diagnostic inventory](/docs/workflows/#diagnostic-inventory)).
`revoke` writes the run's revocation marker from the project folder; `--by` and `--reason` are
required ([workflows](/docs/workflows/)).
`answer` resolves an open
checkpoint through the manager, presenting as the holder that armed it; the manager records the
answerer from your credential, so no `--by` is taken there. A settled step refuses a second
`answer` with `TARGET_NOT_FOUND`, also when it is sent before the run records the first, and a
revoked run refuses every answer with `LIFECYCLE_BLOCKED` and files nothing. A step the run holds no
open request at is also `TARGET_NOT_FOUND`; when an open checkpoint or `ask` has the name you typed,
as `frage` names `/checkpoint:frage#0`, the next step gives its full key, and several matches are
listed for you to pick from. A run ID that no run is recorded under is `TARGET_NOT_FOUND` too, with
or without `--local`, and the refusal says no run is recorded under that ID. An
answer without `--value` carries no value: the step settles as answered, and the
program's checkpoint result has no `value`. `--value` is refused with `SCHEMA_INVALID` before
anything is sent when the record would not hold what you typed: an object that names a key twice,
a number outside the range JSON carries (such as `1e999`), or a value nested more than 1000 levels
deep. An empty or blank `--artifact` is refused with `USAGE_INVALID`. The receipt of `answer` and
`amend` names the step, the value it filed or that none was given, any artifact and the answer id. A value longer than
120 characters is cut there too, with a line that says so. With `--json` they are in
`result.answer`, whole, where `valueGiven` says whether a value was sent. `amend` records a changed
position on a settled checkpoint or `ask`: it files a new answer beside the accepted one, naming it, and the journal lists it under the step. The pause stays
settled and the run keeps the answer it acted on. A step that is still open or settled without an
answer refuses an amend. So does a run ID that no run is recorded under, and that refusal says no
run is recorded under that ID. With `--local` these refusals are `TARGET_NOT_FOUND`, exit 2, as the
hosted ones are, since nothing was written. The run records an accepted answer a moment after `answer` returns, and an
amend sent in between is refused with a message saying the answer is not recorded yet; amend again
once `run show` lists it. A spawned seat may amend only an answer recorded under its own name.
`check` replays a run's journal against an edited program, from this terminal under a read
credential (`--local` only; the manager serves no run-migrate command). It prints whether the
migration is admissible, every orphaned step with its verdict and code, and exits 0 on admissible
and non-zero on not. An edited program that does not validate is refused before the replay, with
each problem's code, position, cause and fix. It writes nothing: the commit that would file the
migration is not reachable yet, and the report says so. A run ID with no record on the endpoint is refused
`TARGET_NOT_FOUND`, exit 2, as `run show` refuses it. `check` refuses `--adopt`, `--release` and `--discard-approvals`, since they decide what a
commit does and the check files nothing. `--timeout` sets the default
limit of a checkpoint, `ask` or turn that sets none of its own (default 1h). A checkpoint that
expires under `onExpiry: "fail"` says in its L4007 whether the deadline came from its own `timeout`
or from this default. It takes a duration
longer than zero, such as `30m`, and refuses anything else with `USAGE_INVALID` before the run starts. `--on <instance>` sends a hosted verb to one manager
instance. It takes the full instance id `ps` prints, not a manager name or host. On a mesh whose managers this terminal can
list, an `--on` that names none of the running ones is refused `TARGET_NOT_FOUND` at once, naming
them, and nothing is sent. On a user-auth mesh where you run more than one
live manager, an unpinned hosted verb is refused and `--on` picks one. `--local` takes no `--on`. `--local` drives in this process instead, over one
connection per invocation under the run's own credential minted from the project folder's trust
material, and is the path on a bare broker with no manager or for a run with no recorded program
(`cotal run resume <runId> --local --file <program>`); `answer --local` and `amend --local` take
`--by <who>` and print the same receipt as the hosted path, and `start --local` requires
`--admit-read` and `--admit-publish` (comma-separated
patterns or `none`), the run's channel ceiling. An entry that is not a channel or a pattern, such as
`a b`, is refused `USAGE_INVALID` naming it. When the run's status records that its program failed
under this drive, `start --local` and `resume --local` fail `OPERATION_FAILED`, exit 5, naming the
run and its language code with that code's title. A failure whose code has no title shows the first
line of its recorded message instead, with control characters escaped. Their Next is
`cotal run show <runId> --local`, which prints the whole message. Any other error the drive ends on
stays `unknown`, exit 3. On a
user-auth mesh the host's own manager refuses the family by name, and `--local` has no credential
there. A participant's manager started with `cotal supervise` hosts a logged-in user's runs through
its issuing host: the auth callout issues the user's manager connection, and every `run` verb rides
the versioned rail under that issuance.
[User-auth run start](https://github.com/Cotal-AI/Cotal/blob/main/docs/design/user-auth-run-start.md)
records the path. The guide is [workflows](/docs/workflows/).


## Workflow command forms

`cotal run` (or `cotal r`) lists the authorized workflow records. `run show <run-id>` reads
recorded state, open requests, each step's recorded error and the last 20 journal rows. `--limit <n>`
prints the last `n` rows instead, from 1 to 1000, and is refused with `--json`. A settled
`race` row names the branch it took, and a cancelled row in a branch that lost a race names the
branch that won and the race. An answer whose step was in a branch that lost a race, or
whose pause was cancelled after accepting it, is listed as `not acted on`. Its JSON result contains
the full returned status and journal. Revoked admission and unreadable revocation markers remain distinct
from the record's state. Neither a record nor a receipt is a live agent-progress report.

```bash
cotal r
cotal r start ./workflow.cotal.js
cotal r show <run-id> --json
cotal r resume <run-id>
cotal r answer <run-id> '<step-key>' --value '<json>'
cotal r amend <run-id> '<step-key>' --value '<json>'
cotal r check <run-id> ./edited.cotal.js
cotal r revoke <run-id> --local --by <actor> --reason '<text>'
```

Start takes a positional source file. A source file a `run` verb cannot read is refused naming
its path and the cause: `TARGET_NOT_FOUND` for a missing file or a link whose target does not exist,
`USAGE_INVALID` for a directory and `PERMISSION_DENIED` for a file it may not read. `-` is a file
name there, since `run` does not read a program from standard input. A run verb that reads a source
file refuses an empty one or one that is not valid UTF-8 with `SCHEMA_INVALID`, before it checks or
sends anything, because a run's source must not be empty and its text is never replaced. A hosted
start sends the program to the manager in one message, so a program over the broker's message limit
(`max_payload`) is refused `UNSUPPORTED` with its size and the limit, and nothing is sent. Resume uses the host's recorded program. Hosted answers
and amendments are attributed to the authenticated caller, never a supplied name. Local answers and
amendments require `--by <who>`; a missing or empty value refuses `USAGE_INVALID` before contacting
the broker, with a JSON refusal under `--json`. `check <run-id> <file>`
replays the run's recorded journal
against the edited program and prints whether the migration is admissible, and it files no migration. Revocation keeps
the existing local trust and explicit actor/reason requirements and does not cancel a task.
Hosted start, resume, answer and amend have JSON and plain receipts. The local drive and local
check retain their native output rather than claiming an unsupported structured format.


### Offline commands

```bash
cotal r validate ./workflow.cotal.js
cotal r plan ./workflow.cotal.js
cotal r simulate ./workflow.cotal.js --handler ./results.json
```

These three forms are offline and reject mesh or caller selection. Validation checks the
language and nothing a broker decides, so a valid program over the broker's message limit is still
refused by a hosted `start`. A program that does not validate is refused by all three with every problem listed: its
code, position, cause and fix, as a hosted `start` lists them. `--json` carries them under
`result.problems`. A warning leaves the program valid. All three print it on stderr with its code,
position, cause and fix, and `validate --json` lists it under `result.warnings`. One is L3021, a `fanOut` over a list written in the call whose items carry no
`id` and that passes no `key`, which the run refuses when it reaches it. A list held in a variable
gets no warning, because it can change before the call; the run refuses it the same way. Plan lists validated static call sites in source order; it does not evaluate branches,
loop counts or an execution schedule. Simulation uses the native simulator with a JSON results
script. It executes no JavaScript handler and invents no unscripted outcome. A scripted
checkpoint result needs a `status` of `resolved` or `expired` and may carry `value`, `by` and
`artifact`; any other shape, a scripted `at` included, is refused `SCHEMA_INVALID`, because the
simulator stamps `at` from virtual time. A scripted turn result needs a `status` of `done`, `blocked` or
`handoff` and may carry `to` and `note`; any other shape, a scripted `at` included, is refused the
same way. When the script has no result for an effect, the
simulation does not complete: it exits 5 with `OPERATION_FAILED`, the message names the step and the
entry to add, and `target.step` carries the step. When the program itself fails under the script,
as a checkpoint scripted `expired` under the default `onExpiry: "fail"` does with L4007, the
simulation completes and exits 0. Its program outcome is failed, with the step and that step's
outcome, or the last step the program settled before its own code failed, and the language's error.
JSON carries this as `result.outcome` and `result.failure`. A simulation does not establish live authority, installed applications or readiness. All three support JSON and
plain output and contact no broker.

The `--handler` file is one JSON object. Each table maps a step's name to its result: one value
for every occurrence, or a list used in order. A step with no name is keyed by its effect, such as
`ask`. `spawn`, `sleep`, `notify`, `monitor`, `observe` and conclaves need no entry, and a sleep
moves the virtual clock forward.

```json
{
  "checkpoints": { "approve-plan": { "status": "resolved", "value": true, "by": "ana" } },
  "asks": { "plan": { "steps": ["design", "build"] } },
  "turns": { "build": [{ "status": "blocked", "note": "needs a key" }, { "status": "done" }] },
  "events": { "await-reply": null }
}
```

| Field | Meaning |
|---|---|
| `turns` | `turn` results: `{status, to, note}`, where `status` is `done`, `blocked` or `handoff` |
| `asks` | `ask` replies, each checked against the step's `schema` |
| `checkpoints` | `checkpoint` results: `{status, value, by, artifact}`, where `status` is `resolved` or `expired` |
| `events` | `wait` results. `null` is a timeout |
| `clock` | `{start, turn, ask, checkpoint, wait}`: the start time in milliseconds and how long each effect takes, such as `"5m"`. A turn takes 5 minutes and the others 1 minute unless set |
| `faults` | `[{at, kind, code, message}]`: the step at `at`, a step key such as `turn:build#1`, fails with that error |

The simulation's `log(...)` lines print under `Log`, and `--json` carries them in `result.log`, each
with its `scope` and `values`. A step with no entry, or a list shorter than the step's occurrences,
stops the simulation with L6001 and exit 5. The failure names the table, the step's name and its step key,
and the remedy names the entry to add. For a list that is too short, it also says which result the step
needs, counted from 1, while the step key counts occurrences from 0. A failure in the program itself
names its code and cause.


### Seat turns

Inside a manager-launched static or open seat, `run turns` reads that seat's assigned turns and
`run yield <turn-id> --status <done|blocked|handoff>` reports an outcome. Handoff also requires
`--to <agent>`; `--note <text>` carries context. The native launcher supplies the caller context.
These forms verify the seat's own credential and manager, never substitute an operator caller,
and refuse user-auth self impersonation. They skip operator catalog and installation repair.
Yielding one turn does not complete the entire workflow.


### Run activation

See the [current command contract](/api-reference/cli-runs/#run) for this behavior.

### Recorded state

See the [current command contract](/api-reference/cli-runs/#run) for this behavior.

### Diagnostic pages

See the [current command contract](/api-reference/cli-runs/#run) for this behavior.

### Revocation

See the [current command contract](/api-reference/cli-runs/#run) for this behavior.

### Pause answers

See the [current command contract](/api-reference/cli-runs/#run) for this behavior.

### Migration check

See the [current command contract](/api-reference/cli-runs/#run) for this behavior.

### Local drive

See the [current command contract](/api-reference/cli-runs/#run) for this behavior.
