Workflow Steps and Checks
Reference for the steps and checks that workflow states run through operation and operations. Steps do work: shell commands, built-in file and environment actions, and AI agent prompts. Checks test a condition and pass or fail.
Looking for command hooks? To run steps and checks before or after an individual command, such as
frg issue draft, configure hooks incommands.toml. See Command Hooks.
Using operations in a state
# One operation
[[workflow.states]]
name = "verify_pr_context"
operation = { id = "check-pr-context", type = "file_matches", path = ".forge/context.toml", pattern = "(?m)^\\s*pr_id\\s*=", on_false = "fail" }
on_success = "pr_draft"
on_failure = "retry_set_pr_context"
# Several operations, run in definition order
[[workflow.states]]
name = "prepare"
operations = [
{ id = "out-dir", type = "builtin", action = "make_directory", path = "output" },
{ id = "mode", type = "builtin", action = "set_env", env_name = "BUILD_MODE", env_value = "release" },
{ id = "has-node", type = "command_exists", command = "node", on_false = "fail" },
]
on_success = "build"
on_failure = "prepare_failed"
Built-in workflows such as issue-to-pr-lite and quick-fix-to-pr verify that create_pr saved a PR ID to .forge/context.toml before continuing to later PR steps. If verification fails, the workflow runs retry_set_pr_context once and checks again. If the retry does not save a valid ID, the workflow pauses at await_pr_intervention for manual action.
Forge infers whether an operation is a step or a check from type. Do not add kind, step_type, or check_type. See Operation types.
Step types
type | Does | Required fields |
|---|---|---|
shell | Runs a shell command | command, or a platform variant |
builtin | Runs a cross-platform built-in action | action plus the action's fields |
agent | Sends a prompt to the configured AI coding agent | prompt |
Step fields
Steps accept exactly these fields. Any other key fails with unknown field ..., expected one of ....
| Field | Type | Applies to | Description |
|---|---|---|---|
id | String | All | Operation identifier, used in logs and dependencies. |
description | String | All | Human-readable description. |
type | String | All | shell, builtin, or agent. |
command | String | shell | Command to run on every platform. |
command_linux | String | shell | Command to run on Linux. |
command_macos | String | shell | Command to run on macOS. |
command_windows | String | shell | Command to run on Windows. |
action | String | builtin | Built-in action name. See Built-in actions. |
path | String | builtin | Target file or directory. |
source | String | builtin | Source file for copy_file. |
destination | String | builtin | Destination file for copy_file. |
content | String | builtin | Content for write_file and append_file. |
env_name | String | builtin | Variable name for set_env. |
env_value | String | builtin | Variable value for set_env. |
prompt | String | agent | Prompt text. Multi-line strings (""") are allowed. |
context | Array of strings | agent | Forge context to inject: issue, code, pr, security, ticket. |
share_session_with | String | agent | Name of an earlier state whose agent session to reuse. |
depends_on | See Dependencies | All | Steps that must complete first. |
requires_check | See Dependencies | All | Check that must pass for this step to run. |
timeout_seconds | Integer | All | Timeout for each attempt. |
retry_count | Integer | All | Number of retries after a failure. |
retry_delay_seconds | Integer | All | Delay between retries. |
Built-in actions
action | Effect | Required fields |
|---|---|---|
make_directory | Create a directory, including parents | path |
make_file | Create an empty file | path |
write_file | Write content to a file | path, content |
append_file | Append content to a file | path, content |
copy_file | Copy a file | source, destination |
remove_file | Delete a file | path |
remove_directory | Delete a directory | path |
set_env | Set an environment variable for later steps and the agent | env_name, env_value |
frg workflow validate does not check an action's required fields. A missing field fails at run time, for example action 'set_env' requires env_name.
Agent steps
operation = { id = "send-initial", type = "agent", prompt = """
Send the initial response to the support agent through the ticketing system.
Locate and read initial_response.md in the ticket work directory, then send it.
""", context = ["ticket"] }
- An unknown
contextvalue fails validation:unknown agent step context 'bogus'. Valid values: issue, code, pr, security, ticket. share_session_withcontinues the named state's agent session. If Forge finds no session for that state, it starts a fresh one (No session found for state, starting fresh session).--list-statesshows agent-step states as typeoperation;frg workflow graphdraws them as steps.
Shell steps and security
Shell commands run with your user's permissions and see the workflow's environment variables. An environment variable containing shell metacharacters can inject commands.
- Prefer built-in actions for file-system work.
- Validate or avoid untrusted values in
command. exportandcdaffect only that shell process. Useset_envto pass values to later operations.
Check types
type | Passes when | Required fields |
|---|---|---|
file_exists | The file exists | path |
directory_exists | The directory exists | path |
path_exists | A file or directory exists at path | path |
file_contains | The file contains content | path, content |
file_matches | The file matches the regular expression pattern | path, pattern |
env_exists | The environment variable is set | env_name |
env_equals | The environment variable equals env_value | env_name, env_value |
command_exists | The command is available on PATH | command |
command_success | The shell command exits with code 0 | command |
tasks_complete | No unchecked tasks (- [ ]) remain in the task list | path (optional) |
counter_matches | The loop counter matches the expression | counter_name, counter_expression |
Check fields
Checks accept exactly these fields.
| Field | Type | Description |
|---|---|---|
id | String | Operation identifier. |
description | String | Human-readable description. |
type | String | Check type. |
path | String | File or directory to test. |
content | String | Substring for file_contains. |
pattern | String | Regular expression for file_matches. |
env_name | String | Variable name for env_exists and env_equals. |
env_value | String | Expected value for env_equals. |
command | String | Command name (command_exists) or shell command (command_success). |
counter_name | String | Loop counter for counter_matches. |
counter_expression | String | Expression for counter_matches. See Counter expressions. |
on_false | String | "fail" or "skip". See Check results. |
timeout_seconds | Integer | Timeout for each attempt. |
retry_count | Integer | Number of retries before the check counts as false. |
retry_delay_seconds | Integer | Delay between retries. |
Checks have no depends_on or requires_check, and steps have no on_false.
Check results
on_false | When the check is false | Log message |
|---|---|---|
"fail" | The state fails and follows on_failure | Check failed with on_false='fail', halting workflow |
"skip" | Logged; execution continues with the next operation | Check failed with on_false='skip', continuing execution |
A single-check state with on_false = "fail" acts as a router: true follows on_success, false follows on_failure.
tasks_complete
Scans a Markdown task list and passes when no unchecked task (- [ ]) remains in a task section. Without path, it reads code/tasks.md in the current job directory, the task list that frg issue plan produces. Checkboxes are ignored in:
- Sections whose heading marks them as non-task content, for example acceptance criteria, summary, requirements, validation, test data, reference, background, or context.
- Fenced code blocks.
Dependencies
depends_on names steps that must complete before this step runs. requires_check names a check that must pass for this step to run. Both accept a single ID (depends_on = "create-dir") or an array of IDs (depends_on = ["create-dir"]).
operations = [
{ id = "has-template", type = "file_exists", path = ".forge/templates/docs.md", on_false = "skip" },
{ id = "load-custom", type = "builtin", action = "copy_file", source = ".forge/templates/docs.md", destination = "templates/current.md", requires_check = "has-template" },
{ id = "load-default", type = "builtin", action = "write_file", path = "templates/current.md", content = "# Default template", depends_on = "failed(load-custom)" },
]
failed(<id>) makes a step depend on another step or check having failed.
frg workflow validate does not resolve dependency IDs. An unknown ID fails at run time with depends_on references unknown step '<id>' or requires_check references unknown check '<id>'.
Environment persistence
Variables set with the set_env action are visible to later operations in the same state, to operations in later states, and to the AI agent:
[[workflow.states]]
name = "configure"
operation = { id = "set-id", type = "builtin", action = "set_env", env_name = "RELEASE_TAG", env_value = "v2.3.0" }
on_success = "tag"
on_failure = "configure_failed"
[[workflow.states]]
name = "tag"
operation = { id = "create-tag", type = "shell", command = "git tag \"$RELEASE_TAG\"", command_windows = "git tag %RELEASE_TAG%" }
on_success = "completed"
on_failure = "tag_failed"
If USER_INPUT contains ; rm -rf / or similar commands, they will be executed.
Best Practices:
- Prefer built-in operations for file system tasks - they're safer and cross-platform
- Validate environment variables before using them in shell commands
- Use absolute paths when possible to avoid PATH injection
- Quote variables carefully though this doesn't fully protect against injection
- Minimize shell step usage - only use when necessary
For security-sensitive operations, always prefer built-in operations over shell commands.
Built-in Steps
Cross-platform file operations that work consistently everywhere.
[[workflows.operations.pre]]
kind = "step"
id = "create-output"
type = "builtin"
action = "make_directory"
path = "output/reports"
Available actions:
| Action | Description | Required Fields |
|---|---|---|
make_directory | Create directory with parents | path |
make_file | Create empty file | path |
remove_file | Delete file | path |
remove_directory | Delete directory recursively | path |
copy_file | Copy file | source, destination |
write_file | Write content to file | path, content |
append_file | Append content to file | path, content |
set_env | Set environment variable (†) | env_name, env_value |
† Note:
set_envsets environment variables in the workflow's execution context. These variables are available to subsequent steps, the agent execution, and post-steps. This is implemented via the step context, not as a file system operation.
Examples:
# Copy a file
[[workflows.operations.pre]]
kind = "step"
id = "backup-config"
type = "builtin"
action = "copy_file"
source = "config.toml"
destination = "config.toml.bak"
# Write content to a file
[[workflows.operations.pre]]
kind = "step"
id = "create-readme"
type = "builtin"
action = "write_file"
path = "output/README.md"
content = "# Generated Output\n\nThis directory contains generated files."
# Set an environment variable
[[workflows.operations.pre]]
kind = "step"
id = "set-api-key"
type = "builtin"
action = "set_env"
env_name = "API_KEY"
env_value = "secret-value"
Agent Steps (Advanced)
Execute an AI agent prompt as part of the workflow.
[[workflows.operations.pre]]
kind = "step"
id = "analyze-context"
type = "agent"
prompt = "Analyze the provided context and summarize key requirements."
The prompt may reference {{issue_id}}, {{pr_id}}, and {{ticket_id}} —
these are substituted with the resolved workflow context before the prompt is
sent to the agent. A placeholder with no value in context (e.g. {{pr_id}}
with no PR set) is left unsubstituted:
[[workflows.operations.pre]]
kind = "step"
id = "summarize-issue"
type = "agent"
prompt = "Summarize issue {{issue_id}} for the team."
context = ["issue"]
context additionally injects a separate <issue_context>/<pr_context>/
<ticket_context> XML block before the prompt (see Workflow Schema Reference for the full field list). Every agent step
writes a trace file to <job_dir>/traces/ with the prompt and the agent's
output, the same as command-based states.
Check Types
Checks probe environment state without modifying it.
| Check Type | Description | Fields |
|---|---|---|
file_exists | File exists | path |
directory_exists | Directory exists | path |
path_exists | Path (file or dir) exists | path |
file_contains | File contains substring | path, content |
file_matches | File matches regex | path, pattern |
env_exists | Env var is set | env_name |
env_equals | Env var equals value | env_name, env_value |
command_exists | Command in PATH | path (command name) |
Check Behavior
on_false = "fail"(default): Stop workflow with erroron_false = "skip": Skip remaining pre-items, proceed to command
[[workflows.operations.pre]]
kind = "check"
id = "has-config"
type = "file_exists"
path = ".forge/config.toml"
on_false = "skip" # Missing config is OK, use defaults
Step Dependencies
Steps can depend on other steps or require checks to pass:
[[workflows.operations.pre]]
kind = "step"
id = "step-a"
type = "builtin"
action = "make_directory"
path = "output"
[[workflows.operations.pre]]
kind = "step"
id = "step-b"
type = "shell"
command = "echo 'setup complete' > output/status.txt"
depends_on = "step-a" # Waits for step-a to complete
[[workflows.operations.pre]]
kind = "step"
id = "step-c"
type = "shell"
command = "process.sh"
requires_check = "has-tool" # Only runs if check passed
Execution Order
For Workflow-Executed Commands
When a workflow definition executes commands (e.g., multi-step feature workflow):
- Workflow Pre-items - Workflow-level setup from definition file
- Command Pre-hooks - Command-specific setup from commands.toml (see Command Hooks)
- Command Execution - The actual agent execution
- Command Post-hooks - Command-specific cleanup from commands.toml
- Workflow Post-items - Workflow-level cleanup from definition file
All items execute in definition order. Steps and checks can be interleaved, and all execute in the same thread/process so environment variables and working directory changes persist throughout the workflow.
Environment Persistence
Environment modifications in pre-steps are visible to subsequent steps and the agent:
[[workflows.operations.pre]]
kind = "step"
id = "set-env"
type = "builtin"
action = "set_env"
env_name = "MY_VAR"
env_value = "value"
# MY_VAR is now available to subsequent steps and agent
Note: Shell commands that set environment variables using export only
affect that specific shell invocation. Use the set_env built-in action for
cross-step persistence.
Error Handling
- Step failure halts the workflow immediately
- Check failure with
on_false="fail"halts the workflow - Check failure with
on_false="skip"skips remaining pre-items and continues to command - Post-step/check failure is logged but doesn't fail the workflow
Examples
The following examples demonstrate workflow-level orchestration. For command-specific setup and validation examples, see Command Hooks.
Example 1: Multi-Command Feature Workflow
Orchestrate multiple commands with workflow-level git operations:
schema_version = "2.1.0"
[metadata]
name = "Feature Development"
version = "1.0.0"
# Workflow-level setup
[[pre]]
kind = "step"
id = "create-branch"
type = "shell"
command = "git checkout -b feature/$FEATURE_NAME"
[[pre]]
kind = "check"
id = "branch-created"
type = "shell"
command = "git branch --show-current | grep feature/"
on_false = "fail"
# Execute commands
[[steps]]
command = "issue draft"
[[steps]]
command = "code create"
[[steps]]
command = "code review"
# Workflow-level cleanup
[[post]]
kind = "step"
id = "push-branch"
type = "shell"
command = "git push -u origin feature/$FEATURE_NAME"
[[post]]
kind = "step"
id = "create-pr"
type = "shell"
command = "gh pr create --fill"
Example 2: Conditional Workflow Based on Environment
Different workflow paths based on CI vs local development:
schema_version = "2.1.0"
[metadata]
name = "PR Workflow"
version = "1.0.0"
# Check environment
[[pre]]
kind = "check"
id = "is-ci"
type = "env_equals"
env_name = "CI"
env_value = "true"
on_false = "skip"
# CI-specific setup
[[pre]]
kind = "step"
id = "ci-setup"
type = "shell"
command = "npm ci && npm run lint"
requires_check = "is-ci"
# Local-specific setup
[[pre]]
kind = "step"
id = "local-setup"
type = "shell"
command = "npm install"
depends_on = "failed(is-ci)"
[[steps]]
command = "pr review"
[[steps]]
command = "pr merge"
# CI-specific notifications
[[post]]
kind = "step"
id = "notify-team"
type = "shell"
command = "slack-notify 'PR merged' #team-channel"
requires_check = "is-ci"
Example 3: Workflow with Fallback Logic
Handle missing resources with fallback steps:
schema_version = "2.1.0"
[metadata]
name = "Documentation Update"
version = "1.0.0"
# Try to load custom template
[[pre]]
kind = "check"
id = "has-custom-template"
type = "file_exists"
path = ".forge/templates/docs.md"
on_false = "skip"
# Use custom template if available
[[pre]]
kind = "step"
id = "load-custom"
type = "builtin"
action = "copy_file"
source = ".forge/templates/docs.md"
destination = "templates/current.md"
requires_check = "has-custom-template"
# Fallback to default template
[[pre]]
kind = "step"
id = "load-default"
type = "builtin"
action = "write_file"
path = "templates/current.md"
content = "# Default Documentation Template"
depends_on = "failed(load-custom)"
[[steps]]
command = "docs update"
[[post]]
kind = "step"
id = "cleanup-template"
type = "builtin"
action = "remove_file"
path = "templates/current.md"
For more examples including command-level validation and setup, see Command Hooks.
Debugging
Each step writes a step_execution event, and each check writes a check_evaluation event, to .forge/work/<job_id>/logs/transaction.jsonl:
jq 'select(.event_type == "step_execution") | {step_id, step_type, phase, outcome, duration_ms}' \
.forge/work/<job_id>/logs/transaction.jsonl