Workflow TOML Schema Reference
Complete reference for workflow definition files in Forge. For step and check fields, see Workflow Steps and Checks. For a guided example, see Creating Your First Workflow.
File location
| Source | Location |
|---|---|
| Project workflows | .forge/config/workflows/<name>.toml |
| Built-in workflows | Compiled into frg; copy them to the project with frg config export --workflows |
Commands refer to a workflow by its name. Keep the file name and name identical.
Structure
workflow_schema_version = "1.0.0"
[workflow]
name = "my-workflow"
description = "What this workflow does"
version = "1.0.0"
required_context = ["issue_id"]
[workflow.initial_state]
name = "first_state"
[workflow.terminal_states]
success = ["completed"]
failed = ["first_failed"]
[workflow.loop_detection] # optional
max_transitions = 150
on_loop_detected = "approval_gate"
[[workflow.states]]
name = "first_state"
command = { target = "issue", operation = "draft" }
on_success = "completed"
on_failure = "first_failed"
[[workflow.states]]
name = "completed"
[[workflow.states]]
name = "first_failed"
Top level
| Field | Type | Required | Description |
|---|---|---|---|
workflow_schema_version | String | Yes | Schema version. Supported: 1.0.x. Any other value fails with Unsupported workflow schema version. |
workflow | Table | Yes | The workflow definition. |
[workflow]
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Workflow identifier used by frg workflow commands. |
description | String | Yes | Shown by frg workflow list. |
version | String | Yes | Version of this definition, shown by frg workflow list --verbose. |
required_context | Array of strings | Yes | Context that must be set before the run starts: issue_id, pr_id, ticket_id. Use [] for none. |
initial_state | Table | Yes | See [workflow.initial_state]. |
terminal_states | Table | Yes | See [workflow.terminal_states]. |
loop_detection | Table | No | See [workflow.loop_detection]. |
states | Array of tables | Yes | See [[workflow.states]]. |
A missing required key fails with, for example, missing field description``.
context accepts only these values: issue, code, pr, security, ticket.
An unrecognized value makes the operation fail workflow validation. Each
recognized value (other than code/security, which are reserved for future
tooltip injection) appends an XML block like <issue_context><issue_id>...
after the prompt's project config, separate from the prompt text itself.
Agent Prompt Placeholders:
The prompt text itself may reference {{issue_id}}, {{pr_id}}, and
{{ticket_id}} directly — these are substituted with the resolved workflow
context values before the prompt is sent to the agent:
operation = { id = "notify", type = "agent", prompt = "Post a status update on issue {{issue_id}}." }
If a value isn't available in the current context (e.g. {{pr_id}} when no
PR is set), the placeholder is left as-is rather than silently removed. Only
these three IDs are supported — project metadata and toolchain placeholders
(e.g. {{issue_tracker_name}}, available to command-based prompts) are not.
Agent Step Traces:
Every agent step writes a trace file to <job_dir>/traces/ (named
{timestamp}_{step_id}.trace.md), containing the prompt sent to the agent and
its stdout/stderr/exit code — the same convention used by command-based
states, so frg --tail/--continue and frg diagnose work for agent steps
too.
Built-in Actions:
make_directory: Create directory (pathfield required)set_env: Set environment variable (env_name,env_valuerequired)copy_file: Copy file (source,destinationrequired)write_file: Write content to file (path,contentrequired)
Check Operations
Evaluate a condition and control workflow based on result:
File Exists:
operation = { id = "config-exists", type = "file_exists", path = ".forge/config.toml", on_false = "fail" }
Environment Equals:
operation = { id = "mode-check", type = "env_equals", env_name = "MODE", env_value = "production", on_false = "skip" }
Check Fields:
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | State where the run starts. Must exist in states. |
[workflow.terminal_states]
| Type | Description | Required Fields |
|---|---|---|
file_exists | File exists | path |
directory_exists | Directory exists | path |
path_exists | File or directory exists | path |
env_exists | Environment variable is set | env_name |
env_equals | Environment variable equals value | env_name, env_value |
file_contains | File contains string | path, content |
file_matches | File matches regex pattern | path, pattern |
command_exists | Command is available in PATH | command |
command_success | Shell command exits with 0 | command or auto_detect |
tasks_complete | All tasks in tasks.md are complete | path (optional) |
counter_matches | Loop counter matches expression | counter_name, counter_expression |
command_success with auto_detect
Instead of a literal command, a command_success check can set auto_detect to
"build" or "test" to run the project's own build/test command:
operation = { id = "project-builds", type = "command_success", auto_detect = "build", on_false = "fail" }
The command is read from .forge/config/project.toml's [project] table
(build_command / test_command) — it is not derived by scanning the
filesystem for build-system markers (no Cargo.toml/package.json detection).
If project.toml is absent or the field isn't set, the check is skipped,
not failed, so it's safe to leave in workflows shared across projects that
don't record a build/test command. A check may set exactly one of command or
auto_detect, and auto_detect is only valid on command_success checks.
# .forge/config/project.toml
[project]
build_command = "cargo build --all-targets"
test_command = "cargo test"
Check Behaviors:
The on_false field controls what happens when a check evaluates to false:
| Value | Behavior |
|---|---|
"fail" | State fails immediately, transitions to on_failure |
"skip" | Log warning, continue to next operation or transition to on_success |
Special Check Types:
tasks_complete Check
Verifies that all tasks in a tasks.md file are complete by scanning for unchecked task markers (- [ ]) in task sections only.
operation = {
id = "all-tasks-done",
type = "tasks_complete",
on_false = "fail"
}
With explicit path:
operation = {
id = "all-tasks-done",
type = "tasks_complete",
path = ".forge/work/job_123/code/tasks.md",
on_false = "fail"
}
Fields:
path(optional): Path to tasks.md file. If omitted, defaults to{job_dir}/code/tasks.md
Section-Aware Parsing:
The check uses intelligent section detection to avoid counting non-task checkboxes:
| Section Type | Detection | Checkbox Behavior |
|---|---|---|
| Task sections | Headers without exclusion keywords | Counted |
| Non-task sections | Headers containing "acceptance", "criteria", "summary", "requirement", "validation", etc. | Ignored |
| Code blocks | Content between ``` or ~~~ | Ignored |
Example:
# Implementation Tasks
- [ ] Create config file ← Detected as incomplete
- [x] Update documentation ← Ignored (complete)
## Acceptance Criteria
- [ ] Feature works as expected ← Ignored (non-task section)
## Example Code
~~~~
- Example checkbox ← Ignored (code block)
~~~~
**Behavior**:
- Check **passes** when no unchecked tasks remain in task sections
- Check **fails** when any unchecked task marker (`- [ ]`) exists in a task section
- Works with both lite plans (no task identifiers) and full plans (with `TASK-`/`TSK-` identifiers)
**Common Pattern - Loop Until Complete**:
```toml
[[workflow.states]]
name = "create_code"
command = { target = "code", operation = "create" }
on_success = "check_tasks"
[[workflow.states]]
name = "check_tasks"
operation = { id = "tasks-done", type = "tasks_complete", on_false = "fail" }
on_success = "code_review" # All done, exit loop
on_failure = "create_code" # Tasks remain, continue loop
counter_matches Check
Evaluates arithmetic expressions on loop counters for conditional logic.
operation = {
id = "every-third",
type = "counter_matches",
counter_name = "code_iterations",
counter_expression = "% 3 == 0",
on_false = "skip"
}
Fields:
counter_name: Name of loop counter to evaluatecounter_expression: Arithmetic expression (e.g.,"% 3 == 0","> 5","== 10")
Supported Operators:
- Arithmetic:
+,-,*,/,%(modulo) - Comparison:
==,!=,<,>,<=,>=
Behavior:
- Evaluates expression against current counter value
- Check passes (success) when expression is true
- Check fails when expression is false
Common Pattern - Periodic Actions:
# Clear context every 3rd iteration
[[workflow.states]]
name = "check_counter"
operation = {
id = "mod-3",
type = "counter_matches",
counter_name = "iterations",
counter_expression = "% 3 == 0",
on_false = "skip"
}
on_success = "create_code_clear" # Use --clear flag
on_failure = "create_code" # Regular operation
Command Operations
Execute a Forge CLI command within the workflow. A command operation is identified solely by the
presence of the target field — there is no type field on command operations, and adding one
causes a parse error:
operation = { target = "issue", operation = "draft", modifiers = ["lite"] }
With arguments:
operation = { target = "code", operation = "review", args = { model = "gpt-4" } }
Command Fields:
| Field | Type | Required | Description |
|---|---|---|---|
target | String | Yes | Command target (e.g., "issue", "code", "pr") |
operation | String | Yes | Command operation (e.g., "draft", "review") |
modifiers | Array | No | List of modifiers to apply |
args | Object | No | Argument overrides |
[workflow.loop_detection]
| Field | Type | Default | Description |
|---|---|---|---|
max_state_visits | Integer | 3 | Visit limit for states without max_visits. |
max_transitions | Integer | 100 | Limit on total transitions in one run. The built-in iterative workflows use 100 or 150. |
on_loop_detected | String | "approval_gate" | "approval_gate": pause for approve or reject. "fail": fail immediately. |
[[workflow.states]]
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Unique state name. Used by transitions, --start-from, and --resume-at. |
description | String | No | Shown by --list-states, frg workflow graph, and progress output. |
is_entry_point | Boolean | No | Recommends this state as a safe --start-from target. Defaults to false; it does not restrict which existing states the CLI accepts. |
command | Inline table | One of four | Run a Forge command. See Command. |
operation | Inline table | One of four | Run one step or check. See Operation types. |
operations | Array of inline tables | One of four | Run several steps and checks in order. |
approval | Table | One of four | Pause at an approval gate. See Approval. |
on_success | String | For non-terminal states | Next state when the state succeeds or the gate is approved. |
on_failure | String | For non-terminal states | Next state when the state fails or the gate is rejected. |
loop_enabled | Boolean | No | Marks a loop state. Requires at least one escape condition. |
loop_counter | String | With loop_enabled | Counter name. counter_matches checks read it through counter_name. |
escape_conditions | Array of tables | With loop_enabled | See Escape conditions. |
max_visits | Integer | No | Per-state visit limit that overrides max_state_visits. Must be greater than 0. |
retry | Table | No | See Retry. |
Every non-terminal state must set exactly one of command, operation, operations, or approval.
is_entry_point = true marks a state that the workflow author recommends as a starting point. frg workflow run --list-states labels it [recommended start]; the CLI still accepts any existing state with --start-from. Forge Desktop offers the initial state and states marked as entry points. Before starting at a later state, verify that its prerequisites and required context are already satisfied.
Command
command = { target = "code", operation = "review", modifiers = ["auto-fix", "issues"] }
| Field | Type | Required | Description |
|---|---|---|---|
target | String | Yes | Command group, for example issue, code, pr, ticket. |
operation | String | Yes | Command within the group, for example draft, plan, create, review, merge, triage. |
modifiers | Array of strings | No | Command flags without --; a flag's value follows as the next element. Built-in workflows use retry, clear, lite, and auto-fix with issues. |
args | Table | No | Argument overrides. |
No other keys are accepted. The validator reports unknown field ..., expected one of target, operation, modifiers, args.
Operation types
An operation is a command, a step, or a check. Forge infers the kind from its fields; there is no kind field.
| Operation is a… | When | Fields |
|---|---|---|
| Command | It has target | target, operation, modifiers, args |
| Step | type is shell, builtin, or agent | Step fields |
| Check | type is a check type | Check fields |
Check types: file_exists, directory_exists, path_exists, file_contains, file_matches, env_exists, env_equals, command_exists, command_success, tasks_complete, counter_matches.
Write the concrete type directly. type = "step", type = "check", step_type, and check_type are rejected:
Operation 'x' declares type = "check", but "check" is not an operation type value.
Use the concrete type directly, without a `step_type`/`check_type` field:
{ id = "x", type = "agent", prompt = "..." } # agent step
{ id = "x", type = "shell", command = "..." } # shell step
{ id = "x", type = "file_exists", path = "..." } # check
Operations in operations run in definition order.
Approval
[workflow.states.approval]
gate_id = "plan_approval"
message = "Implementation plan is ready. Review & edit before approving for code generation"
timeout_seconds = 86400
| Field | Type | Required | Description |
|---|---|---|---|
gate_id | String | Yes | Gate identifier, recorded in approval history and approval transaction events. |
message | String | Yes | Instructions shown to the approver. |
required_code | String | No | Code the approver must pass with --code (case-sensitive). |
timeout_seconds | Integer | No | Time to wait for a decision. All built-in gates use 86400. |
on_timeout | String | No | State to go to when the gate times out. |
Approving follows on_success; rejecting follows on_failure. --resume-at overrides either.
Escape conditions
[[workflow.states.escape_conditions]]
counter_value = 15
action = "approval"
approval_gate = "await_iteration_approval"
| Field | Type | Required | Description |
|---|---|---|---|
counter_value | Integer | Yes | Counter value at which the condition applies. |
action | String | Yes | continue, transition, approval, or abort. |
target_state | String | For transition | State to transition to. |
approval_gate | String | For approval | Name of the approval state to transition to. |
error_message | String | For abort | Error the workflow ends with. |
message | String | No | Message to display. |
| Action | Effect | Missing field error |
|---|---|---|
continue | Keep looping | — |
transition | Go to target_state | Escape condition with action 'transition' requires 'target_state' |
approval | Go to the approval state named by approval_gate | Escape condition with action 'approval' requires 'approval_gate' |
abort | End the workflow (Workflow aborted by escape condition) | Escape condition with action 'abort' requires 'error_message' |
Retry
[workflow.states.retry]
max_retries = 3
retry_delay = "30s"
timeout = "5m"
[[workflow.states.retry.retry_conditions]]
retry_count = 2
action = "approval"
approval_gate = "await_retry_approval"
| Field | Type | Description |
|---|---|---|
max_retries | Integer | Maximum retry attempts. Very high values produce a validation warning. |
retry_delay | String | Delay between attempts, for example "30s". |
timeout | String | Per-attempt timeout, for example "5m". |
retry_conditions | Array of tables | Behaviour at specific retry counts. |
retry_conditions field | Type | Description |
|---|---|---|
retry_count | Integer | Retry count at which the condition applies. |
action | String | continue, transition, or approval. |
timeout | String | Replacement per-attempt timeout. |
target_state | String | For transition. |
approval_gate | String | For approval. |
message | String | Message to display. |
Counter expressions
counter_matches checks evaluate counter_expression against the counter named in counter_name:
| Form | Example |
|---|---|
{{COUNTER}} <operator> N | {{COUNTER}} >= 5 |
{{COUNTER}} % N == M | {{COUNTER}} % 3 == 0 |
{{COUNTER}} % N != M | {{COUNTER}} % 3 != 0 |
An expression without {{COUNTER}} passes frg workflow validate but fails at run time with Expected format: '{{COUNTER}} <operator> N'.
Validation rules
frg workflow validate <name> and workflow loading apply these rules.
Errors (the workflow cannot run):
| Message | Fix |
|---|---|
missing field required_context, description, version) | Add the key under [workflow]. Use required_context = [] for none. |
Unsupported workflow schema version '<v>'. Supported: ["1.0.x"] | Set workflow_schema_version = "1.0.0". |
Initial state does not exist in states list | Fix [workflow.initial_state] name. |
Invalid transition from '<state>' to '<target>': Target state does not exist | Fix the on_success or on_failure target. |
State '<name>' has no operation defined. Non-terminal states must have command, operation, operations, or approval | Give the state work, or list it in [workflow.terminal_states]. |
No terminal state is reachable from initial state | Add a transition path to a terminal state. |
Loop state must have at least one escape condition | Add [[workflow.states.escape_conditions]]. |
Escape condition with action '<action>' requires '<field>' | Add the required field. |
State '<name>' has max_visits=0, which would prevent any execution | Set max_visits to 1 or more. |
Operation '<id>' has unknown check type '<type>' | Use a listed check type. |
unknown field , expected one of ... | Remove or rename the key in the step, check, or command. |
unknown variant , expected ... | Use an allowed value, for example for on_loop_detected, action, or required_context. |
Warnings (the workflow can run):
| Message | Meaning |
|---|---|
State '<name>' is unreachable from initial state '<initial>' | No on_success or on_failure path leads to the state. States entered only through escape conditions also show this warning. |
Loop state '<name>' has no escape that guarantees termination. Consider adding an 'abort' escape at high counter value. | The loop has no abort escape. |
Escape conditions in state '<name>' are not in ascending order. Counter value <n> appears after <m>. | List escape conditions in ascending counter_value order. |
Keys the schema does not define are ignored at the [workflow], state, approval, escape-condition, loop-detection, and retry levels, but rejected inside steps, checks, and commands. Check spelling carefully at the ignoring levels.