Triggo Documentation
Agent Integration

Approval Flows

Gate high-stakes actions behind human approval — list, approve, and reject runs waiting at an approval gate via the runtime API or MCP.

Approval Flows

Approvals let a workflow pause for a human decision before it takes a costly or irreversible step — sending an external message, deleting a record, moving money, applying an autonomous remediation. A workflow is configured to require approval for specific actions or steps; at run time those runs sit in a waiting_for_approval state until a human (or a scoped agent) approves or rejects them. This page covers the decision surface exposed to agents: listing pending runs, approving or rejecting via REST or MCP, and how expiry is handled.

Lifecycle

Two records move together when a run hits an approval gate: the runs row that tracks the execution, and an approval_requests row that carries the decision state, comment, and audit trail.

The approval request itself has four terminal states:

                         approve_run / POST approve (decision=approved)
                 ┌──────────────────────────────────────────────────────▶ approved

   pending ──────┤
                 │                         POST approve (decision=rejected)
                 ├──────────────────────────────────────────────────────▶ rejected

                 │   1h TTL, approval-expiry worker
                 └──────────────────────────────────────────────────────▶ expired

The parent run moves in lockstep: waiting_for_approvalrunning on approve, cancelled on reject, timed_out on expiry. The transition only commits after the resume job has been enqueued, so a Redis outage will not strand a run in running with no executor job behind it.

Listing runs that need a decision

GET /api/v1/runtime/runs with needs_approval=true narrows the list to runs currently paused at an approval gate. The endpoint requires the runs:read scope and accepts these filters — all optional:

Query paramTypeMeaning
needs_approvaltrue | falseWhen true, returns only runs in waiting_for_approval.
action_slugstringFilter to one published action.
statusstringExplicit status filter (for example waiting_for_approval, failed).
sourcestringaction, replay, scheduler, etc.
has_incidenttrue | falseRuns linked to an incident or not.
date_from, date_toISO-8601Bound by created_at.
limitinteger, 1–100, default 20Page size.
offsetinteger, default 0Page offset.
curl -sS 'https://api.triggo.ai/api/v1/runtime/runs?needs_approval=true&limit=20' \
  -H "Authorization: Bearer $TRIGGO_API_KEY"

The response carries one row per run:

{
  "runs": [
    {
      "run_id": "3f2e…",
      "action_slug": "send_refund",
      "source": "action",
      "status": "waiting_for_approval",
      "duration_ms": null,
      "created_at": "2026-04-13T09:41:12.004Z"
    }
  ],
  "total": 1,
  "limit": 20,
  "offset": 0
}

To fetch the full approval context (the pending approval_requests row, resolved inputs, description, expiry), call GET /api/v1/runtime/runs/:runId for the run you intend to decide on.

Approving or rejecting via REST

The same endpoint handles both decisions. The body's decision field selects the outcome.

POST /api/v1/runtime/runs/:runId/approve
Authorization: Bearer <key>   (scope: approvals:decide)
Content-Type: application/json

{
  "decision": "approved" | "rejected",
  "comment": "optional, up to 1000 chars"
}
curl -sS -X POST https://api.triggo.ai/api/v1/runtime/runs/3f2e…/approve \
  -H "Authorization: Bearer $TRIGGO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"decision":"approved","comment":"verified refund with support thread"}'

On success you get:

{
  "run_id": "3f2e…",
  "status": "running",
  "decision": "approved",
  "decided_at": "2026-04-13T09:42:03.118Z"
}

Rejection returns "status": "cancelled". The comment is truncated server-side to 1000 characters and recorded on the approval_requests row along with the acting userId and decided_via: "api", which feeds the audit log (approval.decided).

Approving via MCP

MCP exposes the same operation as the approve_run tool. Same scope (approvals:decide), same payload shape, same result.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "approve_run",
    "arguments": {
      "run_id": "3f2e…",
      "decision": "approved",
      "comment": "verified refund with support thread"
    }
  }
}

decision must be "approved" or "rejected"; comment is optional and capped at 1000 characters by the tool's Zod schema. The tool result mirrors the REST response.

Expiration

Every approval request is created with a 1-hour TTL. Creation schedules a BullMQ approval-expiry job whose jobId is derived from the approval id, so a decision inside the window removes the expiry job cleanly — there is no race where a user-approved run gets expired a moment later.

If the TTL elapses with no decision, approval-expiry.worker resolves the request as expired, writes an approval.decided audit entry with decision: "expired", and — if the approval is tied to a pipeline run — journals an APPROVAL_EXPIRED step failure and transitions the run to timed_out. The user is notified in their configured channel that the request expired and was auto-rejected.

Practically, an expired approval is equivalent-to-rejected for the run: the step does not execute, the workflow does not resume. The distinction matters for analytics and incident review — expired means "nobody decided in time", not "a human said no".

Errors

The endpoint and the MCP tool share the same error codes. HTTP status is derived from each error code by the runtime; MCP returns isError: true with the same code string.

CodeStatusWhen
UNAUTHORIZED401Missing, malformed, or revoked Bearer key.
FORBIDDEN403Key is valid but lacks approvals:decide.
RUN_NOT_FOUND404No run with that id in this workspace.
BAD_REQUEST400decision is not "approved" or "rejected"; run is not in waiting_for_approval; or the approval was already resolved by a concurrent caller.
RATE_LIMIT_EXCEEDED429Sliding-window limit hit — back off per the retry-after header.

The "already resolved" case (BAD_REQUEST with message "Approval already resolved") is the one to handle gracefully: two agents racing on the same run, or a user and an agent both deciding. The update is atomic — UPDATE … WHERE status = 'pending' — so exactly one caller wins. Losers should re-fetch the run and reconcile against its new status.

On this page