orun run
orun run is the execute step: it runs exactly what a verified, compiled plan says, on the backend you choose, and records every step.
orun run executes the jobs and steps from a compiled plan. Execution is the default — add --dry-run to preview without running.
When run from inside a component directory, orun run automatically scopes execution to that component and its transitive dependencies. Use --all to run all jobs.
Usage
orun run [component|planhash]
The optional positional argument controls what gets run:
- component name — generates a fresh plan scoped to that component and runs it immediately
- plan revision —
latest, a revision key, or a checksum prefix: runs that compiled plan from the object model - (omitted) — generates a fresh plan from the current intent and runs it
Every execution is recorded as an immutable node in the object model under its
PlanRevision, with executions/latest moved to point at it. Use
--revision <key> to skip the resolution chain and execute a specific revision
directly. See State model for the resolution order
and the on-disk layout.
# Generate and run a fresh plan scoped to one component
orun run network-foundation
# Run a previously saved plan by name or hash prefix
orun run release-candidate
orun run a1b2c3
# Generate and run a full fresh plan
orun run
Common examples
Preview the execution order without running:
orun run --dry-run
Generate and run a fresh plan for one component immediately:
orun run api-edge-worker
Execute a saved plan by name (legacy --plan form still works but is deprecated):
orun run my-plan
Execute a specific plan file:
orun run --plan /tmp/plan.json
Execute a specific PlanRevision by key (skips the resolution chain):
orun run --revision rev-pr139-def456a-p8f31c09
Execute inside Docker:
orun run --runner docker
Force GitHub Actions compatibility mode:
orun run --gha
Show full step logs instead of the compact summary view:
orun run --verbose
Run only one job:
orun run --job network-foundation.development.validate-terraform
Retry a failed job (clears its saved state first):
orun run --job network-foundation.development.validate-terraform --retry
Resume-aware CI reruns
With the exec-id pinned to the CI run id, a rerun resumes the same execution —
and --retry makes the resume work end-to-end on remote state:
- The failed remote claim is re-opened. A terminally-failed job would
normally refuse a new claim (
already failed on another runner); with--retrythe claim carries the retry marker and the platform's sanctionedJOB_RETRIEDre-open admits it. First attempts are unaffected —--retryon a non-terminal job claims normally. - Failed dependencies are waited out, not fatal. Where a plain run
fail-fasts on blocked/failed upstreams, a
--retryrun polls while the upstream jobs' own retry lanes recover (○ dependencies of <job> failed in a prior attempt — waiting for their retry lanes...), bounded by the 30-minute dependency deadline.
The practical recipe: give each CI job lane --job <id> --retry, and
gh run rerun --failed on an orun-driven workflow just works — succeeded jobs
stand, failed jobs re-open and re-run in dependency order. (Applies to the
native coordination path; ORUN_COORDINATION=legacy keeps the old behavior.)
A result the server never recorded
A job's result reaches the platform with :complete. When that call fails
(the platform throttles or is down), orun retries it for up to three
minutes, renewing the job's lease between attempts. If the result still
does not land, orun prints
✕ result for <job> not recorded on the server: <error> and the run exits
non-zero (N job result(s) could not be recorded on the server), even when
every job passed here. The server would otherwise show the job running or
queued under a green check.
A :complete refused because the lease was lost is not an error when the
platform already holds a result for the job, or when another runner holds
the job now. In both cases the job's result comes from elsewhere. When the
platform holds neither, the result is lost, and the run says so as above.
A heartbeat that fails is printed once per job
(⚠ heartbeat for <job> is failing (<error>) — the lease may lapse before the job ends), and the job keeps running.
On a rerun, a claim refused because the job is finished is classified from
the platform's own record of the run (GET …/runs/{id}/state), not its read
model. A job that passed is skipped as passed, never reported as
already failed on another runner.
Override the working directory for every job:
orun run --workdir ./examples
Filter to one environment or component at runtime:
orun run --env staging
orun run --component api-edge-worker
Run all jobs when inside a component directory (override auto-scoping):
orun run --all
Override plan concurrency:
orun run --concurrency 4
When --concurrency is greater than 1, every job starts as soon as its upstreams finish, and a free slot goes to the ready job with the longest chain of dependents behind it (the scheduler a lane uses). Result lines carry inline component and environment labels (e.g., platform-shared·production/verify-turbo-package) so each line is self-describing without group headers.
Keep a terminal run's output grouped by component by capping how many components have jobs in flight at once:
orun run --concurrency 8 --component-concurrency 3
Pin an execution ID for CI/parallel-safe tracking:
orun run --exec-id ci-run-${GITHUB_RUN_ID}
Run detached and monitor separately:
orun run --background
orun status --watch
Run only changed components (useful in PRs):
orun run --changed --base main
Debug how --changed resolved its git refs:
orun run --changed --explain
Timeouts
Every step is bounded: by its own timeout, else its job's, else the
intent's execution.defaults.timeout (carried on the plan as
execution.timeout), else 60 minutes. A step that outlives its bound fails
its job; the run goes on with jobs that do not depend on it. Set
execution.defaults.timeout for a platform whose longest step (a large
terraform apply, a full rebuild) needs more than an hour, and a job
template's timeout for the one job that does.
A lane job's wait for an upstream in another lane is bounded by
execution.lanes.upstreamWait (default 60 minutes). On Orunbase the wait
is event-driven: the job is woken by the run's next event and starts within
moments of its upstream finishing, and the events the wake returns are
folded onto the state the process keeps, so a waiting job costs the
platform one long-poll per wake (lengthening to one per 15 seconds while
nothing happens); elsewhere it polls, backing off to 15 seconds.
Interrupting a run
SIGTERM (a spot runner's interruption notice forwarded by the pool, a
cancelled CI job) or SIGINT (Ctrl-C) drains the run: running steps are
stopped, no further job is admitted, and every job that was cut is reported
failed with an interrupted error — to the platform too, so a lane
waiting on one of them stops waiting at once instead of waiting out the
upstream timeout. The run exits with the shell's code for the signal (130
for SIGINT, 143 for SIGTERM); a second signal exits immediately.
orun run --retry with the same --exec-id and --job/--lane re-opens
the cut jobs; a job is re-run from its first step, since the staged tree its
earlier steps built no longer exists on the runner that picks it up.
A runner that disappears without a signal (a reclaimed instance) stops
heartbeating. A job waiting on one of its jobs treats a running upstream
whose heartbeat is older than two of the server's leases as having lost its
runner and ends its wait naming it, rather than holding its own runner;
before any claim has told the run the lease length, the legacy five minutes
apply.
Flags
Execution flags
| Flag | Meaning |
|---|---|
--dry-run | Preview what would run without executing |
--verbose | Expand full step logs instead of the compact summary view |
--workdir | Override the working directory for all jobs |
--job | Run only one job ID (matches plan job ID or prefix) |
--lane | Run every job of one lane in this process (plan.lanes, see Lanes); on a plan without lanes it is --job. Mutually exclusive with --job |
--retry | With --lane, re-opens the lane's failed jobs and leaves completed ones completed. Clear existing state for --job before running. On remote state, also re-opens the job's failed remote claim and waits out failed upstream dependencies — see Resume-aware CI reruns |
--runner | Execution backend: local, docker, or github-actions |
--gha | Shortcut for --runner github-actions |
--exec-id | Execution ID for resume or CI tracing (auto-generated if not set) |
--concurrency | Override plan concurrency (0 uses the plan's value). Above 1, jobs run on the lane scheduler: each starts as soon as its upstreams finish |
--component-concurrency | Max components with jobs in flight at once (default 0 = no cap). A job of a new component waits, holding no slot, until a component drains; set it to keep a terminal run's output grouped by component |
--component | Filter jobs to a specific component (repeatable) |
--env, -e | Filter jobs to specific environments (comma-separated) |
--all-envs | Run all environments explicitly (mutually exclusive with --env) |
--all | Disable CWD-based component scoping; run all jobs |
--json | Output execution summary in JSON format |
--isolation | Per-job workspace isolation: auto (on when concurrency > 1), workspace (always on), component (always on; copies only the job's directory and links the rest, see execution runtime), or none (legacy shared tree). Default: auto |
--keep-workspaces | Preserve per-job staged workspaces after the run for debugging |
--background | Run the plan detached and return immediately. Track progress with orun status --watch |
--artifact | Artifact backend for uploading job shard from CI (github) |
--remote-state | Use orun-backend for distributed run coordination (enables remote state) |
--backend-url | orun-backend URL for remote state (or set ORUN_BACKEND_URL) |
--workspace | Workspace scope for remote state, overriding the linked workspace (or set ORUN_WORKSPACE). Disambiguates auto-link when you belong to several workspaces |
--org | Alias of --workspace (legacy spelling; or set ORUN_ORG) |
--project | Project scope for remote state, overriding the linked project (or set ORUN_PROJECT) |
--local | Force local filesystem state for this run, overriding remote-state config/flags (the escape hatch when the backend is down) |
Plan selection flags
| Flag | Meaning |
|---|---|
--plan, -p | Plan reference: file path, name, or checksum prefix (deprecated — pass the reference as a positional argument instead) |
--revision | Execute a specific PlanRevision by key (e.g. rev-pr139-def456a-p8f31c09). Skips the resolution chain. |
Change detection flags
These flags generate a fresh plan scoped to changed components before running. They are ignored when the positional argument resolves to a saved plan.
| Flag | Meaning |
|---|---|
--changed | Generate plan scoped to changed components (requires git) |
--base | Base git ref for change detection |
--head | Head git ref for change detection |
--files | Explicit comma-separated changed-file list (overrides git diff) |
--uncommitted | Scope to uncommitted changes |
--untracked | Scope to untracked files |
--explain | Print how --changed resolved its base and head refs |
--intent-impact | How global intent changes affect components: all, watch (default), or none |
Environment selection and the fail-closed guard
orun run runs every environment by default. Narrow it with --env <list>
(comma-separated) or --component <list>, or make the all-environments choice
explicit with --all-envs. --env and --all-envs are mutually exclusive.
For safety, a mutating orun run with no environment/component selection is
guarded. Today it prints a deprecation warning and still runs all environments;
a future release turns this into a hard error. To run everything explicitly,
pass --all-envs. --dry-run previews are never guarded, and a saved
plan/revision or a component argument already counts as an explicit selection.
orun run --env staging # one environment
orun run --env staging,prod # several
orun run --all-envs # all environments, explicitly
orun run --dry-run # preview all environments (read-only)
Backend resolution
run chooses its backend in this order:
--gha--runnerORUN_RUNNERGITHUB_ACTIONS=true- Auto-detection when the plan contains a
use:step - Default to
local
State and execution records
Each run records an immutable execution in the content-addressed object model
under .orun/objectmodel/ — the execution node and its jobs, attempts, steps, and
per-step log blobs — and moves executions/latest (and executions/by-id/<exec-id>)
to point at it. While the run is in flight it is published under
executions/live/<exec-id> so orun status, orun logs, and the cockpit can
follow it; on finish it is sealed into immutable objects. See
State model for the layout.
That model lets run:
- skip already-completed jobs on resume (
--exec-id), carrying their prior step logs forward - retry a single failed job with
--joband--retry - record immutable per-execution logs accessible via
orun logs
Plan resolution
The positional argument is resolved in this order:
| Argument | Resolves to |
|---|---|
| (omitted) | Generates a fresh plan from intent.yaml, then runs it |
latest or rev-<key> | That plan revision in the object model |
a1b2c3 | The plan whose checksum starts with a1b2c3 |
./plan.json | Explicit file path (when it exists on disk) |
network-foundation | Generates a fresh plan scoped to that component, then runs it (when it does not resolve to a plan revision) |
A name given to orun plan --name is not a run reference; run a plan by its checksum or latest.
The legacy --plan flag accepts the same values and is still supported, but the positional form is preferred.
Scope mismatch warning
When a plan was generated with CWD-based scoping (e.g., from the api component directory) and you run it from a different component directory, orun run prints a warning:
warning: plan was generated for [api, common-services] but current scope is [web, common-services]
To avoid the mismatch, either regenerate the plan from your current directory or use --all.
See context-aware discovery for full details on auto-scoping behavior.
Remote state (distributed execution)
orun run --remote-state delegates execution coordination to a backend: Orunbase by default, or a self-hosted one (orun backend). Each GitHub Actions matrix job runs orun run --remote-state --job <id>, and the backend enforces DAG ordering: a job polls until its dependencies complete before claiming work. See distributed execution with remote state.
Creating the run before the lanes: orun run init
orun run init --plan <ref> --exec-id <id> --remote-state [--env <name>]
orun run init creates the remote run for a plan, or joins it if it already exists,
without executing or claiming any job. Run it in the CI job that compiles the plan, so the
run exists on the platform before the first matrix lane is queued; each lane then runs
orun run --job <id> --exec-id <same id> --remote-state and joins it. It requires remote
state (--remote-state, ORUN_REMOTE_STATE=true, or execution.state in intent.yaml)
and accepts the same --workspace, --project, --backend-url, and --workdir flags as
orun run.
Native event-sourced coordination
Coordination flows over a native, event-sourced wire: claim, heartbeat,
complete, and the runnable-frontier read are reduced from the run's event log (the
same fold the server runs), while run creation, logs, and read-model loads use the
existing REST endpoints. This is the default — the legacy OP2 relational
coordination path is decommissioned. Set ORUN_COORDINATION=legacy as a temporary
escape hatch back to the old path.
Memoized cache hits
A job labelled orun.dev/hermetic: "true" is memoized: when the backend already
has a result for the same inputs from a prior run, the run adopts that result
instead of re-executing, and surfaces it:
✓ memoized api.prod.build — cache hit, skipped execution (a1b2c3d4e5f6)
The digest in parentheses is the resolved result's content hash. A memoized job is
already terminal, so it's skipped — this line distinguishes it from a job skipped
because it was filtered out of the run — and the run summary counts it as
N memoized, apart from the jobs resumed from this execution's own state.
The inputs the key covers are the job's resolved steps, the names of its
environment variables and secret references (never their values), the
composition sources the plan was compiled against (name, kind, reference and
resolved digest), and a content digest of the job's directory in the checkout:
in a git checkout, every tracked file's index entry plus the content of every
file changed, added or deleted in the work tree, so an edit counts before it is
staged and ignored files (build outputs, node_modules) do not; outside git,
every file under the directory less the dependency and output directories a
job recreates. A job whose directory cannot be digested is claimed without a
key and runs, with a note on stderr.
A hit restores nothing: the result records that the job succeeded, not what it produced. Label a job hermetic only when its dependents consume nothing it writes to the tree — a verification, a test, a lint, a plan whose output is a published artifact — and not a build whose output the next job reads.
Enabling remote state
Three equivalent ways, in priority order:
- Flag:
--remote-state --backend-url https://… - Environment:
ORUN_REMOTE_STATE=true ORUN_BACKEND_URL=https://… - intent.yaml:
execution:state:mode: remotebackendUrl: https://orun-backend.example.com
Authentication
Remote state mutations (claim, heartbeat, update, log upload) require a bearer token.
Resolution order:
- GitHub Actions OIDC — when
GITHUB_ACTIONS=trueand the OIDC token endpoint vars are set, an OIDC token with audienceorunis fetched automatically. - Local Orun CLI session — outside GitHub Actions, run
orun auth loginonce. The CLI resolves the repo namespace automatically from the current Git remote using the active session, then refreshes expired Orun access tokens automatically. ORUN_TOKEN— explicit fallback for short-lived Orun machine tokens in other CI or automation.orun cloud linkandorun cloud checkhonor it too, so a headless container can bootstrap its own namespace link with the same token.
Normal local remote-state usage does not require a GitHub PAT.
Local setup
The happy path is two steps:
orun auth login --backend-url https://orun-api.example.workers.dev
orun run --remote-state -i examples/intent.yaml --dry-run
orun auth login already auto-links the repo (see orun auth),
so the scope is cached in ~/.orun/config.yaml before your first run. Even if it
isn't, orun run --remote-state self-heals: a logged-in run against an
unlinked repo links it on the fly and proceeds, rather than dead-ending with "repo
not linked". When you belong to several orgs and the shell is non-interactive,
disambiguate with --workspace / --project (or ORUN_WORKSPACE / ORUN_PROJECT; --org / ORUN_ORG are the legacy spellings).
For headless terminals:
orun auth login --device --backend-url https://orun-api.example.workers.dev
orun cloud link is available for explicit pre-caching or diagnostics but is no longer required before orun run --remote-state.
Local escape hatch
When the backend is unreachable, orun run --local forces local filesystem state
for that run, overriding any --remote-state flag or configured remote state. It
is the safe fallback that keeps a run from being blocked on the platform.
orun run --local -i examples/intent.yaml
Run ID derivation
The run ID is derived in this order:
--exec-idorORUN_EXEC_ID(explicit)gh-{GITHUB_RUN_ID}-{GITHUB_RUN_ATTEMPT}-{planChecksum}(inside GHA)local-{planChecksum}-{randomHex}(fallback)
Step environment variables
When running with remote state, each step receives:
| Variable | Value |
|---|---|
ORUN_PLAN_ID | Plan checksum short-hash |
ORUN_JOB_ID | Job ID (e.g. api.dev.deploy) |
ORUN_JOB_UID | Content-addressed job UID (stable while the job's inputs are unchanged) |
ORUN_JOB_RUN_ID | {execID}/{jobUID} — stable per-job identifier |
GitHub Actions matrix example
See examples/github-actions/remote-state-matrix.yml for a full workflow that:
- Generates a plan and emits the job list as a matrix
- Runs each job as a parallel GitHub Actions matrix worker
- Uses the backend to enforce DAG ordering