--- title: CLI workflow runs description: Hosted and local workflow runs, recorded state, pause answers, offline checks, and seat turns. kind: Reference audience: operators --- 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 [#run] Operate durable workflow runs (cotal-lang programs) from the terminal. ```bash cotal run start --file [--timeout ] [--local --admit-read --admit-publish ] [--on ] cotal run resume [--local --file ] [--on ] cotal run [--endpoint ] [--json] [--on ] cotal run --page --on [--limit ] [--cursor ] [--endpoint ] cotal run show [--endpoint ] [--limit ] [--json] [--on ] cotal run answer [--value ] [--artifact ] [--endpoint ] [--local --by ] [--on ] cotal run amend [--value ] [--artifact ] [--endpoint ] [--local --by ] [--on ] cotal run revoke --local --by --reason [--endpoint ] cotal run check [--endpoint ] ``` `ps` (or `list`), `journal` and `migrate` still work as compatibility aliases of bare `run`, `show` and `check`. Every verb also takes `--space `, `--server ` and `--creds ` 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 ` 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 ` 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. A directory, device, FIFO or socket is refused with `USAGE_INVALID`, exit 1, naming its kind. Use a regular file, or a symlink to one, for the source. 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 `. 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 --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`, along with its recorded checkpoint schema as JSON when present, and only then does Next offer `answer`. The schema is guidance; checkpoint answers are not checked against it. 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 ()` 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 `, 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. If a broker outage prevents a hosted run's final status write, the manager retries that write while the attempt's connection remains open, including when NATS returns before JetStream can answer. The program does not advance during this repair. Once JetStream answers, `run` and `run show` report the recorded terminal state. A newer attempt's status is never overwritten. If the manager stops first, its next boot takes back runs still recorded `running`. 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 : log `, which `cotal service logs stack-manager --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 its recorded `schema`, 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 ` 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](workflows.mdx#diagnostic-inventory)). `revoke` writes the run's revocation marker from the project folder; `--by` and `--reason` are required ([workflows](workflows.mdx)). `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 ` 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 --local --file `); `answer --local` and `amend --local` take `--by ` 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 --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](design/user-auth-run-start.md) records the path. The guide is [workflows](workflows.mdx). ## Workflow command forms [#workflow-command-forms] `cotal run` (or `cotal r`) lists the authorized workflow records. `run show ` reads recorded state, open requests, each step's recorded error and the last 20 journal rows. `--limit ` 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 --json cotal r resume cotal r answer '' --value '' cotal r amend '' --value '' cotal r check ./edited.cotal.js cotal r revoke --local --by --reason '' ``` 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, device, FIFO or socket and `PERMISSION_DENIED` for a file it may not read. The source must be a regular file or a symlink to one. Pipes are refused even when a writer has supplied data, including process substitution paths. Save that output in a regular file first. `-` 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. On a static-auth mesh that caller is one actor derived from the OS login and host, so hosted run commands from that login and host record the same actor. Each command still mints a fresh short-lived credential and lifecycle. A signed-in user keeps that user's `cli` actor, and a managed seat keeps the seat's name. Local answers and amendments require `--by `; a missing or empty value refuses `USAGE_INVALID` before contacting the broker, with a JSON refusal under `--json`. `check ` 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 in UTF-8, with an optional leading byte-order mark. It must be a regular file or a symlink to one, as the program file must be. A missing file or dangling link is refused with `TARGET_NOT_FOUND`, and an unreadable file with `PERMISSION_DENIED`, exit 2. A directory, device, FIFO or socket is refused with `USAGE_INVALID`, exit 1. An empty file or invalid JSON is refused with `SCHEMA_INVALID`, exit 1. These refusals name the path, the cause and a next step. A file that is not valid UTF-8 is refused with `SCHEMA_INVALID`, exit 1, before simulation, naming the path and how to save it as UTF-8. No invalid bytes are replaced. 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 --status ` reports an outcome. Handoff also requires `--to `; `--note ` 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 [#run-activation] See the [current command contract](cli-runs.mdx#run) for this behavior. ### Recorded state [#recorded-state] See the [current command contract](cli-runs.mdx#run) for this behavior. ### Diagnostic pages [#diagnostic-pages] See the [current command contract](cli-runs.mdx#run) for this behavior. ### Revocation [#revocation] See the [current command contract](cli-runs.mdx#run) for this behavior. ### Pause answers [#pause-answers] See the [current command contract](cli-runs.mdx#run) for this behavior. ### Migration check [#migration-check] See the [current command contract](cli-runs.mdx#run) for this behavior. ### Local drive [#local-drive] See the [current command contract](cli-runs.mdx#run) for this behavior.