Skip to content

chant run

llms.txt lists every page for an agent
chant run <name> [flags]
chant run list [flags]
chant run status <name> [flags]
chant run approve <op> <gate> [flags]
chant run cancel <name> [flags]
chant run log <name> [flags]
chant run events <run> [--json] [--follow]
chant run wave --spec <file> --wave <k> [--decide | --share <i> | --group <name> [--share <i>]] [--base <ref>]
chant run resume [--op <name>] [--gate <name>] [--dry-run] [--json]
chant run wave --spec <file> --record-plans
chant run --components <name|all|a,b,c> [flags]
chant run --generate <provider> [--spec <file>] [--output <dir>] [--format json]

chant run runs Op workflows (named, phased workflows defined in *.op.ts files) and, with --components, component deploy compositions.

chant run <name> runs local by default: the Op runs in the chant process. Phase sequencing, retries and activity profiles all work there. So do gates and onFailure compensation.

Every run subcommand addresses one runtime, the thing that hosts the run. Without a flag that is the built-in local runtime, which executes the Op in the chant process. --on <lexicon> hands the run to a configured lexicon that implements the opRuntime hook instead, and the subcommands follow it there: run status, run log, run list, run cancel and run approve all query the runtime named by the same flag.

Terminal window
chant run alb-deploy # the built-in local runtime
chant run alb-deploy --on fountain # a lexicon that hosts runs
chant run status alb-deploy --on fountain

A name that is not a configured lexicon is refused with the list of lexicons that are. A configured lexicon that hosts nothing is refused by name. Core ships only the local runtime and never imports a hosting lexicon, so a project gets the hosted path by declaring the lexicon, not by upgrading chant.

A project whose Ops always run on one lexicon’s runtime can set it once in chant.config.ts. run.on is the default for chant run and every subcommand. --on overrides it, and --on local selects the built-in runtime. When run.on names a lexicon that is not configured or has no opRuntime, the refusal is the one --on gives. Its label names run.on and the config file in place of the flag.

A run block that does not validate is refused too. That covers a non-string or empty on and a misspelt key, so a broken default never falls back to the local runtime unnoticed. run: { on: "fountain" } works like --on fountain on every run: the line fountain posts carries --on local, so the sandbox runs the Op itself rather than posting it again (#3225).

chant.config.ts
export default {
lexicons: ["aws", "mylexicon"],
run: { on: "mylexicon" },
};

The fountain lexicon hosts a run in a steward’s conversation. chant run <op> --on fountain posts the command line chant run <op> --on local to the steward as a prompt, tails the conversation’s event stream, and exits with the status the run reached. The Op executes in fountain’s sandbox, so the run record lands on the ledger from there rather than from this process. --env <env> rides on the posted line as chant run <op> --env <env> --on local, so the run in the sandbox sees the environment you named (#3232). An environment name that is not one plain word (letters, digits and . _ : @ + = / -) is refused by name before anything is posted. Each --param name=value rides on the posted line too, in double quotes when the value holds a space or a quote, so the sandbox runs the line you typed (#3539). --work and --holder ride on the posted line too, quoted the same way. An Op that needs a work item therefore runs on fountain, and the run in the sandbox takes the lease. Without --holder, the sandbox’s run names the holder. In the sandbox, chant acp names the run by the conversation’s id (fountain hands the command FOUNTAIN_CONVERSATION_ID), so the run record written there and the run chant run status --on fountain reports carry the same id. Every run on one steward goes to the same conversation, so its runs share the id too.

Terminal window
chant run alb-deploy --on fountain
chant run alb-deploy --on fountain --progress-json
chant run status alb-deploy --on fountain
chant run log alb-deploy --on fountain --limit 10
chant run cancel alb-deploy --force --on fountain

The endpoint and the token come from fountain.profiles in chant.config.ts, and so does the steward: a profile’s steward names the steward whose conversation the prompt goes to, opened with channel_id chant:steward:<name> on its first run and resumed by it after that. team, its old name, still works and prints a deprecation warning. An Op a loaded Steward lists goes to that steward whatever the profile says. Without either, the Op’s labels.Agent or a --param agent=<name> binding opens a fresh conversation on that agent instead. An Op with none of these is refused with each one named.

A steward’s conversation runs one turn at a time. A prompt sent while another op is running comes back as conversation_busy, and chant reports it with the conversation’s address rather than queueing behind a turn whose length it cannot predict. Wait for the conversation to go idle and run the command again.

Tool calls on the stream become phases, so --progress-json and the human renderer show the run’s shape as it happens. With --events-json the tail writes the run-event stream on the steward runtime: a phase per stage the turn reports, and a step per tool call, started when the call starts. Fountain closes an idle stream connection after 60 seconds, which is not the end of a turn: the reader reconnects with Last-Event-ID and the server replays what arrived meanwhile. Only real silence ends the wait, after 30 minutes, and FOUNTAIN_STREAM_IDLE_TIMEOUT (in seconds) widens that for a run that thinks for longer.

chant run approve <op> <gate> --on fountain writes the resolution to the gate ledger as always. It then posts chant run <op> --on local to the steward, with --approver and --url on it (quoted when the approver’s name holds a space), and with the --params, --work, --holder and --env the gated run carried. --env on the approve line names the environment for the re-run in place of the gated run’s. The sandbox re-runs the op and walks through the gate, reading the resolution back off the ledger. --durable-requests is reserved for answering fountain’s own pending request instead; until the endpoint it needs ships, the flag refuses and names the upstream issue.

chant run <op> for an Op a declared steward lists is one of that steward’s turns, not a run beside one (#2750). Which rule applies depends on the steward’s form in the run’s environment (--env, default local):

  • fountain: the run belongs in the steward’s conversation. Without --on fountain, it is refused outright, naming the command that posts it there instead of running it here, unaware of the steward’s own turns.
  • local: the run takes the steward’s turn lease, the same one a chant operator --steward round holds for the length of its own scheduled tick (refs/chant/lease/_turns/<name>). Held already, chant run <op> waits briefly for it to clear (long enough to ride out a race with a turn that is just ending, never long enough to sit through a whole one), then refuses, naming the steward and, where known, who holds the turn. It never queues past that wait. A round’s own leases skip and report rather than wait, and fountain’s busy steward above is refused rather than retried; this follows the same rule. Wait for the turn to finish and re-run the command, or check chant operator status --steward <name>. If it looks stuck, stopping the operator process holding it (Ctrl-C, or killing it) frees the lease immediately, and its own TTL frees it either way.

An Op the steward runs beside its turns (#2861) is the exception in the local form. chant run <op> for it takes the Op’s own lease, refs/chant/lease/<op>, as --holder or this process, and never the steward’s turn, so it holds none of the steward’s other Ops up. The lease is renewed while the run lasts and released when it ends. The operator starts no run of the Op while the lease is held, and a hand run while one is in flight is refused at once, naming the holder. See operator --steward.

An Op no steward lists is untouched by any of this.

Run an Op, rendering each phase and step as it runs.

Terminal window
chant run alb-deploy # the local runtime (default)
chant run alb-deploy --json # the run's ledger record on stdout
chant run alb-deploy --on fountain

The phase and step summary is printed when the run ends. While a step runs, the local runtime prints its output on stderr as each line arrives (#3956). Each line a shell step’s command writes, on stdout or stderr, is printed as [<phase>] <step> | <line>. <step> is the step’s id, or its call as the summary prints it, such as shellCmd(cmd=npm test), when it has none. A step that prints nothing for 60 seconds, such as an activity that only returns a value or a command that is quiet, gets [<phase>] <step>: still running after <n>s each time another 60 seconds pass. CHANT_STEP_HEARTBEAT sets that interval as a duration (30s, 5m), and 0 or off turns the line off. Stdout stays the --json record’s and --progress-json’s, so neither changes. The step’s published stdout and stderr still hold the command’s whole output.

[Seed] wait-agent | waiting for the agent
[Seed] wait-agent: still running after 60s
[Seed] wait-agent | agent ready
[phase] Seed
✓ shellCmd(cmd=./seed.sh) 75.2s

A gate in the Op stops the run pending a resolution on the gate ledger. The run prints the gate’s description and the chant approve line, and exits 3 rather than 1, so a CI job can report “waiting on approval” rather than a failure. No phase after the gate runs, and no onFailure phase runs either: nothing failed, so there is nothing to compensate for. See Gate-as-fact.

An activity that asks a decision point whose question is still open, escalated to people or proposed by a model, stops the run the same way, with status waiting (#2749). The run prints the question’s id and the chant workspace points answer line, records the question on its ledger record as point, and exits 3, which --gated-exit remaps too. No later step and no onFailure phase runs. Once a person has answered, the next run asks the same point with the same inputs and reads the answer.

A step whose command stops at a gate of its own ends the run gated at that gate too, when the step names the command’s gated exit code in gatedExit (#2779). The run prints the chant approve line for the command’s gate, for example chant approve workspace-upgrade ., and its ledger record names the gate with the op it is recorded under. See Ops.

Recording the pending fact is two steps: an append to the local chant/lifecycle branch, then a push. The append always succeeds, and the run gates on it even offline. The push can still fail (the branch moved under it since the run started, or git refused it). The run then stays gated, since the local fact is correct, and prints a warning naming the push failure and its reason, so the operator knows a chant approve from another checkout finds nothing to resolve until the fact reaches the remote. A checkout with no remote for chant/lifecycle, such as a laptop working against a local database, is not a failure: nobody elsewhere can approve, so the run prints one note, recorded locally (no remote for chant/lifecycle), and chant approve ends its line the same way. --json carries the same information as pushed: false and pushWarning alongside the record.

An Op that declares workLease runs its steps under one work item’s work lease (#2748). The run claims the lease before the steps it covers and renews it every third of its time to live while they run. One more renewal comes before the run is recorded. When the run ends the lease is released with its outcome, from the closed list: done after a completed run, not_done after a failed one and gated after a gate stopped it. An Op that changes the checkout keeps a not_done attempt’s work at refs/chant/kept/<item>/<token>. A lease the heartbeat finds lost (it ran out and another worker claimed the item) stops the step in flight, and the run fails with a workLease:lost record that starts lease-lost. The run never releases a lease it lost. Running under a work lease covers the declaration.

Terminal window
chant run dispatch # the item the Op picks, or names
chant run apply-build --work W-017 # an Op whose lease leaves the item to the run
chant run apply-build --work W-017 --holder box-steward/apply-build@host

--work <id> takes the place of the Op’s own item. An Op that leaves the item to the run is refused without it, and --work on an Op with no work lease is refused too. --holder names who holds the lease. Without it, an Op a steward lists is held as <steward>/<op>@<process>, the name chant workspace status --json uses to show the lease a steward’s turn holds. Any other Op is held as this process. A run that finds the item held claims nothing, runs none of the leased steps, and exits 0 with the refusal on the claim’s record, naming the holder. On a hosted runtime the lease is taken where the run executes. fountain posts both flags to the sandbox; a runtime that does not carry them refuses them by name.

GitHub Actions has no neutral conclusion for a run: step, so a push-to-main apply that stops at its gate is a failed workflow run until someone approves. --gated-exit <code> maps that one outcome to a code of your choosing:

Terminal window
chant run prod-apply --gated-exit 0 # a pending approval is not a broken build

Only the gated outcome is remapped. A run that fails still exits 1 and a run that completes still exits 0, so the flag can never hide a broken apply. The --json payload is untouched by it: the record still says "status": "gated" and still carries the gate and the exact chant approve line, so a job that maps the code to success can still tell the two apart and report the pending gate. The value must be a process exit status (0-255); anything else is refused before the Op runs.

Whenever a run is gated, chant run also appends a short markdown block to the file named by GITHUB_STEP_SUMMARY, when that variable is set. The block names the Op and the gate, gives the exact chant approve command, and points at the _gates/<op>.jsonl ledger the pending fact was written to. GitHub Actions, Forgejo Actions and Gitea all set it, and any other CI that does gets the same block. Nothing is written when the variable is absent, and a write that fails never changes the run’s outcome. When this run’s own push did not reach the remote, the block says so, since the step summary is what an operator in another checkout reads.

The github Op generator passes --gated-exit 0 on push-triggered jobs and adds a follow-up job that posts the pending gate on the merged pull request, so a gated apply is a green run with a visible pending state rather than a red one.

A test run or a claim run often should leave nothing on the ledger. --no-record runs every step and records nothing on the chant/lifecycle branch (#3955). That includes the run’s own record and any gate fact it would write. Nothing is pushed to the remote, a lease ref included. The run prints a line before its first step and another after its last, saying that nothing was recorded and how many ledger writes it dropped:

Terminal window
chant run smoke --no-record
# warning: not recording (--no-record): nothing this run writes goes on chant/lifecycle, and nothing is pushed. A gate step fails.
# ...
# warning: not recorded (--no-record): nothing went on chant/lifecycle and nothing was pushed; 2 ledger writes were dropped (2 files).

Set CHANT_NO_RECORD=1 once in a CI test job instead of passing the flag to each command. Every chant process that inherits it behaves the same way, including a chant command a step starts. An empty value, 0 or false leaves recording on.

Each write still happens, in memory, so a later step in the same process reads what an earlier one wrote. An effect step finds its own receipt, and a work lease reads as held. The writes are dropped when the process ends, and the branch ref is never moved. A command that reads the branch with plain git does not see them.

A gate step fails under --no-record, and the run handles it like any failing step: the steps after it don’t run, onFailure runs, and the exit code is 1. It does not stop as gated, so --gated-exit doesn’t apply. A standing approval doesn’t pass the gate either. Passing it would apply under that approval without recording that the approval was used, so one approval could pass this run and the next recorded one. A pending fact it wrote could never be approved, since nobody else would see it. chant approve refuses while CHANT_NO_RECORD is set, for the same reason.

The flag covers records only. A step that applies a stack, pushes a branch or calls an API still does so. --no-record runs on the local runtime only: with --on <lexicon> the hosting runtime records where it executes the run, so the combination is refused before anything starts.

A run whose lifecycleDiff step finds drift still completes, and exits 0. --fail-on-drift makes such a run exit 2 when its Drift outcome is true, so a scheduled drift job turns red without a script that reads the run ledger (#3675):

Terminal window
chant run prod-watch --fail-on-drift

An Op that declares failOn: "drift" (on WatchOp, or on any Op) gets the same exit code without the flag. A run without drift exits 0, a failed run 1 and a gated run 3, as before. See Failing the job on drift.

Every run appends one record to the run ledger on the chant/lifecycle branch, at <env>/runs__<name>.jsonl beside the converge ledger. The record carries the run id, the start and end instants, the status (ok, fail), the Op’s labels, every outcomeAttribute the run captured, and each phase’s per-step status. --json prints that same record, so what you read on stdout and what chant operator log reads back later are one document. <env> is the Op’s own labels.Env, or local when it declares none.

The record also names where the run was built and where it ran (#4020). gitSha is the checkout’s HEAD, or the commit the CI job started for when the checkout has no git. runOrigin is set when the Op ran in CI and has the same shape as a release record’s runOrigin: the forge (github, which also covers Forgejo, or gitlab), the repository and a link to the CI run. The record’s id stays the Op’s own run id. The gate facts the run writes carry the same gitSha and the run id, so a gate joins to its run by either. Chant 0.124.28 and newer write these fields, and a record from an older chant has neither.

Given a runner key, with --key <file> or the PEM in CHANT_RUNNER_KEY, the run signs its record as it settles (#4044). The record gains statement, which --json prints too. It is a DSSE envelope over a statement that names the run and its commit and hashes the record. chant ci tick from chant runs counts only a run that carries one. The key must be one .chant/trust.json at HEAD lists under runners, for a principal holding the runner role. --key refuses any other before the run starts. A CHANT_RUNNER_KEY that is not such a key prints a warning, and the run is recorded unsigned. A run without a key records no statement, as before. Chant 0.124.28 and newer write it.

List all discovered Ops (*.op.ts files) with their declared labels and the run state the selected runtime reports for each.

Terminal window
chant run list
chant run list --on fountain

Output columns: NAME, PHASES, DEPENDS, OVERVIEW. The name carries the run state when the runtime reports one. With --components, the command points at chant list --components instead, which is where a component’s declared shape lives.

Show the state the selected runtime reports for an Op’s latest run. A runtime with no record of the Op says so and exits 0.

Terminal window
chant run status alb-deploy
chant run status alb-deploy --on fountain

A Gate line appears when the latest run stopped at one, naming the gate and how long it has been pending.

Record a gate’s resolution, then wake the runtime hosting the gated run. This is the rename of run signal, and a different act: a resolution is a fact on the gate ledger, not a message to a waiting workflow. The ledger write is the same one chant approve performs, so a resolution recorded either way reads back identically; the runtime is told afterwards only so a parked run wakes now rather than on its next tick. A runtime with no wake hook is fine, since the next run reads the fact.

Terminal window
chant run approve alb-deploy release-approval --approver alex
chant run approve alb-deploy release-approval --on fountain --url https://github.com/acme/infra/pull/42

--approver <name> records who approved, defaulting to GITHUB_ACTOR, GITLAB_USER_LOGIN, or USER. The address the approval happened at goes in --url, which must be an absolute http/https URL. Free-text context goes in --note.

Cancel the active run on the selected runtime. Requires --force. The local runtime runs an Op in the foreground and so has nothing to cancel; use Ctrl-C there, as the command says.

Terminal window
chant run cancel alb-deploy --force --on fountain

On a steward, --force terminates the steward’s conversation and takes its sandbox with it, so the checkout and the tool cache go too. The next run opens a fresh conversation under the same channel_id.

Show the run history the selected runtime holds for an Op, newest first.

Terminal window
chant run log alb-deploy
chant run log alb-deploy --on fountain --limit 10

Replay the events a run on this host kept, or follow a run that is still going (#4042). The events are the run-event stream that --events-json prints, read back from a file instead of from the run’s stdout.

Terminal window
chant run events local-1792206777928 --json
chant run events alb-deploy --json # the newest run of the Op named alb-deploy
chant run events alb-deploy --follow # a run in progress, until it settles

<run> is a run id, or the name of an Op or a component selection, for which the newest kept run of that name stands. --json prints the kept lines exactly as the writer wrote them, one run-event.schema.json object per line. Without it each event is one line for a person, with its time and type, then the component, phase and step it belongs to, then its status. A settled run and a run in progress both replay what has been written so far. A run in progress replays up to now and exits 0.

--follow replays what is kept and goes on printing each event as the run writes it. It returns when the run writes run-settle. It polls the file every 100 ms. If no file exists yet it waits up to 5 seconds for the run to open one, so chant run <op> & followed at once by chant run events <op> --follow works. Ctrl-C ends the follow without a verdict. A run killed with SIGKILL never writes run-settle, so its follow runs until interrupted; SIGINT and SIGTERM settle the run failed first. --follow reads this host only. A run hosted elsewhere (--on fountain) writes nothing here, and following one remotely waits on the off-host in-flight state of #3990. --on other than local is refused for that reason.

The exit code is the replayed run’s verdict, as chant run gives it: 0 for ok, 1 for failed, 3 for gated or waiting. A command that finds no kept events for <run> exits 1.

Where the events are kept, and which runs keep them:

  • Each run gets one file, <git-common-dir>/chant/run-events/<run id>.jsonl, beside the in-flight records in <git-common-dir>/chant/runs/. It is outside the working tree and not on chant/lifecycle, so keeping events adds nothing to the ledger. A run id used again replaces the earlier file.
  • chant run <op> on the local runtime keeps its events whenever it records to the run ledger, with or without --events-json, because an Op run has an id and a ledger record from its first event. --no-record keeps nothing, as it keeps nothing else.
  • chant run --components, components promote, rollback, redeploy, fan-out, pr-plan and pr-apply keep the stream only when they were given --events-json, and then write each line to stdout and to the file. Without the flag they build no stream, so they are not replayable.
  • Old files go when chant lifecycle gc --yes runs. It deletes those older than the retention policy’s runEvents window, 14 days by default, and keeps the newest 100 (#4036). Deleting old ones by hand is safe too, since the files are plain.
  • A run written by a chant older than 0.124.28 has no events to replay.

Why events is a reserved name and not part of run log

Section titled “Why events is a reserved name and not part of run log”

chant run <name> takes an Op name where a subcommand would go, so each subcommand name is reserved: list, status, log, cancel, approve, wave, resume and now events. run log lists the runs a runtime holds for an Op, from the runtime’s own history, and takes --on. run events takes a run id and prints what happened in that run, from files on this host. Folding replay into run log would give one subcommand two argument kinds and two outputs, so events is its own name. An Op named events can no longer be started with chant run events; rename it. The change is listed in level-0 exceptions.

Plans, gates and applies one wave of an Op waves spec. It is the command each job of a pipeline rendered from that spec runs, and it can be run by hand from the repository root.

Terminal window
chant run wave --spec waves.json --wave 2
chant run wave --spec waves.json --wave 3 --decide
chant run wave --spec waves.json --wave 3 --share 1
chant run wave --spec waves.json --wave 4 --group prod --share 2

The wave’s runs are planned with the spec’s plan command, or with one planWave call. Its gate policy and its stage hooks are read from the spec file at --base (default: the spec’s base, else HEAD^1). A wave the policy holds waits on <gate>-wave-<k> under op <name>, bound to the set digest over its runs’ plan digests, and exits 3 until that digest is approved. --decide plans and decides without applying, for a wide wave’s deciding job. --share <i> applies share i of a decided wide wave, refusing a run whose plan moved since the decision. --group <name> does the same for one job group of a decided wave, and with --share <i> for one share of that group. A hook with onFailure: approve that fails makes the wave wait whatever its policy. --json prints the decision, which runs applied and how each hook ran.

Starts again each CI job that waits at a gate an approval now answers, through the forge’s API: a GitHub re-run, a GitLab job retry, or a Forgejo workflow dispatch. It reads every gate ledger on chant/lifecycle, or --op’s alone, and approves nothing. A rendered Op waves pipeline runs it on a schedule when the spec sets resume.

Terminal window
chant run resume --op migrations
chant run resume --op migrations --gate migrations-wave-3
chant run resume --dry-run
chant run resume --op tf-apply --dry-run --json

--gate <name> resumes only the job waiting at that gate, so a caller that has just approved one wave starts that wave’s job and leaves the op’s other waiting jobs alone (#3854). Without --op it looks for the gate in every gate ledger.

A pending fact whose answering approval a wave already applied under (approvals.spend) is spent and resumes nothing. An apply under an approval that stopped before it wrote its finished line, and whose applied line names its run, is started again once the forge says that run is gone (A spent approval and a stopped apply).

--dry-run starts nothing. With --json it prints { dryRun, reports }, one report per gate with resumable, reason (approved, answered, not-approved, spent or stopped), source (pending or stopped) and status (would-resume where it would start a job), so a product that resumes in its own job can act on chant’s decision (Resume in the product’s own job). A dry run reads the forge only to ask whether a stopped apply’s run is gone.

A gate that asks a decision point counts as answered once its question has an answer the gate passes on. The command reads the workspace’s questions for it, including those held on chant/lifecycle (#3835). A pending fact recorded outside CI names no job, and is listed as such. A run still going, already successful, or already resumed is left alone, so the command is safe to run every few minutes. The token comes from CHANT_FORGE_TOKEN, else GITHUB_TOKEN, GH_TOKEN or GITLAB_TOKEN; Resume the waiting job lists the access each forge needs. It exits 1 when a forge refuses a call or no token is set.

--record-plans, in a pull request’s pipeline, plans every wave at the pull request’s head and records each wave’s digest on chant/lifecycle. A wave whose approval is pr-review compares its digest with that record after the merge (Which approvals count).

Run a discovered component’s deploy composition on the same in-process executor an Op uses. all runs every discovered component in dependency order (parallel-safe waves); a single name runs just that component.

A comma-separated list runs the named components and everything they depend on through dependsOn, transitively, in the order all would give them. The whole set runs in one process, so the release records it writes come from one run rather than from several runs racing on the ledger. Before the first component starts, the command prints the resolved order on stderr, wave by wave, along with the dependencies it pulled in that the list did not name:

Terminal window
chant run --components search-service --env staging
chant run --components all --env production
chant run --components web,worker --env staging
# run order: cache + db, then api + worker, then web
# added as dependencies: api, cache, db

A component whose enabled is false under this run’s parameters sits out of all. A component that depends on it still runs, and its dependency on the disabled one is dropped, since dependsOn only orders the run. Naming a disabled component on its own fails with the reason. A pipeline from chant build --components --generate <lexicon> takes the same components.

A list is refused before anything runs when it names a component that does not exist, names one that is disabled (enabled: false) under this run’s parameters, or contains all. The refusal names each offending component. A disabled component that is only a dependency is dropped the same way all drops it. A dependsOn entry that matches no discovered component fails the run up front, as it does under all. The single-name form still runs just that component and leaves its dependencies to earlier jobs, which is what a generated pipeline’s per-component job relies on.

A gate anywhere in a component’s composition stops that component pending chant approve <component> <gate> --env <env>, exit 3, with no saga unwind and no rollback phase. The steps that ran before the gate are the steps the author meant to run before the approval. The approval counts only for that environment and that plan, as Gate approvals and environments explains.

Under --no-record a component’s gate fails instead, and the component unwinds as it would for any failed step. A successful run records no release and persists no build manifest.

--param name=value and --params-file <path> bind declared build-time parameters for the component build, with the same precedence as chant build. --sandbox runs any project source the driver has to execute in one isolated child process; an Op with a policyGate step is refused under --sandbox, since that gate builds the project in the chant process.

--target <component>:<address> (repeatable) plans and applies only that address of the component. A target that names a component the run does not run refuses it, and so does a step whose verb can’t narrow. See Plan and Apply a Few Resources.

Cross-job outputs: --dump-outputs and --seed-outputs

Section titled “Cross-job outputs: --dump-outputs and --seed-outputs”

A single-component run in one CI job can reference a component that ran in an earlier job. --dump-outputs <file> writes the accumulated cross-component and cross-stack outputs after the run as JSON keyed by component name. It is written even when the run fails, so a resumed run still has what completed. --seed-outputs <file> (repeatable) loads such files before the run, so a stackOutput() or @<dep>.publish.* reference to an upstream component resolves.

Terminal window
chant run --components shared-alb --env staging --dump-outputs out/shared-alb.json
chant run --components search-service --env staging --seed-outputs out/shared-alb.json

--digest-file <file> writes one <component>=<digest> line for each release the run recorded (see Auto-recorded releases), after a successful run. The file is empty when the run recorded nothing, for example under --no-release-record, or when it recorded only components with no publish step, whose records name no artifact. The line is the form chant components promote --digest takes, so a later job can promote the exact release this job built rather than whatever is latest in the environment. A pipeline from chant build --components --generate <lexicon> --promote-to <env> uses it that way.

Terminal window
chant run --components search-service --env staging --digest-file search-service.digest
chant components promote --from staging --to prod --digest "$(cat search-service.digest)"

--progress-json streams one NDJSON record per line to stdout while the run executes, so a consumer can render live progress instead of tailing logs. On the component path each line is a RunProgressEvent (wave, component, phase, step). On chant run <name> each line is a StepRecord for a settled step, fed by whatever runtime is hosting the run. Run semantics, ordering and exit code are unchanged. Both shapes are deprecated for new readers.

--events-json prints the run-event stream instead (#4041). Every line names its run, its commit and its runtime. A step’s start comes before its settle, and a gate’s decision is an event of its own. On chant run <name> the runtime writes it: the local runtime as the run executes, and fountain as the steward’s turn goes. The two flags are refused together, since both print to stdout.

On a successful run, chant run --components auto-emits one immutable release record per component that published a digest-bearing artifact, (component, env, digest, gitSha, runId, timestamp, actor), appended to the same release ledger the standalone chant components release command writes to. It is a CLI step after the run, outside the composition, and runs only when the deploy succeeds. See Auto-recorded releases below.

Writes one CI pipeline file per scheduled Op for github, gitlab or forgejo. A --spec file that holds an Op waves spec (a waves key) renders one pipeline of gated waves instead; see Op Waves. It is the Op counterpart to chant build --components --generate, and the command-line form of generateOpsPipeline from @intentius/chant/op. The Ops guide covers what each provider’s generator emits.

Terminal window
chant run --generate github
chant run --generate gitlab --spec ops.json
chant run --generate forgejo --output ci/ --format json

Without --spec, every discovered Op that declares a schedule is generated, on its own cron. An Op triggered by a pull request or a push, or one that needs a finding mode, setup steps or generator options, is named in a JSON file passed as --spec. The file holds either an array of ScheduledOpSpecs or { "ops": [...], "options": {...} }, and both are handed to the generator as they are:

{
"ops": [
{ "name": "app-plan", "trigger": { "kind": "pull_request", "branches": ["main"] }, "findingMode": "comment" },
{ "name": "app-apply", "trigger": { "kind": "push" }, "permissions": { "id-token": "write" } }
]
}

Each file is written exactly as generateOpsPipeline returns it, with one line added at the top:

# Generated by chant — do not edit directly. Regenerate with: chant run --generate github

That line is the generated-file marker chant uses elsewhere, and it names the command, --spec included, that rewrites the file. Run the command from the repository root. GitHub files go to .github/workflows/ and Forgejo files to .forgejo/workflows/, each with a name: that is its Op’s name, so the Actions tab lists workflows by Op. GitLab’s single ops.gitlab-ci.yml (one job per Op) to the root, where the project’s .gitlab-ci.yml includes it. --output <dir> writes somewhere else. --format json prints { files: [{ name, path, content }], jobs } and writes nothing. An unknown Op name, a provider with no Op generator, or no scheduled Op and no --spec exits 1.

When the command runs in a member of a workspace, the files it writes are that member’s. They go to the forge’s directory at the repository root, not the member’s. A GitHub or Forgejo file is named <member>-<op>.yml, and GitLab’s is .gitlab/ci/<member>-ops.gitlab-ci.yml. The workflow’s name: is <member>/<op>, and GitHub concurrency groups and GitLab job names start with the member’s name.

Each Op keeps the trigger its spec gives it, and no trigger is added. A push or pull_request trigger gets a path filter listing the member’s directory and the file itself. On GitLab the filter is a changes: rule. A cron trigger has no paths to filter. The Op’s job runs in the member’s directory. GitHub and Forgejo get there through defaults.run.working-directory, and GitLab through a leading cd.

The marker line names the command to run from the member’s directory, and the member records each file in .chant/generated.json there, until #2541 moves that list into chant.workspace.json. --format json adds member and gives each file’s path relative to the repository root.

Auto-recording is on by default. It writes exactly one record per component a successful run deployed; a failed deploy writes nothing. For a component whose deploy composition published a digest-bearing artifact (via publish-image, publish-artifact, or load-image-on-host), the record’s digest is that artifact’s. A component that only deploys infrastructure publishes nothing, and it is recorded too (#4274). Its record’s digestKind says what digest names in place of an artifact. pr-apply records plan, the plan digest its approval bound; with several planned members that is the set digest over them. A run that planned nothing, as chant run --components does, records deploy-input, the component’s input digest from componentInputDigests (packages/core/src/components/fan-out-inputs.ts). Under --release the record carries the release’s templates digest with templates (#4222).

A plan or deploy-input record says the component applied at that commit and names no artifact. Promote, rollback and redeploy never deploy from it. A promote of every component lists it as not promoted, and --digest-file leaves it out. Status compares two environments’ records of it on gitSha.

A ledger-write failure (e.g. a concurrent push, or no git remote configured) is reported as a warning and never turns an otherwise-successful deploy into a nonzero exit.

Opt out per-invocation with --no-release-record, or project-wide via chant.config.ts:

Terminal window
chant run --components search-service --env staging --no-release-record
chant.config.ts
export default {
release: { autoRecord: false },
};

The --no-release-record flag always wins over config for that one invocation. --no-record also turns release recording off, along with everything else the run would write to the ledger. See Observability for the release ledger this feeds, and chant components status to query it.

--release <label|digest>, from chant 0.124.29, says that this run deploys a release (#4043). The run builds from the checked-out commit, so chant checks the claim before anything deploys. The release must have been cut from the commit HEAD is at, and must have a target for --env. Otherwise the run stops with exit 1 and a message that says which check failed. When it passes, each release record the run writes carries the release’s releaseDigest, and release list shows the environment as running the release.

A run without --release writes records with no releaseDigest, even when HEAD is a commit that a release was cut from. chant does not infer a release, because a rebuilt image is not guaranteed to be the release’s own. The templates the run reads come from the checkout and not from the release’s recorded target, so use components promote --release to deploy the recorded templates. If a recorded deploy’s build manifest holds other bytes than the release’s artifactDigests, the run warns about it as a promote does.

FlagDescription
--localRun with the local in-process executor — the default (run <name>, run --components)
--on <lexicon>Which runtime hosts the run: a configured lexicon with an opRuntime, or local. When omitted, run.on in chant.config.ts decides, and the built-in local runtime runs the Op when that is unset too (every run subcommand)
--componentsTarget a component instead of an Op (run --components)
--generate <provider>Write one CI pipeline file per scheduled Op for github, gitlab or forgejo (run --generate)
--spec <file>JSON Op specs to generate instead of every Op with a schedule (run --generate), or the Op waves spec (run --generate, run wave)
--wave <k>The wave to run, from 1 (run wave)
--decidePlan the wave and decide its gate without applying it (run wave)
--record-plansPlan every wave at a pull request’s head and record the digests a pr-review wave compares (run wave)
--share <i>Apply share i of a decided wide wave, or with --group, of that job group (run wave)
--group <name>Apply one job group’s runs of a decided wave (run wave)
--base <ref>The commit the wave’s gate policy is read from (run wave)
--output <dir>Where generated pipeline files go, instead of the forge’s own directory (run --generate)
--release <label|digest>The run deploys this release, which must be cut from the checked-out commit; the records name it in releaseDigest (run --components). See Running a release
--no-release-recordSkip auto-emitting a release-ledger record after a successful run --components deploy — default is ON (run --components)
--no-recordRun every step but record nothing on chant/lifecycle and push nothing; a gate step fails. CHANT_NO_RECORD=1 does the same for every chant process that inherits it (run <name>, run --components, local runtime)
--env <name>Target environment, threaded into every capability’s context (run --components); defaults to local
--jsonEmit the run’s ledger record as JSON on stdout (run <name>); the structured run result for run --components
--key <file>Sign the run’s record with this runner key, an Ed25519 private key in PEM, so ci tick from chant runs counts it. CHANT_RUNNER_KEY holds the PEM when the flag is absent (run <name>, local runtime)
--gated-exit <code>Exit code for a run that stopped at an unapproved gate, instead of the default 3. Only that outcome is remapped; a failure is still 1
--work <id>The work item an Op with a workLease runs under, in place of the Op’s own (run <name>, local runtime)
--holder <name>Who holds the run’s work lease; <steward>/<op>@<process> for an Op a steward lists, this process’s id otherwise (run <name>, local runtime)
--forceRequired for run cancel
--progress-jsonStream one NDJSON progress record per line to stdout while the run executes
--events-jsonStream the run’s events to stdout, one run-event.schema.json line each
--dump-outputs <file>After the run, write the accumulated cross-component outputs as JSON to file (run --components)
--seed-outputs <file>Before the run, load outputs written by --dump-outputs; repeatable (run --components)
--digest-file <file>After a successful run, write one <component>=<digest> line per release the run recorded (run --components)
--param <name=value>Bind a declared build-time parameter for the component build; repeatable (run --components)
--params-file <path>JSON file of build-time parameter values (run --components)
--sandboxExecute project source in one isolated child process. Refuses an Op with a policyGate step
--approver <name>Who approved a gated change; recorded on the gate-resolution fact (approve, run approve)
-p, --profile <name>Named connection profile the hosting runtime targets, such as a fountain.profiles entry; each runtime’s own default when omitted
--durable-requestsReserved. It would resolve the gate on the hosting runtime’s own request path instead of by prompt (run approve --on <lexicon>). No runtime implements it: fountain’s needs the request-answer path from BinaryBourbon/fountain#1635, which has not shipped, and the flag refuses by naming that issue
-v, --verboseList every resolved build parameter, and show stack traces on errors
CodeMeaning
0Success (the run completed, or the command succeeded)
1Error (the run failed, the Op was not found, the runtime was unreachable)
3Gated: the run stopped at an unapproved gate and recorded the pending fact. For run wave, the wave waits for an approval of its set digest. For --components, also a step that refused a plan that moved since it was approved (#3997)
4run wave --share: a run’s plan moved since its wave was decided, so it was not applied
--gated-exit <code>The gated outcome only, remapped to <code>. 0 and 1 keep their meanings above for every other outcome, and a moved plan still exits 3

chant