Approval

Manual approval pauses the workflow and prompts the user for yes/no before routing.

Approval Block

An approval state pauses execution after its handler and asks the user a question:

review:
  type: engine
  approval:
    question: "Does this look good?"
    PASSED: deploy
    FAILED: rework

User interaction:

When running with --next=N, Raili may stop after the approval state if the configured --next execution limit has been reached; in that case Raili records the approval response but will not follow the configured approval transition. This enables single-step inspection (for example --next=1) without progressing the workflow.

There is an optional multiline mode (see below).

Approval Fields

Field Required Type Description
question string Question shown to the user. Supports ${variable_name} interpolation
PASSED string Next state if user presses Enter
FAILED string Next state if user types a reason
notify string Shell command run BEFORE the prompt (e.g., alert reviewers)
multiline boolean When true, allow the user to enter multiple lines as a reason; terminate input with a line containing only /q

With Optional Notification

Send an alert before showing the prompt:

review:
  type: engine
  approval:
    notify: "msg.sh 'Review needed for ticket $RAILI_VAR_TICKET_ID'"
    question: "Deploy to production?"
    PASSED: deploy
    FAILED: rework

The notification runs after the state handler completes but before the user is prompted.

Multiline approval

Enable multiline input for richer rejection reasons:

review:
  type: engine
  approval:
    multiline: true
    question: |
      The diff is large. Please provide a reason for rejection (finish with /q):
    PASSED: continue
    FAILED: rework

Behavior:

With Variable Interpolation

Use ${variable_name} in questions:

review:
  type: script
  script: lint
  approval:
    question: |
      Update ticket ${ticket_id}?
      Branch: ${branch}
      Changes: ${description}
      
      Approve?
    PASSED: deploy
    FAILED: code

Missing variables → immediate error (fail-fast).

Pluggable Approval & Feedback Resolvers

Raili can automatically run JS resolver modules instead of showing the interactive prompt. Place resolver files under the workflow directory (e.g. .raili/main/):

Resolver configuration (.raili/<workflow>/config.json)

Resolvers and trigger behavior may be tuned via an optional config.json file placed in the workflow directory (for example .raili/main/config.json). When present, Raili merges user values with sensible defaults. Supported blocks (all values in seconds):

Example:

{
  "trigger": { "interval": 60, "timeout": 86400, "retry_interval": 10 },
  "approval": { "timeout": 1800 },
  "feedback": { "timeout": 3600 }
}

Defaults used when config.json is absent:

Behavior notes:

Behavior:

// Legacy string form
module.exports = async function (input) { return 'PASSED'; };

// New structured form
module.exports = async function (input) { return { outcome: 'FAILED', reason: 'Missing tests' }; };

The engine calls the resolver with an input object containing { question, stateName, vars?, outputPath? }. The runner normalizes legacy and structured shapes and validates the result; invalid values or thrown errors cause the run to fail immediately (fail-fast).

// Legacy string form
module.exports = async function (input) { return 'Automated note'; };

// New structured form
module.exports = async function (input) { return { feedback: 'Auto note', metadata: 'auto-generated' }; };

When a feedback resolver returns a string the engine stores that value into the workflow context.vars under the state’s declared expose_var. When the resolver returns an object the feedback string is stored into context.vars (same as typed feedback) and any metadata field is persisted to context.feedbacks and recorded in the state’s meta.feedback.metadata for auditability. Example feedback resolver:

module.exports = async function (input) {
  return 'Automated review: looks good';
};

Notes:

Approval Response Tracking

Approval questions, answers, and any notify metadata are recorded in context.json as part of the originating state’s history entry under a meta object. When manual prompts or feedback incur idle wait time, Raili records meta.waitMs (milliseconds) for that state — this represents the total time spent waiting for human input and is persisted to make runs auditable.

Example:

{
  "stateHistory": [
    {"state": "code", "enteredAt": "2026-03-13T08:15:00Z"},
    {
      "state": "review",
      "enteredAt": "2026-03-13T08:16:30Z",
      "meta": {
        "approval": { "question": "Does this look good?", "chosen": "PASSED", "reason": "" },
        "waitMs": 120000,
        "notify": { "command": "msg.sh 'Review needed'", "success": true }
      }
    }
  ]
}

Raili’s run-log computation (.raili/<workflow>/run-log.jsonl) now subtracts accumulated meta.waitMs across the run from the total recorded duration. This produces a duration value representing active processing time (total run wall-clock time minus human idle waits). The run-log line also includes waitMs so both active and idle times are available for analysis.

Key Differences from transitions:

Common Patterns

Pre-deployment review

deploy:
  type: engine
  approval:
    notify: "msg.sh 'Deploy review required'"
    question: "Deploy to production?"
    PASSED: deploy_prod
    FAILED: abort

Code review with context

review:
  type: script
  script: show_changes
  approval:
    question: |
      Changes for ticket ${ticket_id}:
      ${description}
      
      Merge?
    PASSED: merge
    FAILED: edit

Multi-step approval

step1_review:
  type: engine
  approval:
    question: "Pass step 1?"
    PASSED: step2_review
    FAILED: rework

step2_review:
  type: engine
  approval:
    question: "Pass step 2?"
    PASSED: complete
    FAILED: rework

Approval reason persistence

When a resolver returns an object that includes a reason, or a user declines an approval and supplies a typed reason, Raili persists that non-empty reason in two places in the workflow context:

Only non-empty reasons are persisted. Reasons are persisted for any outcome when provided (for example a resolver may return { outcome: 'PASSED', reason: 'Auto-approved by CI' } and the reason will be recorded). This keeps approval metadata separate from declared inputs while preserving env-compatible access for shell hooks.

Interaction with teach: mappings

When a state declares teach:, any approval-exposed variables (e.g. REVIEW_FAILED) are now written to context.vars before the state’s teach: mappings are processed. This means a teach: entry on the same state can reference approval-produced variables such as ${REVIEW_FAILED}. This ordering ensures learnings can be created from the user’s approval reason within the originating state (fail-fast errors for missing variables are avoided in this scenario).