Skip to main content

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 --retry the claim carries the retry marker and the platform's sanctioned JOB_RETRIED re-open admits it. First attempts are unaffected — --retry on a non-terminal job claims normally.
  • Failed dependencies are waited out, not fatal. Where a plain run fail-fasts on blocked/failed upstreams, a --retry run 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​

FlagMeaning
--dry-runPreview what would run without executing
--verboseExpand full step logs instead of the compact summary view
--workdirOverride the working directory for all jobs
--jobRun only one job ID (matches plan job ID or prefix)
--laneRun every job of one lane in this process (plan.lanes, see Lanes); on a plan without lanes it is --job. Mutually exclusive with --job
--retryWith --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
--runnerExecution backend: local, docker, or github-actions
--ghaShortcut for --runner github-actions
--exec-idExecution ID for resume or CI tracing (auto-generated if not set)
--concurrencyOverride 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-concurrencyMax 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
--componentFilter jobs to a specific component (repeatable)
--env, -eFilter jobs to specific environments (comma-separated)
--all-envsRun all environments explicitly (mutually exclusive with --env)
--allDisable CWD-based component scoping; run all jobs
--jsonOutput execution summary in JSON format
--isolationPer-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-workspacesPreserve per-job staged workspaces after the run for debugging
--backgroundRun the plan detached and return immediately. Track progress with orun status --watch
--artifactArtifact backend for uploading job shard from CI (github)
--remote-stateUse orun-backend for distributed run coordination (enables remote state)
--backend-urlorun-backend URL for remote state (or set ORUN_BACKEND_URL)
--workspaceWorkspace scope for remote state, overriding the linked workspace (or set ORUN_WORKSPACE). Disambiguates auto-link when you belong to several workspaces
--orgAlias of --workspace (legacy spelling; or set ORUN_ORG)
--projectProject scope for remote state, overriding the linked project (or set ORUN_PROJECT)
--localForce local filesystem state for this run, overriding remote-state config/flags (the escape hatch when the backend is down)

Plan selection flags​

FlagMeaning
--plan, -pPlan reference: file path, name, or checksum prefix (deprecated — pass the reference as a positional argument instead)
--revisionExecute 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.

FlagMeaning
--changedGenerate plan scoped to changed components (requires git)
--baseBase git ref for change detection
--headHead git ref for change detection
--filesExplicit comma-separated changed-file list (overrides git diff)
--uncommittedScope to uncommitted changes
--untrackedScope to untracked files
--explainPrint how --changed resolved its base and head refs
--intent-impactHow 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:

  1. --gha
  2. --runner
  3. ORUN_RUNNER
  4. GITHUB_ACTIONS=true
  5. Auto-detection when the plan contains a use: step
  6. 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 --job and --retry
  • record immutable per-execution logs accessible via orun logs

Plan resolution​

The positional argument is resolved in this order:

ArgumentResolves to
(omitted)Generates a fresh plan from intent.yaml, then runs it
latest or rev-<key>That plan revision in the object model
a1b2c3The plan whose checksum starts with a1b2c3
./plan.jsonExplicit file path (when it exists on disk)
network-foundationGenerates 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:

  1. Flag: --remote-state --backend-url https://…
  2. Environment: ORUN_REMOTE_STATE=true ORUN_BACKEND_URL=https://…
  3. intent.yaml:
    execution:
    state:
    mode: remote
    backendUrl: https://orun-backend.example.com

Authentication​

Remote state mutations (claim, heartbeat, update, log upload) require a bearer token.

Resolution order:

  1. GitHub Actions OIDC — when GITHUB_ACTIONS=true and the OIDC token endpoint vars are set, an OIDC token with audience orun is fetched automatically.
  2. Local Orun CLI session — outside GitHub Actions, run orun auth login once. The CLI resolves the repo namespace automatically from the current Git remote using the active session, then refreshes expired Orun access tokens automatically.
  3. ORUN_TOKEN — explicit fallback for short-lived Orun machine tokens in other CI or automation. orun cloud link and orun cloud check honor 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:

  1. --exec-id or ORUN_EXEC_ID (explicit)
  2. gh-{GITHUB_RUN_ID}-{GITHUB_RUN_ATTEMPT}-{planChecksum} (inside GHA)
  3. local-{planChecksum}-{randomHex} (fallback)

Step environment variables​

When running with remote state, each step receives:

VariableValue
ORUN_PLAN_IDPlan checksum short-hash
ORUN_JOB_IDJob ID (e.g. api.dev.deploy)
ORUN_JOB_UIDContent-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:

  1. Generates a plan and emits the job list as a matrix
  2. Runs each job as a parallel GitHub Actions matrix worker
  3. Uses the backend to enforce DAG ordering