> ## Documentation Index
> Fetch the complete documentation index at: https://velt-mintlify-40d8baa1.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Approval Engine (Beta)

> Multi-step approval workflows where AI agents and humans review work together.

## What is the Approval Engine?

Build approval flows as a graph. Define the workflow once, dispatch it against a trigger, and the engine runs everything: state, retries, fan-out, parallel review, SLAs, and webhooks.

You write nodes (`agent`, `human`) and connect them with edges. You record reviewer decisions and agent resolutions through the REST API. The engine does the rest. Every state change is persisted, replayable, and (when you configure a webhook) pushed to your receiver in real time.

This is the part of an approval system you don't want to build yourself: the runtime that keeps state, handles concurrent reviewers, enforces quorum, fires events, and survives outages.

## What you get

* **Agent + human steps in one flow.** Run AI agents inline. Park them for human review when needed. Mix and match without writing glue code.

* **Parallel review with quorum.** Run reviewers in parallel. Gate downstream work behind a quorum policy: wait for everyone, advance once N approve, or require specific people regardless of count.

* **Real-time webhooks.** Every state change is POSTed to your receiver with an HMAC-SHA256 signature and exponential-backoff retry. Missed events are recoverable via `/executions/getEvents`.

* **Idempotent dispatch.** Replay safely. Same `idempotencyKey`, same execution id, no duplicates. Even concurrent dispatches with the same key return the original id.

* **SLA-aware.** Set a deadline per step. The engine transitions to `breached` and routes through dedicated `breached` edges.

* **Versioned definitions.** Updates increment the version. In-flight executions stay pinned to the version they started on.

* **Static linting.** Definitions are validated at write time for cycles, dangling edges, unreachable nodes, and quorum misconfiguration. No surprises at runtime.

## Use cases

<CardGroup cols={2}>
  <Card title="Marketing copy approval" icon="newspaper">
    An AI agent drafts copy. Legal and brand reviewers approve in parallel. A publish agent ships the asset once both approve.
  </Card>

  <Card title="AI agent + human review" icon="robot">
    Park a blocking agent step until N external resolutions arrive, then continue. Useful for agent-assisted moderation or QA pipelines where humans review specific outputs.
  </Card>

  <Card title="Regulated workflows" icon="shield-check">
    Multi-stakeholder sign-off where specific reviewers (compliance, legal) must approve even if a numeric quorum is otherwise met. Every decision is auditable with correlation IDs.
  </Card>

  <Card title="Multi-stakeholder review" icon="users">
    3-of-5 quorum with `cancelOnQuorum` stops bothering remaining reviewers once the threshold is met. System-actor cancellation events go into the audit trail.
  </Card>

  <Card title="Contract sign-off" icon="file-signature">
    Sequential or parallel approval chains with SLAs. If a step breaches its deadline, route to an escalation node or fail-fast the execution.
  </Card>

  <Card title="Document approval" icon="file-check">
    Scope workflows per-document so each instance is bound to a specific `documentId`, with per-execution webhook receivers for downstream integrations.
  </Card>
</CardGroup>

## How it works

1. **Define the workflow.** Register a definition with nodes, edges, and optional parallel groups. The engine lints it at write time.
2. **Dispatch an execution.** POST a `definitionId`, an idempotency key, and optional `triggerContext` (exposed as `execution.input.*` in edge expressions). The engine pins the current version and enqueues the first step.
3. **The engine drives the flow.** Agent nodes run immediately. Human nodes (and blocking agent nodes) park in `waiting` until decisions arrive.
4. **Record decisions.** Call `/steps/recordReviewerDecision` for humans and `/steps/recordAgentResolution` for blocking agents. Matching edges fire. Quorum policies trigger their side effects.
5. **Consume events.** Subscribe via webhook for real-time delivery. Poll `/executions/getEvents` with `sinceSeq` to reconcile after an outage.

## Mental model

### Definition

A **definition** is the blueprint. An **execution** is one live run of that blueprint. A **step** is one node executing inside an execution.

### Nodes

Work units that can run.

| Type      | What it does                                                                                              | Parks in `waiting`?        |
| --------- | --------------------------------------------------------------------------------------------------------- | -------------------------- |
| `agent`   | Runs an agent. Non-blocking by default. With `blocking: true`, parks until external resolutions arrive.   | Only when `blocking: true` |
| `human`   | Requires reviewer approval. Use `reviewers: [{ userId, mandatory }]`. Legacy `reviewerIds[]` still works. | Yes                        |
| `webhook` | Deferred in v1: the type validates in a definition, but the runtime handler isn't enabled yet.            | Doesn't run in v1          |

### Edges

Connections between nodes. Edges optionally carry a `when` expression like `output.passesBrandCheck == true`. Expressions compile at write time (pure AST, no `eval`) and walk at runtime.

Supported operators: equality, comparison, boolean, regex, `includes`, `startsWith`, `endsWith`, `length`, `isEmpty`.

Path roots: `output.*`, `step.*`, `execution.input.*`.

### Execution

An execution is one live run of a definition. Dispatch pins the definition version, stamps a correlation ID and idempotency key, and enqueues the first step(s).

**Lifecycle:**

```
pending → running → completed | failed | cancelled
```

### Step

A step is one runtime instance of a node.

**Lifecycle:**

```
pending → running → (waiting) → completed | failed | skipped | cancelled | breached
```

`waiting` only applies to human steps and blocking agent steps.

Step IDs are deterministic, so retries land on the same doc:

* **Root steps** (no incoming edges): `step_<nodeId>_<timestamp>_<rand>`
* **Per-edge fan-out**: `${parentStepId}__to__${childNodeId}`
* **`joinOnQuorum` fan-out**: `group_<groupId>__to__<childNodeId>` (one instance regardless of how many group members ran)

## Scope

Pick the level that matches how your product is structured.

| Level          | Bound to                             |
| -------------- | ------------------------------------ |
| `apiKey`       | Workspace-wide                       |
| `organization` | A specific `organizationId`          |
| `document`     | A specific `documentId` under an org |

Defaults to `{ level: "apiKey" }`.

<Note>
  Approval Engine state is partitioned per tenant: each tenant's executions, definitions, and events are routed to that tenant's dedicated `storeDbId`.
</Note>

## Idempotency

Dispatch is idempotent on `idempotencyKey`. Replay with the same key returns `{ deduplicated: true, executionId: <original> }`. Safe to retry from clients and queues.

## Events and webhooks

Every state change writes an event doc with a monotonic `seq`. Set `webhookUrl` and `webhookSecret` on dispatch and externally-visible events are POSTed to your receiver with an HMAC-SHA256 signature and exponential-backoff retry. Recover missed events via `/executions/getEvents` with `sinceSeq`.

### Inbound webhook handler

In addition to outbound delivery (above), the Approval Engine exposes an **inbound** webhook handler: an HTTP endpoint with a stable URL that external systems such as Salesforce, Stripe, or GitHub can POST raw JSON to.

Unlike the standard REST endpoints, payloads do **not** need to be wrapped in a `{data: ...}` envelope. The handler accepts raw JSON bodies directly.

The inbound handler enforces:

* **Bearer-token validation:** requests must carry a valid bearer token; unauthenticated requests are rejected before any processing.
* **Signed callback tokens:** callbacks issued by the handler are signed, allowing downstream consumers to verify authenticity end-to-end.
* **Rate limiting:** per-source request rate is capped to prevent abuse.
* **Body-size limits:** oversized payloads are rejected at the boundary before parsing.
* **SSRF URL guard:** any URL values in the incoming payload are validated against an allowlist to block server-side request forgery.

<Note>
  The inbound and outbound webhook surfaces are complementary, not duplicative. **Inbound**: external systems push events into the Approval Engine. **Outbound**: the Approval Engine pushes state-change events out to your receiver. Both paths are active independently and can coexist on the same workflow.
</Note>

## Get started

<CardGroup cols={2}>
  <Card title="Setup" icon="gear" href="/ai/approval-engine/setup">
    A hands-on walkthrough: author a workflow definition, dispatch a run, record decisions, and receive the outcome via webhooks or polling.
  </Card>

  <Card title="Customize Behavior" icon="sliders" href="/ai/approval-engine/customize-behavior">
    Node configuration, edge gating expressions, parallel groups and quorum policies, SLAs, linter rules, event reference, and the error vocabulary.
  </Card>
</CardGroup>
