Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Workflow TOML Schema Reference

This document provides a complete reference for the workflow TOML schema used to define stateful workflows in nia.

File Location

Workflow files are located in .nia/config/workflows/ with the .toml extension. Each file defines one workflow.

Schema Version

All workflow files must specify a schema version:

workflow_schema_version = "1.0.0"

Currently supported versions:

  • 1.0.x - Initial release (current)

Root Structure

workflow_schema_version = "1.0.0"

[workflow]
name = "my-workflow"
description = "Description of what this workflow does"
version = "1.0.0"

[workflow.initial_state]
name = "first_state"

[[workflow.states]]
# State definitions...

Workflow Metadata

[workflow] Section

FieldTypeRequiredDescription
nameStringYesUnique workflow identifier (used in CLI)
descriptionStringYesHuman-readable description
versionStringYesSemantic version (e.g., “1.0.0”)

[workflow.initial_state] Section

FieldTypeRequiredDescription
nameStringYesName of the starting state

Loop Detection Configuration

Workflows are protected against infinite loops with configurable thresholds. By default, nia aborts workflows that exceed reasonable iteration limits, but you can adjust these for workflows with legitimate repetitive patterns (like iterative code generation).

Global Loop Detection

Configure loop detection at the workflow level:

[workflow.loop_detection]
max_state_visits = 10        # Allow each state to be visited up to 10 times
max_transitions = 200        # Allow up to 200 total state transitions
on_loop_detected = "approval_gate"  # Create approval gate on loop (default)

Fields:

FieldTypeDefaultDescription
max_state_visitsNumber3Maximum times a single state can be visited
max_transitionsNumber100Maximum total state transitions in workflow
on_loop_detectedString“approval_gate”Action when loop detected: “approval_gate” or “fail”

Loop Detection Actions:

ValueBehavior
"approval_gate" (default)Pause workflow, create approval gate for user decision. Counters reset on approval.
"fail"Immediately fail workflow with error.

Approval Gate Behavior: When loop detection triggers with "approval_gate":

  1. Workflow pauses at the current state
  2. Dynamic approval gate created with detailed message showing:
    • State name that triggered detection
    • Current visit count and configured limit
    • Total transition count and limit
    • Available options (approve/reject)
  3. User can:
    • Approve: Resets visit counter to 0, workflow continues from current state
    • Reject: Terminates workflow gracefully with recovery hints

Note: The transition counter is NOT reset on approval, serving as a safety net against infinite loops.

Per-State Overrides

Individual states can override the global max_state_visits threshold:

[[workflow.states]]
name = "create_code"
max_visits = 15              # Allow this state to be visited 15 times
command = { target = "code", operation = "create" }
on_success = "check_tasks"

When to Use:

  • Iterative states: States like create_code that legitimately loop many times
  • Retry states: States with built-in retry logic that may execute repeatedly
  • Check states: Validation states that are revisited frequently in loops

Default Behavior:

  • States without max_visits use the workflow-level max_state_visits
  • If no workflow-level config exists, defaults to max_state_visits = 3

Loop Counter Environment Variables

Loop counters are automatically exposed as environment variables for use in shell scripts and commands:

# If loop_counter = "code_iterations", the following env var is available:
echo $NIA_LOOP_COUNTER_CODE_ITERATIONS

Format: NIA_LOOP_COUNTER_{COUNTER_NAME} (uppercase, underscores)

Usage Example:

[[workflow.states]]
name = "create_code"
loop_enabled = true
loop_counter = "iterations"
command = { target = "code", operation = "create" }

# In a subsequent shell step:
# $NIA_LOOP_COUNTER_ITERATIONS will contain the current count

Complete Example

[workflow]
name = "iterative-workflow"

[workflow.loop_detection]
max_state_visits = 5         # Global default
max_transitions = 150
on_loop_detected = "approval_gate"

[[workflow.states]]
name = "create_code"
max_visits = 12              # Override for this state only
loop_enabled = true
loop_counter = "code_iterations"
command = { target = "code", operation = "create" }
on_success = "check_tasks"

[[workflow.states]]
name = "check_tasks"
max_visits = 15              # Another override
operation = { id = "tasks-done", type = "tasks_complete", on_false = "fail" }
on_success = "code_review"
on_failure = "create_code"   # Loop back

State Definitions

States are defined with [[workflow.states]] array syntax:

[[workflow.states]]
name = "state_name"
description = "Optional description"
# ... other fields

State Fields

FieldTypeRequiredDescription
nameStringYesUnique state identifier
descriptionStringNoHuman-readable description
operationObjectNoSingle operation to execute
operationsArrayNoMultiple operations to execute in sequence
commandObjectNoNia command to execute (legacy)
pre_stepsArrayNoSteps to run before command (legacy)
post_stepsArrayNoSteps to run after command (legacy)
approvalObjectNoApproval gate configuration
on_successStringNoState to transition to on success
on_failureStringNoState to transition to on failure
loop_enabledBooleanNoEnable loop behavior (default: false)
loop_counterStringNoCounter variable name for loops
escape_conditionsArrayNoConditions to exit loops
retryObjectNoRetry configuration
max_visitsNumberNoOverride loop detection threshold for this state

Note: States must specify one of: operation, operations, command, or approval. The operation/operations fields represent the new operation model, while command/pre_steps/post_steps are legacy patterns maintained for backward compatibility.


State Operations

States can execute operations using the operation (single) or operations (multiple) fields. Operations allow you to execute steps, checks, and commands as first-class workflow state actions.

Single Operation

Execute one operation per state:

[[workflow.states]]
name = "setup"
operation = { id = "create-dir", type = "builtin", action = "make_directory", path = "output" }
on_success = "next"
on_failure = "failed"

Multiple Operations

Execute a sequence of operations in one state:

[[workflow.states]]
name = "setup-and-validate"
operations = [
    { id = "create-dir", type = "builtin", action = "make_directory", path = "output" },
    { id = "verify-dir", type = "file_exists", path = "output", on_false = "fail" },
    { id = "set-env", type = "builtin", action = "set_env", env_name = "READY", env_value = "true" },
]
on_success = "next"
on_failure = "failed"

Execution Rules:

  • Operations execute in definition order
  • First failure stops execution and triggers on_failure transition
  • Environment variables set by earlier operations are available to later operations
  • Progress display shows [n/total] for multi-operation states

Operation Types

An operation is one of three kinds: step, check, or command. The kind is inferred from the operation’s fields — there is no separate kind field:

Operation is a…When…
Commandit has a target field
Steptype is shell, builtin, or agent
Checktype is one of the check types listed below

Important: type holds the concrete operation type directly. Do not write type = "step" or type = "check", and do not use a separate step_type or check_type field — those forms are rejected during workflow validation.

Step Operations

Execute a step (shell command, builtin action, or AI agent):

Shell Step:

operation = { id = "run-tests", type = "shell", command = "cargo test" }

Built-in Directory Creation:

operation = { id = "create-output", type = "builtin", action = "make_directory", path = "output" }

Environment Variable:

operation = { id = "set-mode", type = "builtin", action = "set_env", env_name = "MODE", env_value = "production" }

Agent Step:

operation = { id = "send-initial", type = "agent", prompt = "Send the initial response.", context = ["ticket"] }

Step Fields:

FieldTypeRequiredDescription
idStringYesUnique step identifier
typeStringYes“shell”, “builtin”, or “agent”
commandStringFor shellShell command to execute
actionStringFor builtinBuilt-in action name
promptStringFor agentPrompt sent to the AI agent
contextArrayNoContext to inject into an agent prompt
share_session_withStringNoState name whose agent session to reuse
timeout_secondsNumberNoExecution timeout (default: 300)
retry_countNumberNoNumber of retries (default: 0)
retry_delay_secondsNumberNoDelay between retries (default: 1)

Agent Context Values:

context accepts only these values: issue, code, pr, security, ticket. An unrecognized value makes the operation fail workflow validation.

Built-in Actions:

  • make_directory: Create directory (path field required)
  • set_env: Set environment variable (env_name, env_value required)
  • copy_file: Copy file (source, destination required)
  • write_file: Write content to file (path, content required)

Check Operations

Evaluate a condition and control workflow based on result:

File Exists:

operation = { id = "config-exists", type = "file_exists", path = ".nia/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:

FieldTypeRequiredDescription
idStringYesUnique check identifier
typeStringYesType of validation (see below)
on_falseStringYes“fail” or “skip”
timeout_secondsNumberNoExecution timeout (default: 30)
retry_countNumberNoNumber of retries (default: 0)
retry_delay_secondsNumberNoDelay between retries (default: 1)

Check Types:

TypeDescriptionRequired Fields
file_existsFile existspath
directory_existsDirectory existspath
path_existsFile or directory existspath
env_existsEnvironment variable is setenv_name
env_equalsEnvironment variable equals valueenv_name, env_value
file_containsFile contains stringpath, content
file_matchesFile matches regex patternpath, pattern
command_existsCommand is available in PATHcommand
command_successShell command exits with 0command
tasks_completeAll tasks in tasks.md are completepath (optional)
counter_matchesLoop counter matches expressioncounter_name, counter_expression

Check Behaviors:

The on_false field controls what happens when a check evaluates to false:

ValueBehavior
"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 = ".nia/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 TypeDetectionCheckbox Behavior
Task sectionsHeaders without exclusion keywordsCounted
Non-task sectionsHeaders containing “acceptance”, “criteria”, “summary”, “requirement”, “validation”, etc.Ignored
Code blocksContent 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 evaluate
  • counter_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 Nia CLI command within the workflow:

operation = { type = "command", target = "issue", operation = "draft", modifiers = ["lite"] }

With arguments:

operation = { type = "command", target = "code", operation = "review", args = { model = "gpt-4" } }

Command Fields:

FieldTypeRequiredDescription
typeStringYesAlways “command”
targetStringYesCommand target (e.g., “issue”, “code”, “pr”)
operationStringYesCommand operation (e.g., “draft”, “review”)
modifiersArrayNoList of modifiers to apply
argsObjectNoArgument overrides

Environment Persistence

Environment variables set by steps persist across operations and states:

Within State: Available to subsequent operations in the same state

[[workflow.states]]
name = "multi-op"
operations = [
    { id = "set-var", type = "builtin", action = "set_env", env_name = "JOB_ID", env_value = "123" },
    { id = "use-var", type = "shell", command = "echo $JOB_ID" },
]

Across States: Available to operations in subsequent states

[[workflow.states]]
name = "configure"
operation = { id = "set-id", type = "builtin", action = "set_env", env_name = "JOB_ID", env_value = "123" }
on_success = "process"

[[workflow.states]]
name = "process"
operation = { id = "use-id", type = "shell", command = "echo \"Processing $JOB_ID\"" }
on_success = "done"

Example Workflows

See the example workflows in .nia/config/workflows/:

  • 06-step-check-demo.toml: Basic steps and checks
  • 07-multi-operation-state.toml: Multiple operations in one state
  • 08-conditional-validation.toml: Conditional branching with checks

Legacy Command and Pre/Post Steps (Backward Compatibility)

Deprecated: The command, pre_steps, and post_steps fields are maintained for backward compatibility. New workflows should use the operation or operations fields instead (see State Operations section above).

Commands (Legacy)

Execute a nia command within a state:

[[workflow.states]]
name = "draft_issue"

[workflow.states.command]
target = "issue"
operation = "draft"
modifiers = ["edit"]  # Optional
args = { model = "gpt-4" }  # Optional argument overrides

Command Fields

FieldTypeRequiredDescription
targetStringYesCommand target (e.g., “issue”, “code”, “pr”)
operationStringYesCommand operation (e.g., “draft”, “review”)
modifiersArrayNoList of modifiers to apply
argsObjectNoArgument overrides

Pre/Post Steps (Legacy)

Deprecated: Pre/post steps are maintained for backward compatibility. New workflows should use state operations instead.

Steps execute before or after the main command:

[[workflow.states.pre_steps]]
kind = "step"
id = "run-tests"
type = "shell"
command = "cargo test"
timeout_seconds = 300

[[workflow.states.pre_steps]]
kind = "check"
id = "verify-env"
type = "env_var_set"
name = "API_KEY"
on_false = "fail"

Step Types

TypeDescriptionRequired Fields
shellRun shell commandcommand
builtinBuilt-in actionaction, varies by action
agentAI agent executionprompt

Check Types

TypeDescriptionRequired Fields
file_existsFile existspath
directory_existsDirectory existspath
env_var_setEnvironment variable existsname
env_equalsEnv var equals valuename, env_value
command_existsCommand in PATHcommand
file_containsFile contains stringpath, content
file_matchesFile matches regexpath, pattern

Step/Check Fields

FieldTypeDescription
kindString“step” or “check”
idStringUnique identifier
typeStringStep/check type
depends_onArrayDependencies (step IDs)
timeout_secondsNumberExecution timeout
retry_countNumberNumber of retries
retry_delay_secondsNumberDelay between retries
on_falseStringFor checks: “fail” or “skip”

Approval Gates

Pause workflow for human approval:

[[workflow.states]]
name = "await_approval"

[workflow.states.approval]
gate_id = "deploy_approval"
message = "Ready to deploy to production. Approve?"
required_code = "DEPLOY"  # Optional confirmation code
timeout_seconds = 86400   # 24 hours

Approval Fields

FieldTypeRequiredDescription
gate_idStringYesUnique approval identifier
messageStringYesMessage shown to user
required_codeStringNoConfirmation code to type
timeout_secondsNumberNoAuto-reject after timeout
on_timeoutStringNoState on timeout (else on_failure)

Loop Configuration

Enable state looping with escape conditions:

[[workflow.states]]
name = "retry_deploy"
loop_enabled = true
loop_counter = "deploy_attempts"

[[workflow.states.escape_conditions]]
counter_value = 3
action = "approval"
approval_gate = "manual_check"
message = "Failed 3 times. Continue?"

[[workflow.states.escape_conditions]]
counter_value = 10
action = "abort"
error_message = "Maximum retries exceeded"

Escape Condition Fields

FieldTypeRequiredDescription
counter_valueNumberYesCounter threshold
actionStringYes“continue”, “transition”, “approval”, “abort”
target_stateStringFor transitionTarget state name
approval_gateStringFor approvalApproval gate ID
messageStringNoDisplay message
error_messageStringFor abortError message

Retry Configuration

Automatic retry on failure:

[[workflow.states]]
name = "flaky_operation"

[workflow.states.retry]
max_retries = 5
retry_delay = "10s"
timeout = "2m"

[[workflow.states.retry.retry_conditions]]
retry_count = 3
action = "approval"
approval_gate = "retry_approval"
message = "Failed 3 times. Approve to continue retrying?"

Retry Fields

FieldTypeRequiredDescription
max_retriesNumberYesMaximum retry attempts
retry_delayStringYesDelay between retries (“5s”, “1m”)
timeoutStringNoPer-attempt timeout
retry_conditionsArrayNoConditional behavior

Retry Condition Fields

FieldTypeDescription
retry_countNumberRetry count threshold
actionString“continue”, “transition”, “approval”
timeoutStringOverride timeout at this count
target_stateStringFor transition
approval_gateStringFor approval
messageStringDisplay message

Terminal States

Terminal states end the workflow. By convention, terminal state names must end with:

  • _success - Successful completion
  • _failed - Failure
  • _completed - Neutral completion
  • _cancelled - User cancelled
[[workflow.states]]
name = "deploy_success"
description = "Deployment completed successfully"
# No on_success/on_failure - this is terminal

Duration Strings

Duration fields accept strings in the format:

  • "5s" - 5 seconds
  • "2m" - 2 minutes
  • "1h" - 1 hour

Complete Example

The production issue-to-pr workflow in .nia/config/workflows/issue-to-pr.toml demonstrates advanced patterns:

Key Features:

  • Iterative code generation with loop logic and tasks_complete check
  • Counter-based context clearing using counter_matches check (every 3rd iteration)
  • Loop detection configuration with higher thresholds for code generation
  • Per-state max_visits overrides for iterative states
  • Multiple approval gates for human oversight
  • Automated PR creation and review

Example Patterns from issue-to-pr.toml:

Loop Detection Configuration:

[workflow.loop_detection]
max_transitions = 150       # Allow longer workflow due to looped code creation
on_loop_detected = "approval_gate"  # Allow recovery instead of immediate failure

Iterative Code Generation with Task Checking:

[[workflow.states]]
name = "create_code"
max_visits = 12              # Override global threshold
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 = "context_counter"       # Tasks remain, check counter

[[workflow.states]]
name = "context_counter"
operation = {
    id = "context_check",
    type = "counter_matches",
    counter_name = "code_iterations",
    counter_expression = "% 3 == 0",
    on_false = "fail"
}
on_success = "create_code_clear"   # Counter % 3 == 0, use --clear
on_failure = "create_code"          # Counter % 3 != 0, continue normally

View the Full Example:

cat .nia/config/workflows/issue-to-pr.toml

Validation

Workflows are validated when loaded:

  1. Schema validation - Valid TOML syntax and required fields
  2. Semantic validation - All transition targets exist, no orphan states
  3. Loop validation - Loops have abort/transition escape conditions
  4. Terminal validation - Terminal states follow naming convention

If validation fails, you’ll see detailed error messages with line numbers and suggestions.

See Also