Approval Workflows

HITL Workflows

Build workflows where AI agents pause for human review, approval, or input before continuing.

Approval is routing. A person approves work the same way any agent receives it: a can_call condition sends the work to a human agent, and that human agent's own can_call sends their decision on. There is no separate approval switch on an agent — require_human_confirmation, confirmation_seat and human_escalation_triggers were removed in 2.4.382, and a config that turns one of them on is refused at load. Every pattern on this page is built from routing.

Mid-Flow Human Review

The most common pattern: an LLM agent processes a request, routes to a human for review, then the human's decision drives the next step.

Configuration

name: "budget_review" architecture: "managed_mesh" entry_points: - name: "review_budget" target: "analyzer" agents: analyzer: name: "analyzer" agent_type: "llm" model: "gpt-4o-mini" communication_type: "dual" prompt: | Analyze the budget data and prepare a structured review summary. Flag any line items that exceed 10% variance from last quarter. yields: summary: "string" flagged_items: "list" total_variance: "number" can_call: - agent: "human_reviewer" human_reviewer: name: "human_reviewer" agent_type: "human" is_human_powered: true human_interface: "api" human_timeout_seconds: 600 communication_type: "dual" webhook_config: connection: "review-dashboard" # Outbound webhook record (Infrastructure → Add custom) outbound_timeout: 30 inbound_endpoint: "/webhook/review_budget" max_retries: 3 retry_delay: 5 yields: decision: "string" notes: "string" can_call: - agent: "submitter" condition: "decision == 'approved'" - agent: "revision_handler" condition: "decision == 'revision_needed'" submitter: name: "submitter" agent_type: "programmatic" yields: status: "string" confirmation_id: "string" revision_handler: name: "revision_handler" agent_type: "programmatic" yields: notification_sent: "boolean"

What Happens

POST /api/mesh/request
  {"entry_point": "review_budget", "data": {"message": "Review Q1 budget"}}

1. analyzer (LLM) runs → produces summary + flagged items
2. can_call routes to human_reviewer
3. LeafMesh sends outbound webhook to your-app.example.com/review-needed
4. Flow PAUSES — mesh_call returns analyzer's result immediately

   --- Human reviews in your dashboard ---

5. Human responds: POST /webhook/review_budget
   {"session_id": "...", "decision": "approved", "notes": "Looks good"}

6. LeafMesh resumes → evaluates can_call → routes to submitter
7. submitter runs → budget submitted

Testing the Flow

# Step 1: Trigger the workflow curl -X POST http://127.0.0.1:18820/api/mesh/request \ -H "Content-Type: application/json" \ -d '{"entry_point": "review_budget", "data": {"message": "Review Q1 budget"}}' # Returns: {"status": "completed", "session_id": "abc-123", ...} # Step 2: Load your webhook signing secret from the environment # import os # secret = os.environ["LEAFMESH_WEBHOOK_SECRET"] # (header: X-LeafMesh-Signature, format: sha256=<HMAC-SHA256 hex digest>) # Step 3: Respond as the human. # # Inbound webhooks are HMAC-SHA256 signed and bound to a fresh # timestamp + nonce so captured signed requests cannot be replayed. # Three headers are required: # - X-LeafMesh-Signature : sha256=<hex> # - X-LeafMesh-Timestamp : Unix seconds # - X-LeafMesh-Nonce : 8–128 chars [A-Za-z0-9_-], unique per request # # Signed material: `f"{timestamp}.{nonce}.".encode() + body` BODY='{"session_id": "abc-123", "decision": "approved", "notes": "Budget approved"}' SECRET="YOUR_SECRET" TS=$(date +%s) NONCE=$(python3 -c 'import secrets; print(secrets.token_urlsafe(16))') SIG=$(printf '%s' "$TS.$NONCE.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}') curl -X POST http://127.0.0.1:18820/webhook/review_budget \ -H "Content-Type: application/json" \ -H "X-LeafMesh-Signature: sha256=$SIG" \ -H "X-LeafMesh-Timestamp: $TS" \ -H "X-LeafMesh-Nonce: $NONCE" \ -d "$BODY" # Returns: {"status": "resumed", "session_id": "abc-123", "agent": "human_reviewer"}

Key response statuses:

  • "resumed" — LeafMesh found the pending session and resumed the flow
  • "accepted" — No pending session found, treated as a new request

Conditional Approval

Route to human review only when thresholds are exceeded:

agents: processor: name: "processor" model: "gpt-4o" yields: recommendation: "string" risk_level: "string" amount: "number" can_call: - agent: "human_reviewer" condition: "amount > 1000 or risk_level == 'high'" - agent: "auto_approve" condition: "amount <= 1000 and risk_level != 'high'"

Low-risk items never reach a person. High-risk items are routed to the human reviewer, and the flow continues from their decision.

Multi-Level Approvals

Chain multiple human agents for escalating approval levels:

agents: team_lead: name: "team_lead" agent_type: "human" is_human_powered: true human_timeout_seconds: 300 webhook_config: connection: "team-lead-review" # Outbound webhook record (Infrastructure → Add custom) inbound_endpoint: "/webhook/approve" can_call: - agent: "executor" condition: "decision == 'approved' and amount <= 10000" - agent: "director" condition: "decision == 'approved' and amount > 10000" - agent: "rejection_handler" condition: "decision == 'rejected'" director: name: "director" agent_type: "human" is_human_powered: true human_timeout_seconds: 1800 webhook_config: connection: "director-review" # Outbound webhook record (Infrastructure → Add custom) inbound_endpoint: "/webhook/approve" can_call: - agent: "executor" condition: "decision == 'approved'"

Each level has its own webhook, timeout, and routing conditions.

Timeout Handling

By default, a human who doesn't respond is treated as an error, not an assumed answer. When the timeout elapses, LeafMesh raises a timeout that escalates through the Manager — it never fabricates a decision on the person's behalf. This is deliberate: a silent human should never be read as approval.

agents: human_reviewer: name: "human_reviewer" agent_type: "human" human_timeout_seconds: 300 # fallback_on_timeout defaults to false — timeout escalates/errors.

The HUMAN_INPUT_TIMEOUT event triggers Manager coordination:

  • Escalate (default): raise the timeout to a different human reviewer or a Manager escalation target.
  • Retry: send the request again.
  • Stop: halt the workflow.

Opting into an automatic fallback

If — and only if — you want a timed-out human to be replaced by a fixed answer so the chain continues, opt in explicitly with fallback_on_timeout: true and supply the fallback_response:

agents: human_reviewer: name: "human_reviewer" agent_type: "human" human_timeout_seconds: 300 fallback_on_timeout: true # explicit opt-in fallback_response: decision: "timeout_default" message: "Request timed out — used default"

Behavior change (v2.4.82): fallback_on_timeout now defaults to false. Deployments that previously relied on the old auto-fallback-on-timeout behavior must set fallback_on_timeout: true explicitly to keep it.

Human as Initiator

A human agent can also be the entry point — the person who starts workflows:

agents: client: name: "client" agent_type: "human" is_human_powered: true human_interface: "api" webhook_config: inbound_endpoint: "/webhook/start" can_call: - agent: "greeter_agent"
# Human initiates via webhook curl -X POST http://127.0.0.1:18820/webhook/start \ -H "Content-Type: application/json" \ -H "X-LeafMesh-Signature: sha256=..." \ -d '{"message": "I need help with my account"}'

Next Steps


LeafMesh — Structured approval workflows with human judgment

Last updated: 11/10/2026