New to FORGE? Start a free 30-day trial

Workflow Execution Model

Updated on Oct 5, 2026

This page explains what Forge does between frg workflow run and the final state: how a run starts and ends, how each state executes, how locks and approvals behave, and what Forge persists. Read it to design reliable workflows and to understand what you see in frg workflow status and the transaction log.

Two levels of execution

A workflow run has two nested levels:

  1. The workflow engine walks the state machine: it picks the current state, executes it, and follows a transition.
  2. The command lifecycle runs inside every command state, and whenever you run a command like frg code create directly. It wraps the AI agent call with validation and hooks.

Workflow run lifecycle

text
frg workflow run <name>
│
├─ Load and validate the definition same checks as `frg workflow validate`
├─ Check required_context issue_id, pr_id, ticket_id
├─ Acquire .forge/.workflow.lock one workflow per repository
├─ Mark orphaned approvals as stale approvals left by an earlier run
│
├─ Repeat from initial_state (or --start-from):
│ ├─ Loop detection state visits and total transitions
│ ├─ Escape conditions (loop states) continue | transition | approval | abort
│ ├─ Execute the state
│ │ command → command lifecycle (below)
│ │ operation → steps and checks, in order
│ │ approval → wait for approve, reject, or timeout
│ ├─ Record the transition .forge/work/<job>/logs/transaction.jsonl
│ └─ Follow on_success / on_failure or --resume-at from an approval
│
├─ Stop at a terminal state success | failed | cancelled
├─ Mark pending approvals as stale
└─ Release the lock

Command lifecycle

Every Forge command executes in the same phases, whether you run it directly or a workflow state runs it:

text
┌─ Input validation (built-in) required context, context files and directories
├─ Pre-checks and pre-steps your pre hooks from commands.toml, in definition order
├─ Agent operation the AI coding agent runs the compiled prompt
├─ Output validation (built-in) expected output files exist
│ └─ Auto-retry (once) only with --auto-retry, only for missing outputs
└─ Post-steps and post-checks your post hooks from commands.toml

Pre and post items are configured per command in .forge/config/commands.toml. See Command Hooks. Operation states in a workflow file run their steps and checks directly, without an agent call.

How failures propagate

PhaseOn failureEvidence (log message)
Input validationThe command stops before the agent runsPre-flight check failed:
Check with on_false = "fail"The command or state failsCheck failed with on_false='fail', halting workflow
Check with on_false = "skip"Logged; execution continues with the next itemCheck failed with on_false='skip', continuing execution
Pre-stepThe command failsPre-item execution failed:
Agent operationThe command fails; the workflow follows on_failureAgent step failed
Output validationThe command fails, or retries once with --auto-retryOutput validation failed:, Auto-retry condition met, invoking retry
Post-itemLogged; does not fail the commandPost-item failed (non-fatal after successful agent), Workflow post-item failed, continuing with workflow

In a workflow, a failed command state is not the end of the run. The engine follows on_failure, which the built-in workflows use to route to retry states, intervention gates, or failure terminals.

Environment and processes

Steps in a run share one execution context. A set_env built-in action makes a variable available to later steps and to the agent. Each shell step runs in its own process, so export or cd inside a shell command does not carry over to the next step. Loop counters are also exposed to operations as environment variables.

Workflow lock

Only one workflow can run in a repository at a time. When a run starts, Forge writes .forge/.workflow.lock, which records the owning process ID and host name.

  • If another live process holds the lock, the new run is refused with Workflow lock held by PID <pid>.
  • If the process that held the lock no longer exists, Forge overrides the stale lock and continues (Overriding stale workflow lock).
  • The lock is released when the run ends.

If a run was killed and the lock remains, release it:

bash
frg workflow unlock # "No workflow lock found in this repository." if none exists
frg workflow unlock --force # force unlock even if lock appears valid

To run workflows concurrently, use Git worktrees or separate repository folders.

Approvals

An approval gate creates a pending approval and waits. Each approval ends in one of these states:

StateMeaning
PendingWaiting for frg workflow approve or frg workflow reject
ApprovedApproved; records approver_email, approval_code, and an optional resume_at
RejectedRejected; records rejector_email and an optional reason
Timed Outtimeout_seconds elapsed without a decision
BypassedSkipped via --bypass-approvals
StaleOrphaned from a previous workflow run

Forge marks approvals as stale in two situations: approvals orphaned by an earlier run are marked when a new run starts, and approvals still pending when a run ends are marked when it ends. frg workflow approve --list-all shows the full history with staleness detection, so a leftover code from yesterday's run cannot approve today's gate.

resume_at

By default, approving follows the gate's on_success and rejecting follows its on_failure. --resume-at <STATE_NAME> on approve or reject overrides that target for one decision, and Forge logs Using resume_at override instead of on_success. The target must be a state defined in the workflow.

Loop-detection approval gate

When a state exceeds its visit limit (state visit limit exceeded) or the run exceeds max_transitions (transition limit exceeded), and on_loop_detected = "approval_gate", Forge logs Loop detection triggered, requesting approval and creates a dynamic approval that shows the state, visit count, and transition count.

  • Approve: Reset counters and continue from current state (Loop detection counters reset).
  • Reject: Terminate workflow gracefully (Loop detection approval rejected for state '<name>').

Approval gates handle loop recovery reliably. With on_loop_detected = "fail", the run fails immediately instead.

State persistence

Each run works in a job directory under .forge/work/, for example .forge/work/job_20260713_120000_5678/. It holds the work artifacts that commands produce and the transaction log, logs/transaction.jsonl. The log records every state transition, step, check, and approval as JSON lines. See Transaction Log Format.

The engine keeps the current state, loop counters, and retry counters with the job. A new frg workflow run starts at initial_state. To continue an interrupted run, pass --start-from <STATE>, and use --list-states to find valid names.

frg workflow status resolves which job to report on in this order: explicit context (issue, PR, or ticket), the current directory if it is inside a job, then the most recent job. Override it with --work-dir. The status output includes the job's work artifacts as a summary; --verbose lists each artifact with its file size and timestamp.

Execution identity

Every execution has an identity: the workflow run, each command it invokes, and each step. When events are uploaded to OpenSearch or OTEL, each document carries:

  • instance_id: a globally unique <type>-<uuid> for the emitting execution, for example workflow-3f2a9c81-....
  • callers: the ancestor instance IDs, root first, so you can reconstruct which command started which workflow and which step.

These fields are attached at upload time, not written to the local transaction.jsonl. See Execution Identity Fields.

Debugging a run

bash
# Step and check results for a job
jq 'select(.event_type == "step_execution" or .event_type == "check_evaluation")' \
 .forge/work/<job_id>/logs/transaction.jsonl

# State transitions only
jq 'select(.event_type == "workflow_state_transition") | {from_state, to_state, outcome}' \
 .forge/work/<job_id>/logs/transaction.jsonl

# Verbose engine logging
RUST_LOG=debug frg workflow run my-workflow

See Also