---
title: "Run lifecycle"
description: "The durable states of a managed OpenTofu run from dispatch through terminal evidence."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.stackship.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Run lifecycle

Cloudflare Workflows coordinates each managed run, while PlanetScale stores the
authoritative run, attempt, signal, approval, and state-commit facts. Workflow
state carries opaque identities and capability handles, not secret plaintext or
artifact bodies.

The Workflow instance ID is the run public ID. Every run also retains the
Workflow definition and application build SHA selected at creation. The run
review returns those values rather than inferring a version from the currently
deployed Worker.

## Source provenance

For a run created from GitHub, the authenticated run review follows the
organization-scoped run, input snapshot, committed configuration, ready source
ingestion, and configuration artifact records. It displays the provider, full
commit SHA, working directory, source-ingestion public ID and `ready` status,
source Workflow ID, GitHub delivery ID, configuration-version public ID,
archive SHA-256, and configuration-artifact public ID.

This provenance comes from the persisted relational chain, not source content
embedded in the input snapshot. A run created without source ingestion reports
no source provenance. The review never includes R2 object keys, ciphertext
metadata, upload capabilities, archive bytes, installation tokens, webhook
secrets, or repository content.

## Main path

```text
dispatch_pending
  → queued
  → planning
  → plan_complete
  → policy_evaluating
  → policy_passed
  → awaiting_approval
  → applying
  → succeeded
```

`plan_only` and `speculative` runs move from `policy_passed` directly to
`succeeded`. A `plan_and_apply` run whose finished plan has no changes also
terminates there: its approval and apply stages are marked skipped, its pending
apply record is closed, and no no-op apply is dispatched.

The `plan_complete` transaction also reserves the deterministic policy-check
resource with status `running`. That pending resource is visible atomically
with the plan, so native OpenTofu clients never have to race policy-evaluation
row creation. Its input digest remains null only until the evaluator binds the
exact sanitized plan input; completed evaluations always have a valid digest
and complete result evidence.

For apply runs, the review reports the apply record independently from state
publication. A populated `resulting_state_version_id` is resolved to its exact
state public ID, checksum, serial, and creation time. A null pointer is reported
as `not_recorded`; a non-null pointer that cannot be resolved is reported as
`unavailable`. Neither condition is converted into a successful publication
claim.

## Durable statuses

| Status | Meaning |
| --- | --- |
| `dispatch_pending` | Durable run exists; Workflow dispatch is pending |
| `queued` | Workflow accepted the run and execution is waiting |
| `planning` | The sandbox is running the pinned OpenTofu plan |
| `plan_complete` | Immutable plan evidence is committed |
| `policy_evaluating` | The sanitized plan derivative is being evaluated |
| `policy_passed` | Persisted policy evidence permits progression |
| `awaiting_approval` | The exact plan and policy binding awaits a human decision until its persisted seven-day deadline |
| `applying` | Apply-start authorization was redeemed and the sandbox is applying |
| `succeeded` | The run completed; the dashboard labels plan-only and speculative success as **Plan completed**, and apply success as **Applied** |

Terminal or intervention statuses are:

- `plan_credentials_required`
- `plan_failed`
- `policy_failed`
- `rejected`
- `expired`
- `canceled`
- `apply_failed`
- `apply_start_uncertain`
- `apply_interrupted`
- `needs_recovery`

An unexpected state transition is rejected instead of silently skipping a
control.

If no decision commits before the persisted `approval_expires_at` instant, the
Workflow durably requests database arbitration after its user-action wait
throws. PlanetScale either returns a persisted pre-deadline action, reports
that the wait is still pending, or atomically moves `awaiting_approval` to
terminal `expired` with reason `APPROVAL_EXPIRED`, closes the approval stage,
and releases the workspace writer reservation. A pre-deadline approval
continues even when Workflow event delivery was delayed. A still-pending
result preserves the original wait failure rather than labeling it expiry.
The expired Workflow runs normal cleanup and never prepares apply. Late
approve and reject requests return `409 APPROVAL_EXPIRED`; operators must
create a new run.

## Planning

The runner bootstraps from an immutable manifest and streams encrypted
configuration, lock-file, and predecessor-state artifacts. Inputs bind the
source, exact OpenTofu version, region, variables, credential decisions, and
plan options. The runner produces bounded logs and encrypted plan artifacts.

At most 100 MB of plaintext is accepted for launch artifacts. Workers forward
streams with backpressure and do not aggregate a complete project in memory.
If fail-closed launch controls temporarily deny a new run, the native cloud API
returns `503 service_unavailable` with `Retry-After: 60` and creates no run
record. Existing runs and read paths remain available.

## Policy

Policy publication and evaluation use the same managed sandbox base and
generated policy-runner bundle. The sandbox has unrestricted public egress for on-demand
toolchain bootstrap; the evaluator still receives only a sanitized JSON
derivative and fixed deterministic Rego capabilities, with no customer cloud
credentials. Its result is persisted and digest bound before progression.
The evaluator can only bind the pending row reserved by that run's successful
plan commit. Rebinding a different input digest, completing a row without a
digest, or recording results before binding is rejected.

## Approval and apply

Approval binds the plan, dependency bundle, predecessor state, and policy
result. Before apply, StackShip acquires a fenced workspace lock and rechecks
launch controls, tenant suspension, region, capacity, and the pinned runner
release identity against both the runner and policy-runtime allowlists.
Runner start repeats those exact checks against the release identity stored on
the run; a policy result digest never substitutes for the runtime identity.

Credential capabilities are attempt-, phase-, sandbox-, and expiry-bound.
Bootstrap and secret capabilities are one-time. The bootstrap broker may
replay the same JTI only for an exact, expiry-bounded attempt, phase, and
sandbox redemption so a lost runner-start response remains recoverable; the
runner-session store consumes that JTI once. The apply-start capability retains
an expiry-bounded exact redemption result so a lost response can be retried
without accepting changed input.

Workflow runner-start steps are replay-safe. Before minting or redeeming a new
bootstrap handle, the orchestrator asks the control plane whether the exact
attempt already has command/session evidence or an authenticated terminal
result. A retry with matching persisted authorization, sandbox identity, and
deterministic execution nonce completes from that evidence without starting
the runner again. Current launch gates still protect new starts but cannot
invalidate an already-proven start. Partial or mismatched evidence fails
closed.

The apply process reports its start with the manifest digest already verified
during bootstrap; the runner never recomputes that authority with a second
serializer. OpenTofu reads the run-pinned predecessor and publishes the
successor through the Cloud/TFE API. After the command exits zero, a
session-bound metadata check must prove that the current published state has
the exact run, attempt, sandbox, predecessor, lock, fence, and checksum. Only
then can the result callback commit that state and release the writer lock.
Unavailable or mismatched confirmation is recovery uncertainty, not success.
Ordinary successful Cloud applies create no local state/output artifacts.
OpenTofu can publish partial state even when the provider operation exits
nonzero. That path remains a failed apply and retains its writer authority for
recovery; the published state is not silently reclassified as success.

## Recovery posture

StackShip never treats an ambiguous provider response as success. An uncertain
apply enters `apply_start_uncertain` or `needs_recovery`. A user cancellation
that wins the apply-result race stops the bound sandbox and enters
`apply_interrupted`; the writer fence remains held until explicit recovery.
The run page's recovery panel — positioned between the commit-identity header
with its approval bar and the stage timeline — compares the exact pre-apply
state pointer with the current pointer and, when present, displays the
emergency-state digest and size.
While that writer authority is held, a new native OpenTofu run fails with an
actionable workspace-lock error directing the operator to complete pending run
recovery before retrying. It is not reported as ordinary state movement.

Recovery requires both `run:recover` and `state:recover`, step-up
reauthentication within 15 minutes, a non-empty operator reason, and explicit
confirmation that
StackShip may revoke the interrupted authority and advance the workspace
fence. If the current pointer moved after the run began, the server accepts
`current_state_verified` only when the current version and its published upload
carry the exact interrupted run, attempt, sandbox, predecessor, lock, and
fence. A contiguous chain of up to 1,024 exact publications is accepted. This
covers roughly 720 twenty-second persistence intervals during the four-hour
runner authority window with operational headroom.
`TF_STATE_PERSIST_INTERVAL` is platform-reserved so workspace variables cannot
expand that publication rate. The apply authorization, exact managed-run lock
and writer reservation must still be active. Prepare and commit independently
revalidate that provenance while holding the lineage lock. Any other movement
is rejected. `import_emergency_state` still requires
the original predecessor to remain current; its emergency snapshot must be the
exact next lineage/serial successor and cannot overwrite a newer version.

Durable uploaded or claimed-pending successors are finalized before recovery.
Only an expired pending upload with neither a raw nor JSON claim is discarded
automatically under a database compare-and-set. An unresolved claimed upload
blocks recovery by default. Run review exposes its exact state upload id and
claim timestamp. Submission first attempts exact durable finalization. If the
artifact is still unavailable, a destructive override must confirm both values
and acknowledge that in-flight state can be lost; commit fences the apply and
discards that exact row atomically. `run.recovered` records the superseded id
and claim timestamp.

The recovery mutation records a durable operation before copying artifacts.
An exact replay by the same principal and request resumes the same target state
identity. A completed replay returns the durable result without depending on
artifact storage. A prepared emergency replay verifies the persisted target
through EOF; corrupt persisted content fails closed and is never silently
replaced from the source artifact. A prepared recovery leases that principal
for 15 minutes. No takeover
is accepted before expiry; after expiry, another operator with both recovery
permissions and recent step-up must explicitly confirm takeover while resending
the same resolution and exact current/emergency evidence. The handoff reuses
the recovery and target-state identities, fences the old principal from commit,
and appends bounded audit evidence. The run review reports the database-derived
`lease_expires_at` instant and a clamped `lease_remaining_seconds` value from
0 through 900, and the dashboard shows both before an operator confirms a
takeover. Completion revokes the
runner session and apply authorization, advances the fence under a dedicated
recovery reservation, optionally publishes the emergency snapshot, releases
the reservation, and writes `run.recovered` and `state.recovered` audit
evidence in the state-commit transaction. It never retries, resets, or
replaces `tofu apply`. The recovered run remains historical
`needs_recovery` evidence; the next infrastructure action is a new plan.

## Related pages

- [Runs](/runs)
- [Run approvals](/runs/approvals)
- [Policy sets](/governance/policies)
- [Architecture](/architecture)

Source: https://docs.stackship.run/runs/lifecycle/index.mdx
