Skip to content

Runs

A run is one execution of a stack's infrastructure code: it plans, optionally waits for approval, and applies. Runs are triggered from the stack's Trigger menu, by pushes and pull requests, or by drift schedules. Triggering, approving, discarding and cancelling runs require the Write role.

Run types

Type Trigger menu item What it does
Tracked Plan & Apply Plans, waits for approval, then applies. The standard deployment workflow.
Proposed Plan Only Plans and stops. Pull-request and plan-only drift runs are proposed runs.
Destroy Destroy Plans and destroys everything the stack manages. Destroy runs do not wait for approval.

A fourth type, Task, is reserved in the API and does nothing yet.

Lifecycle

Queued → Claiming → Preparing → Initializing → Planning → Awaiting Approval → Applying → Finished

That is the tracked run. A proposed run stops after Planning. A destroy run has no Planning status: it goes Preparing → Initializing → Destroying → Finished.

Status Meaning
Queued Waiting for a worker in the stack's pool.
Claiming A worker is taking the run.
Preparing The worker downloads the repository at the pinned commit and the configuration bundles, applies variables and, for Zenfra-managed state, configures the backend.
Initializing terraform init or tofu init.
Planning Generates the plan. A tracked run then either waits for approval or finishes with No changes (below).
Awaiting Approval The plan is ready. Confirm opens Approve & Apply with the resource impact when the policy verdict allows it. While the policy evaluation is incomplete, the row explains why in place of Confirm and links to the Policy panel, where a Write or Admin user can Re-evaluate. Discard cancels the run. A banner shows the approval deadline; after it passes the run may be cancelled automatically.
Applying Executes the approved plan.
Destroying Destroys the stack's resources (destroy runs; the destroy plan happens inside this status).
Finished Completed successfully, whether something was applied or there was nothing to apply.

Terminal statuses other than Finished: Failed (an error in any phase, a plan denied by a policy, or logs that did not fully reach Zenfra), Cancelled (cancelled or discarded by a user), Skipped (reserved; a policy never skips a run) and Dead Letter (failed with no more retries). Performing is reserved for task runs and currently unused.

Cancel on the run page stops a live run and is available in every live status except Awaiting Approval, where Discard is the equivalent. Both leave the run Cancelled.

Cancelling a running tool

When you cancel while Terraform or OpenTofu is running (init, plan, apply or destroy), the worker sends the tool one interrupt, the same as pressing Ctrl-C once, and gives it time to stop on its own: the chance to end the operation in progress, save its state and release its state lock. The run keeps its current status while the tool stops and shows Cancelled once it has ended. If the tool is still running when the cancellation window ends, the worker kills it. The window is 5 minutes by default; a private worker can set it (see Cancellation window), and Zenfra caps it at 15 minutes.

Cancelling gives the tool the chance to save its state and release its lock; it does not guarantee it. The run log says what was observed:

Log line Meaning
[Zenfra] cancel: interrupt sent; waiting up to <window> for the tool to stop The tool got its one interrupt.
[Zenfra] cancel: tool stopped on its own The tool ended by itself; it had the chance to save state and unlock.
[Zenfra] cancel: tool killed after <duration>; it may not have saved state or released its lock. Confirm the operation has stopped and review the state before recovering by hand The window ended, or the run was stopped for another reason, and the tool was killed.

After a kill, the state may miss the last changes and the lock may still be held. Confirm nothing is still running against your infrastructure, then review the state. With your own backend, release a stuck lock with terraform force-unlock or tofu force-unlock; with Zenfra-managed state, an Admin releases it from the stack page (see A state lock left behind).

Even when the tool stops on its own, cancelling an apply or a destroy can leave resources half created or half destroyed. Review the plan of the next run before applying it.

  • Nothing to interrupt. A cancel while the worker prepares the workspace or is between steps ends the run at once.
  • Hooks are killed at once. A hook that is running when you cancel gets no window; a hook that runs terraform or tofu itself can leave a lock held.
  • An unresponsive worker. Zenfra ends the run itself when the worker stops reporting. It becomes eligible 4.5 minutes after you cancelled if the worker never confirmed the cancel, or 2.5 minutes after the end of the window if it confirmed (the window starts when the worker confirms, up to 2 minutes after you cancel); Zenfra checks once a minute, and a run whose worker stopped heartbeating can end sooner. Such a run has no closing log line.
  • Older private workers kill the tool at once, as before. A worker that stops tools gracefully lists cancel-graceful-v1 under Capabilities in its pool's Connected Workers table.

Force cancel

An Admin can stop waiting for the window: once a run has been cancelled for 60 seconds, Force cancel on the run page (or POST /api/v1/runs/{run_id}/force-cancel) requests an immediate kill. The confirmation reads "Requests an immediate kill. The tool may not save state or release its lock." The worker kills the tool on its next heartbeat (at most about 30 seconds while a tool runs) and the run log shows the kill line above. Forcing never moves the kill later: if the window ends first, it ends first. A repeated request changes nothing and returns the first one. The request is refused (409 force_cancel_unavailable) before the 60 seconds have passed or once the run has ended. Each request appears in the audit log as run.force_cancel_requested, with recorded: false for a repeat.

If the worker no longer responds, the force cannot reach it; the run becomes eligible for Zenfra to end it 2.5 minutes after the force, Zenfra checks once a minute, and the outcome is forced.

GET /api/v1/runs/{run_id} reports the cancellation: cancel_ack_at (when the worker confirmed it), cancel_deadline_at (the latest moment of the recorded window; the tool can be killed earlier, for example when the worker shuts down or the run reaches its time limit) and, once the run has ended, cancel_outcome: stopped, killed, kill_unconfirmed (the kill was sent but could not be confirmed), no_tool or forced (Zenfra ended the run itself). After a force request it also reports force_cancel_requested_at and cancel_effective_deadline_at (the earlier of the window's end and the force request); while a cancelled run is live, force_cancel_available_at says when Force cancel becomes available.

No changes

A tracked run whose plan reports no changes finishes right after planning: the timeline shows No changes (Infrastructure matches the configuration; nothing to apply), there is no approval step, nothing is applied and the stack's lock is released. On GitHub, the commit check also reads "No changes". A plan that changes only outputs, or contains moved or import changes, still waits for approval because applying it updates state. Refresh-only differences found by an empty plan are not written to the state; the next run with changes writes them.

Source commit

On a stack backed by a source control integration, every run is pinned to a commit when it is created: the branch is resolved once and both plan and apply use that commit. The 7-character SHA is shown on the run page (with a link to the provider) and in the stack's Runs tab, and the Preparing log records [Zenfra] Source commit: <sha>. A plan approved after someone pushes to the branch still applies the commit it planned. The commit is absent on raw-git stacks and on runs created before mid-September 2026.

Because the branch is resolved at creation, triggering can now fail before a run exists. Branch and provider errors name only the branch; connector errors use the connector wording:

Cause Error code
The branch is missing or was renamed ref_unresolvable
The repository or integration no longer resolves source_config_invalid
The VCS connector is offline or was deleted connector_offline, connector_deleted
The VCS credential was rejected or lacks scope vcs_auth_failed, vcs_scope_insufficient
The provider rate-limited the lookup rate_limited (HTTP 429)
The provider timed out or was unavailable provider_timeout, provider_unavailable, provider_error

Trigger sources

The stack's Runs tab shows Status, Changes (+n ~n -n), IaC, Started, Duration, Trigger and Actions, and filters on the trigger source: Manual, Push or Pull Request (runs started by a drift schedule show a Drift badge). See Source Control for what starts push and pull-request runs.

Failures you may see

Message or code Meaning Where to look
BACKEND_POLICY_REFUSED A customer-managed stack did not resolve an s3 backend after init. Terraform State
ENV_POLICY_REFUSED A stack, bundle or run variable sets one of the reserved variables. Terraform State
log_upload_incomplete Part of the run's output never reached Zenfra. Below, under Logs.
policy_failed A plan policy denied the plan; the run page reads Blocked by policy and nothing was applied. A failing plan hook is not a policy verdict: it fails the run with hook_failed, or hook_timeout when it ran too long, before any policy runs. Can this run still proceed?, Run Hooks
approval_gates_pending Approval was refused because the policy evaluation has not completed, did not complete, or is being redone. The run page and approval
Repository requires authentication A raw-git stack points at a private repository; raw Git supports public repositories only. Switch the source to a VCS integration. Source Control
cloud credentials failed: … The AWS role could not be assumed; the message names the identity the trust policy has to allow. AWS Integration
worker credential mode requires a private worker pool A worker-mode AWS integration was used on the Public Worker Pool. AWS Integration, Worker Pools
project root "…" does not exist in the repository, contains no … configuration file The stack's Path is wrong or holds no .tf file directly (.tofu counts on OpenTofu 1.8 and later). Source Control

The run page

The title line shows the stack, the run type and status, and the change count. The metadata row shows the branch, the trigger (Manual, Push, PR or Drift), the actor, the source commit, start time, duration and run ID. Re-run on a finished run starts a new run of the same type against the stack's current configuration.

Resource impact lists every resource the plan will create, update, replace or delete, derived from the plan JSON; it may appear a little after the plan finishes. The same panel is shown in the Approve & Apply dialog.

Logs

Run History is a timeline of the phases; each phase expands into its log panel, streamed live (LIVE), with Copy logs and Download logs.

Logs are complete or the run says so. The worker spools every byte of tool output to disk and uploads it independently, so a slow connection delays the upload but never blocks the apply. If any output was lost, the run is marked Failed with log_upload_incomplete even when the apply itself succeeded; check your cloud before re-running such a run. Runs planned by a worker older than September 2026 carry no such guarantee. For the worker-side spool settings, see Worker Pools.

A Preparing log looks like this (the backend lines appear only for Zenfra-managed state):

[Zenfra] Preparing workspace for run <run_id>
[Zenfra] Source commit: 3f2a9c1e5b7d4a0c8e6f1b2d3c4a5e6f7a8b9c0d
[Zenfra] Downloading source bundle (whole repository)...
[Zenfra] Source bundle downloaded (1.2 MiB, 824ms)
[Zenfra] Project root: infra/prod
[Zenfra] Downloading configuration bundle...
[Zenfra] Configuration bundle downloaded (0 B)
[Zenfra] Applied 3 environment variables
[Zenfra] Configuring managed state backend...
[Zenfra] Backend configuration complete
[Zenfra] Workspace preparation complete (860ms)