Skip to main content

Distributed execution with remote state

A plan's dependency order is part of the standard it encodes, and it has to hold even when the jobs run on many machines at once. With remote state, every runner, whether a GitHub Actions matrix job or a terminal on a laptop, reads and writes one shared run record. Each job claims its work atomically and waits until its dependencies have succeeded, so the DAG compiled into the plan is enforced across runners exactly as it is on one machine.

Remote state is served by Orunbase, the hosted control plane, by default. You can self-host a backend on Cloudflare with orun backend init and point orun at it instead.

Terraform state needs no AWS either

Remote runs also export a TF_HTTP_* environment pointing Terraform's backend "http" at the platform's native state store, so a matrix job needs no S3 bucket or OIDC role for Terraform state — see Terraform state on the platform.

Local state and remote state​

By default, orun run records execution in the object model under .orun/objectmodel/. That works on one machine but cannot coordinate independent runners. --remote-state moves coordination to the backend.

Local stateRemote state
Where the run record lives.orun/objectmodel/ on diskThe backend (Orunbase, or your own)
Coordination across machinesNoYes: one shared run record
Claiming a jobAdvisory file lock (flock)Atomic claim on the backend
Waiting on dependenciesPolls local statePolls the backend's runnable set
AuthenticationNoneGitHub Actions OIDC, or your orun auth login session
orun status / orun logs from another machineNoYes

Which backend​

orun run --remote-state (and orun status / orun logs with --remote-state) resolve the backend in this order:

  1. --backend-url
  2. ORUN_BACKEND_URL
  3. execution.state.backendUrl in intent.yaml
  4. cloud.url in ~/.orun/config.yaml
  5. Orunbase's production API, when nothing else names one

So on Orunbase you need no URL at all. Set one only for a self-hosted backend.

Authentication​

On GitHub Actions, give the workflow permissions: id-token: write. orun requests a GitHub Actions OIDC token with audience orun, and the backend verifies it against GitHub's signing keys. No secrets or static tokens are needed.

On your machine, sign in once with orun auth login (or orun auth login --device on a headless terminal). The session is refreshed automatically. The repository is linked to a workspace on first use; orun cloud link does it explicitly. See workspaces and tenancy.

The credential is resolved in this order:

  1. GitHub Actions OIDC, when ACTIONS_ID_TOKEN_REQUEST_URL is set
  2. ORUN_TOKEN (or ORUN_TOKEN_FILE), a short-lived machine token for headless use
  3. The session stored by orun auth login

GitHub personal access tokens are never used or stored.

Prerequisites​

ItemPurposeRequired
orunThe CLIYes
jqThe harness and workflow scriptsYes
orun auth loginAuthentication for local runsFor local runs
A self-hosted backend and its URLOnly if you do not use OrunbaseNo

Install orun​

cd /path/to/sourceplane/orun
go build -o orun ./cmd/orun
export PATH="$PWD:$PATH"

Or from the module path:

go install github.com/sourceplane/orun/cmd/orun@latest

Install jq​

# macOS
brew install jq

# Ubuntu / Debian
apt-get install -y jq

# Fedora / RHEL
dnf install -y jq

Local remote-state harness​

The harness at examples/remote-state-matrix/run-local-harness.sh proves the same backend coordination semantics as GitHub Actions without leaving your laptop.

What it proves (live run)​

  • Local CLI sessions can claim, heartbeat, update, and upload logs through the backend.
  • Two local processes targeting the same job do not both execute it (duplicate claim check: exactly one log contains execution markers, the other contains zero).
  • Jobs with unmet dependencies poll /runnable instead of failing because local state is empty.
  • orun status --remote-state returns success for expected jobs from a separate command.
  • orun logs --remote-state returns non-empty output for the executed job.

What ORUN_DRY_RUN=1 proves (no credentials needed)​

ORUN_DRY_RUN=1 ./run-local-harness.sh prints the full intended command sequence and exits 0 without making any real backend calls.

Dry-run mode verifies:

  • Command construction is correct (right flags, right job IDs, right backend URL).
  • Shared ORUN_EXEC_ID is exported before concurrent processes launch.
  • Duplicate and dep-wait processes are included.
  • Status and log commands follow the run.

Dry-run mode does not prove:

  • Live backend duplicate-claim enforcement.
  • Real dependency polling via /runnable.
  • Actual remote status or log content.

Use dry-run for CI structure checks. Use the live run (after orun auth login) to prove real backend behavior. orun cloud link is optional — namespace is auto-resolved on first run.

How to run (live)​

# 1. Log in (required)
orun auth login
# or headless:
orun auth login --device

# 2. Run the harness — namespace is auto-resolved on first run
cd examples/remote-state-matrix
./run-local-harness.sh

The harness passes its own default backend URL to every command; set ORUN_BACKEND_URL to point it at Orunbase's API or at your own backend:

ORUN_BACKEND_URL=https://my-backend.example.com ./run-local-harness.sh

Pin a specific exec ID:

ORUN_EXEC_ID=local-my-test-run ./run-local-harness.sh

How to run (dry-run — no credentials)​

cd examples/remote-state-matrix
ORUN_DRY_RUN=1 ./run-local-harness.sh

Manual step-by-step equivalent​

cd examples/remote-state-matrix

orun auth login

orun plan --name remote-state-e2e --all

PLAN_ID="$(orun get plans -o json | jq -r '.[] | select(.Name == "remote-state-e2e") | .Checksum')"
export ORUN_EXEC_ID="local-$(date +%s)-${PLAN_ID}"
export ORUN_REMOTE_STATE=true
# Optional, for a self-hosted backend: export ORUN_BACKEND_URL=https://…

# Launch two processes for foundation.dev.smoke (duplicate claim; the workspace is resolved on first call)
orun run "${PLAN_ID}" --job foundation.dev.smoke --remote-state &
orun run "${PLAN_ID}" --job foundation.dev.smoke --remote-state &

# Launch api.dev.smoke: it waits until foundation.dev.smoke has succeeded
orun run "${PLAN_ID}" --job api.dev.smoke --remote-state &
wait

# Verify
orun status --remote-state --exec-id "${ORUN_EXEC_ID}" --json
orun logs --remote-state --exec-id "${ORUN_EXEC_ID}" --job foundation.dev.smoke

GitHub Actions conformance workflow​

See .github/workflows/remote-state-conformance.yml for the full conformance workflow.

Required repository configuration​

SettingLocationValue
ORUN_BACKEND_URLSettings → Variables → ActionsBackend URL; leave unset to use Orunbase
ORUN_REMOTE_STATE_E2ESettings → Variables → Actionstrue to enable on push/PR (optional)

Workflow permissions​

permissions:
contents: read
id-token: write

The id-token: write permission is mandatory for OIDC authentication.

Plan generation step​

- name: Compile plan
id: plan
working-directory: examples/remote-state-matrix
run: |
orun plan --name remote-state-e2e --all
plan_id="$(orun get plans -o json | jq -r '.[] | select(.Name == "remote-state-e2e") | .Checksum')"
run_id="gha-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${plan_id}"
echo "plan_id=${plan_id}" >> "${GITHUB_OUTPUT}"
echo "run_id=${run_id}" >> "${GITHUB_OUTPUT}"

Matrix execution step​

run-one-job-per-runner:
needs: plan
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.plan.outputs.jobs) }}
env:
ORUN_BACKEND_URL: ${{ vars.ORUN_BACKEND_URL }}
ORUN_REMOTE_STATE: "true"
ORUN_EXEC_ID: ${{ needs.plan.outputs.run_id }}
steps:
- name: Run selected job
working-directory: examples/remote-state-matrix
run: |
orun run '${{ needs.plan.outputs.plan_id }}' \
--job '${{ matrix.job }}' \
--remote-state \
--backend-url "${ORUN_BACKEND_URL}" \
--gha --verbose

Environment fan-out​

# Two GHA jobs, same plan, same exec ID, different env slices:
orun run <plan_id> --env dev --remote-state
orun run <plan_id> --env stage --remote-state

Trigger the conformance workflow​

Manual trigger (always available):

  1. Actions → "orun remote-state conformance" → Run workflow
  2. Select the branch

Automatic (when ORUN_REMOTE_STATE_E2E=true):

Set the repository variable and the workflow runs automatically on push to main or on PRs touching remote-state code paths.

Dry-run guard (always-on, no credentials):

The Harness dry-run guard job always runs on every PR. It checks harness syntax, dry-run command construction, and assertion helper correctness without requiring a live backend.

Intent configuration​

Instead of passing --remote-state on every command, configure it in intent.yaml:

execution:
state:
mode: remote
backendUrl: https://orun-api.example.workers.dev
workspace: ws_3KF9TQ2P # Workspace ID (short, immutable); a slug or org_… id is also accepted

With this in place, orun run, orun status, and orun logs automatically use the backend. The workspace field is the leading spelling of the committed tenancy claim; org is a retained alias.

Monitoring​

From any machine with access to the backend:

orun status --remote-state --backend-url https://… --exec-id gha-12345678-1-a1b2c3 --json
orun logs --remote-state --backend-url https://… --exec-id gha-12345678-1-a1b2c3 \
--job foundation.dev.smoke

Troubleshooting​

orun not found​

'orun' not found on PATH.

Install:

go install github.com/sourceplane/orun/cmd/orun@latest
# or build from source:
go build -o orun ./cmd/orun && export PATH="$PWD:$PATH"
# or set ORUN_BIN to the full path:
ORUN_BIN=/path/to/orun ./run-local-harness.sh

jq not found​

'jq' not found on PATH.

Install:

brew install jq # macOS
apt-get install -y jq # Debian/Ubuntu
dnf install -y jq # Fedora/RHEL

Missing auth — not logged in​

Not logged in to Orun.

Run:

orun auth login # browser OAuth (interactive)
orun auth login --device # device flow (headless / SSH)
repo sourceplane/orun is not known to your Orun session; run `orun auth login` again to refresh namespace access

The backend does not have slug data for this repo in your session. This typically means the session was created before the backend recorded namespace slug mappings.

Fix:

orun auth login

After re-login, re-run orun run --remote-state — namespace is auto-resolved from the fresh session.

No Git remote found​

Could not determine the current Git remote from 'orun auth status'.

Ensure you are running from within a Git repository that has a GitHub remote:

git remote -v # should show a github.com origin
orun cloud link

Expired tokens — 401 Unauthorized​

Access tokens expire. The CLI refreshes them automatically. If the refresh token is also expired or revoked:

orun auth logout
orun auth login

Check status:

orun auth status

Talking to the wrong backend​

If runs, status, or logs do not appear where you expect, check which backend orun resolved. The order is --backend-url, then ORUN_BACKEND_URL, then execution.state.backendUrl in intent.yaml, then cloud.url in ~/.orun/config.yaml (the older backend.url key still works, with a deprecation warning), then Orunbase's production API. orun mcp doctor probes the backend it resolves and reports what it found.

Revoked refresh tokens​

orun auth status # shows "(expired)" or error
orun auth logout # clears local credentials
orun auth login # start a fresh session

Missing OIDC permission (GitHub Actions)​

GitHub Actions OIDC token not available

Add to the workflow:

permissions:
contents: read
id-token: write

Dependency wait timeout​

job <id>: dependency wait timeout (30m0s) exceeded

Check:

  • Did the upstream job fail? (orun status --remote-state)
  • Is the upstream runner still running? (check GitHub Actions job)
  • Is the backend healthy? (curl -fsS $ORUN_BACKEND_URL/)

Logs empty after run​

orun logs --remote-state returns empty output.

  • Logs are uploaded best-effort. If a runner crashed before uploading, logs may be missing.
  • Verify --exec-id matches the run you are inspecting.
  • Re-run the harness with a fresh exec ID.

Why not GitHub PATs?​

GitHub PATs are long-lived credentials with broad scopes. orun auth login issues a short-lived Orun access token scoped to your Orun account, stored in your OS keychain or ~/.orun/credentials.json at 0600. This is more secure and does not require rotation.