Skip to main content

Workflow schema

The complete field reference for orun.dev/v1 workflow files — the language executed in-process by orun since v2.35.0, standalone via orun workflow run or embedded as a workflow: plan step or blueprint hook.

Parsing is strict (KnownFields): any key not documented here is a parse error, never silently ignored.

Top level​

apiVersion: orun.dev/v1 # exactly this value
kind: Workflow
metadata:
name: release # ^[a-z][a-z0-9-]*$
inputs: # optional — map of input name → spec
service:
type: string
required: true
connections: # optional — declared names only; values arrive via the grant
gh: { type: http.bearer }
outputs: # optional — name → CEL over the final state
pr: "steps.open-pr.outputs.number"
maxParallel: 4 # optional — engine default 4
steps: [ … ] # required
FieldTypeMeaning
apiVersionstringMust be orun.dev/v1. A torkflow/v1 file is rejected by name — no converter ships; the v3 design's mapping table is the manual migration path.
kindstringMust be Workflow.
metadata.namestringWorkflow name, ^[a-z][a-z0-9-]*$. Used in the default exec id (<name>-<unixNano>).
inputsmapDeclared inputs. Names match ^[a-z][a-zA-Z0-9]*$.
connectionsmapDeclared connection names and types. Values never appear in the file — they arrive through the step's connections: grant or --connection flags.
outputsmapWorkflow-level outputs: name → CEL expression evaluated over the final state (sees every step). Declared-only — a workflow never dumps raw context across its boundary.
maxParallelintConcurrency bound for the scheduler. Workflow value > run option > default 4.
stepslistThe DAG.

Input spec​

FieldMeaning
typeInput type (string, …).
requiredFail before the first step if unset and no default. Missing required input errors inputs.X is required (--set X=...).
defaultValue used when the input is not provided.
valuesEnum of allowed values.
patternRE2 pattern the value must match.
secretMark the value secret — registered with the redactor.
descriptionShown in help/errors.

Steps​

steps:
- name: build # ^[a-z][a-z0-9-]*$, unique
needs: [prepare] # pull edges — the only edge form; AND-join
if: "inputs.mode != 'skip'" # CEL; false ⇒ skipped (satisfies dependents)
run: ["make", "build"] # exactly one verb per step
env: { CGO_ENABLED: "0" } # run: only
envInherit: [SSH_AUTH_SOCK] # run: only
cwd: "./svc" # run: only; default = the step's run dir
timeout: 10m # default 10m
retry: { maxRetries: 3, baseDelay: 5s, kind: exponential }
continueOnError: false
poll: { interval: 30s, timeout: 20m, until: "exitCode == 0" }
outputs:
digest: "stdout.trim()" # CEL over this step's sealed result
FieldApplies toMeaning
nameallStep name, ^[a-z][a-z0-9-]*$, unique in the file.
needsallUpstream step names. AND-join: the step starts when every upstream reached a satisfying terminal state. A skipped upstream satisfies dependents; a failed upstream blocks them (status blocked) unless this step sets continueOnError.
ifallCEL condition. false ⇒ status skipped, which still satisfies dependents.
runverbArgv array — executed directly, never through a shell. ; and $( ) in arguments are literals. Shell semantics are an explicit opt-in: ["bash", "-lc", "…"].
actionverbA built-in action: http.request, script, sleep. Unknown action → unknown action "x" (built-ins: http.request, script, sleep).
workflowverbPath to a nested workflow, relative to the parent's directory. Cycle-detected by absolute path. with: becomes the child's inputs; the child's declared outputs become this step's output.
withaction/workflowArguments. Validated against the action's schema before invoke. Rejected on run: steps.
envrunExtra environment for the child process. Values are interpolated; wins over envInherit on collision.
envInheritrunList of host env var names passed through to the child — the declared, review-visible escape hatch for process-inherited channels like SSH_AUTH_SOCK. A name absent from the host env is silently omitted.
cwdrunWorking directory. Defaults to the step's run dir under .orun/wfruns/<execId>/ — never the process cwd.
connectionactionA declared connection name. http.bearer injects Authorization: Bearer <token> on http.request. Undeclared name → compile error.
timeoutallPer-attempt timeout (Go duration). Default 10m. A timed-out run: is killed: run: timed out (killed): <argv0>.
retryall{maxRetries, baseDelay, kind}. Attempts = maxRetries + 1; baseDelay default 1s; kind is exponential (default — delay doubles per attempt) or fixed. Mutually exclusive with poll.
continueOnErrorallThis step starts even if an upstream failed.
pollallThe wait primitive — see below.
outputsallDeclared step outputs: name → CEL over the step's sealed result. If a step declares no outputs: and its verb produced an output map, that map is used verbatim.

env, envInherit, and cwd on an action: or workflow: step are a validation error: step X: env/envInherit/cwd apply only to run: steps.

poll: — the wait primitive​

poll:
interval: 30s # required, positive
timeout: 20m # required, positive
until: "exitCode == 0" # required CEL

The step's verb re-executes every interval until until evaluates true, timeout expires, or the run is cancelled. For a run: step, a non-zero exit is an evaluable attempt, not a terminal fault — gh pr checks exits non-zero while checks are pending, which is exactly the loop poll exists for. Outputs are sealed from the satisfying attempt; every attempt increments the recorded attempts count. On expiry: poll: <until> not satisfied within <timeout> after N attempt(s).

poll and retry on one step are rejected at validate time — the poll loop subsumes retry.

Built-in actions​

Actionwith:Result (output)
http.requesturl (required), method (default GET), body, headers{status, body, json?} — json present when the body parses as JSON; 4 MiB read cap
scriptsource (required — JavaScript), input{value} — sandboxed pure compute: no host bindings, no I/O, interrupted on timeout
sleepduration (required, Go duration){slept}

Expressions​

Expressions are CEL, with the strings extension enabled (stdout.trim() works). They appear in if:, poll.until, step outputs, workflow outputs, and in {{ … }} interpolation inside run[], env, cwd, and with (recursively). A string that is exactly one {{ }} segment keeps the expression's native type; mixed strings stringify.

VariableWhereShape
inputseverywhereThe resolved workflow inputs.
connectionseverywhereDeclared connection values (granted at run time).
steps.<name>everywhere (visibility-checked){outputs, status, failed, skipped, exitCode, stdout, stderr} of an ancestor step.
outputstep outputs, poll.untilThe verb's structured result (see action table; empty for run:).
exitCode, stdout, stderrstep outputs, poll.untilThe run: result triple. Captures are capped at 4 MiB per stream.
attemptpoll.until1-based attempt counter.

Dashed step names work: steps.open-pr.outputs.number is rewritten to steps['open-pr']… before compilation.

Compile-time guarantees​

orun workflow validate (and orun plan, for embedded workflows) verifies:

  • the DAG is acyclic and every needs target exists;
  • every step has exactly one verb;
  • every action resolves and its with: validates;
  • every CEL expression compiles;
  • transitive-upstream visibility — steps.X is referenceable only where X is an ancestor via needs. Reading a parallel branch's outputs is rejected as a race by construction;
  • every inputs.N / connections.N reference is declared.

Execution and run state​

The scheduler is event-driven, bounded by maxParallel. Step status is one of succeeded, failed, skipped (an if: that evaluated false), or blocked (an upstream failed). A failed run: step records the full exitCode/stdout/stderr triple and surfaces the stderr tail in its error.

Run state is file-backed JSON under .orun/wfruns/<execId>/:

.orun/wfruns/<execId>/
├── metadata.json # {workflow, digest, startedAt}
├── steps/<name>.json # per-step: status, outputs, exitCode, stdout, stderr, error, attempts
├── result.json # {status, outputs}
└── .orun-bin/orun # the self shim (see below)

Resume (--resume <execId>, or resume: true on a plan step) re-executes only non-succeeded steps — succeeded and skipped stand; failed, blocked, and in-flight states are cleared. A resumed run refuses a changed file: workflow digest changed (A → B) — a resumed run must execute the file it started with.

How run: steps see the environment​

The child environment is a hygienic allowlist, assembled in this order (later wins):

  1. PATH, HOME, TMPDIR — the base;
  2. ORUN_FLOW_SOURCE_REPO / ORUN_FLOW_SOURCE_REF / ORUN_FLOW_SOURCE_SHA / ORUN_FLOW_SOURCE_URL — ambient provenance for remotely-fetched workflows;
  3. envInherit names present in the host environment;
  4. env: declared keys (interpolated).

PATH is prepended with <runDir>/.orun-bin, which holds a shim to the very binary running the workflow — a step that invokes orun always gets the engine's own binary, never a stale install on the host PATH.

Digest​

sha256:<hex> over the file's raw bytes — printed by orun workflow digest, pinned into plan.json and .orun/provenance.lock, re-verified fail-closed at run time. The format is unchanged from v2, so existing provenance locks stay stable.

See also​

  • Workflows — the concept and the two embedded surfaces
  • orun workflow — validate / digest / run / view, remote refs, resume
  • Plan schema — how a workflow: step materializes