> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.withverifa.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.withverifa.com/_mcp/server.

# Workflows

> Design verification pipelines with the visual workflow builder or API.

A **workflow** defines the verification pipeline for a session — which checks to
run, in what order, and how to route based on results. Workflows are directed
acyclic graphs (DAGs) where each node is a check, a conditional branch, or a
terminal outcome.

## How workflows work

When a session enters `processing`, the workflow engine:

1. Loads the workflow graph assigned to the session (or the org's default)
2. Starts at the entry node
3. Executes each check node, storing results as it goes
4. Routes through conditional nodes based on check outcomes or extracted data
5. Reaches a terminal node: `approved`, `rejected`, or `needs_review`

The engine supports automatic retries (up to 4 per check), external gates for
human-in-the-loop decisions, and percentage-based rollout for canary deployments.

## Workflow lifecycle

| Status      | Description                                         |
| ----------- | --------------------------------------------------- |
| `draft`     | Under construction. Cannot be assigned to sessions. |
| `published` | Active and available for session processing.        |

Workflows are versioned — every edit creates an immutable version snapshot for
audit and rollback. Sessions always execute against the version that was active
when processing began.

## Graph structure

Workflows use a graph format with four node types:

### Start node

The entry point. Every workflow has exactly one.

```json
{
  "start": {
    "type": "start",
    "next": "document_ocr_step"
  }
}
```

### Policy node

Executes a verification check. Routes to different nodes based on the outcome.

```json
{
  "face_match_step": {
    "type": "policy",
    "policy": "face_match",
    "config": {
      "security_level": "standard"
    },
    "on_pass": "age_check_step",
    "on_fail": "review_terminal",
    "on_error": "review_terminal"
  }
}
```

### Conditional node

Routes based on extracted data or check results. Evaluates conditions in order
and takes the first matching route, with a default fallback.

```json
{
  "age_gate": {
    "type": "conditional",
    "routes": [
      {
        "conditions": [
          { "field": "extracted.age", "op": ">=", "value": 21 }
        ],
        "target": "screening_step"
      },
      {
        "conditions": [
          { "field": "extracted.age", "op": ">=", "value": 18 }
        ],
        "target": "limited_approve"
      },
      {
        "conditions": [],
        "target": "reject_underage"
      }
    ]
  }
}
```

Supported operators: `>=`, `<=`, `>`, `<`, `==`, `!=`, `in`, `not_in`,
`contains`, `exists`.

### Terminal node

The final outcome. Every path through the graph must reach a terminal.

```json
{
  "approve": {
    "type": "terminal",
    "outcome": "approved"
  },
  "reject": {
    "type": "terminal",
    "outcome": "rejected"
  },
  "review": {
    "type": "terminal",
    "outcome": "needs_review"
  }
}
```

### Wait node (external gate)

Pauses the workflow and waits for an external decision via API callback. Useful
for integrating with your own decision engine.

```json
{
  "external_check": {
    "type": "wait",
    "policy": "external_gate",
    "config": {
      "timeout_hours": 24,
      "on_timeout": "review_terminal"
    }
  }
}
```

When the workflow pauses, the session status changes to `awaiting_external` and
a `session.awaiting_external` webhook is fired. Your server can then call the
resume endpoint with the decision.

## Example workflow

A standard KYC workflow that extracts document data, verifies biometrics,
screens against watchlists, and auto-approves or routes to review:

```json
{
  "entry": "start",
  "nodes": {
    "start": {
      "type": "start",
      "next": "ocr"
    },
    "ocr": {
      "type": "policy",
      "policy": "document_ocr",
      "on_pass": "face_match",
      "on_fail": "reject"
    },
    "face_match": {
      "type": "policy",
      "policy": "face_match",
      "config": { "security_level": "standard" },
      "on_pass": "age_check",
      "on_fail": "review"
    },
    "age_check": {
      "type": "policy",
      "policy": "age_verification",
      "config": { "min_age": 18 },
      "on_pass": "screening",
      "on_fail": "reject"
    },
    "screening": {
      "type": "policy",
      "policy": "aml_screening",
      "on_pass": "auto_decision",
      "on_fail": "review"
    },
    "auto_decision": {
      "type": "policy",
      "policy": "auto_approve",
      "on_pass": "approve",
      "on_fail": "review"
    },
    "approve": { "type": "terminal", "outcome": "approved" },
    "reject": { "type": "terminal", "outcome": "rejected" },
    "review": { "type": "terminal", "outcome": "needs_review" }
  }
}
```

## Data requirements

Workflows declare which data they need from the capture flow:

```json
{
  "data_requirements": {
    "selfie": true,
    "liveness": true,
    "document": {
      "enabled": true,
      "accepted_types": ["passport", "drivers_license", "state_id", "national_id"]
    },
    "user_info": {
      "enabled": true,
      "fields": ["first_name", "last_name"],
      "required_fields": ["first_name", "last_name"],
      "verify_phone": false,
      "verify_email": false
    },
    "proof_of_address": {
      "enabled": false,
      "accepted_types": ["utility_bill", "bank_statement", "council_tax"]
    }
  }
}
```

The capture UI automatically adjusts to collect the required data. If a workflow
requires proof of address, the capture flow adds an additional upload step.

`proof_of_address.accepted_types` holds canonical document-type keys —
`utility_bill`, `bank_statement`, `council_tax`, `tax_document`,
`government_letter`, and the rest of the
[proof-of-address vocabulary](/core-concepts/verifications-checks#document-types-and-recency-windows).
Leave it empty to accept everything your organization's proof-of-address policy
allows; the capture flow renders its picker from the resolved list either way.

The `proof_of_address_check` policy node itself defaults to **use organization
settings** — the policy configured under **Settings → Proof of address**. Set
any of the policy fields in the node's `config` to override them for this one
workflow; the two layers merge field by field, with the node winning. See
[Proof of address](/core-concepts/verifications-checks#proof-of-address) for
the full policy surface and the three-state outcome.

`liveness` controls the capture-time liveness check (a randomized head-turn
pose challenge after the selfie). It defaults to `true` and only applies when
`selfie` is `true` and your plan includes liveness detection. Set it to
`false` for a lower-friction flow that captures the selfie without the pose
challenge. Some workflows always run the liveness check regardless of this
flag — IAL2 workflows, and any workflow containing the
[Returning User Face Verification](#returning-user-re-verification) policy.

### Document-free workflows

Set `document.enabled` to `false` to run a selfie-only flow — for example a
low-friction KYC level 1 that collects the applicant's name and a
liveness-checked selfie with no ID document:

```json
{
  "data_requirements": {
    "selfie": true,
    "document": { "enabled": false, "accepted_types": [] },
    "user_info": {
      "enabled": true,
      "fields": ["first_name", "last_name"],
      "required_fields": ["first_name", "last_name"]
    }
  }
}
```

Rules for document-free workflows:

* `selfie` must be `true` — a workflow has to capture at least one artifact.
* The graph cannot contain document-dependent policies (document scan, face
  match, age verification, watchlist screening, and others that read document
  data). The policies endpoint marks these with `requires_document: true`,
  and creating or updating a workflow that combines them with
  `document.enabled: false` returns a validation error.
* The capture flow skips all document steps and goes straight from personal
  information to the selfie and liveness check.
* Billing still follows pay-on-capture-success: one IDV is billed when a
  session whose selfie passed the capture liveness gate begins processing.
* Selfie-compatible checks still work — in particular **Duplicate Face
  Detection** (`face_duplicate`), which flags a session when the same face
  has already verified under a different `external_ref` in your
  organization. Matches route to manual review by default; sessions that
  share the session's `external_ref` (the same account re-verifying) or
  have no `external_ref` never flag.

## Returning-user re-verification

A **returning-user re-verification** confirms that the person in front of the
camera is the same person you already verified — a step-up check before a
sensitive action such as a large withdrawal, a password reset, a device change,
or a high-value transaction. Instead of re-running full onboarding, the returning
user takes a single liveness-checked selfie, and the platform compares it against
that person's previously verified facial records to pass or fail the session.

This is a specialised [document-free workflow](#document-free-workflows): it
captures a selfie only, with no ID document. The comparison is one-to-one — the
new selfie against the one known person — which is the opposite of
[Duplicate Face Detection](#document-free-workflows), where a selfie is searched
one-to-many against everyone in your organization.

### Setting it up

Add the **Returning User Face Verification** (`face_reauth`) policy to a
document-free workflow, or start from the **Returning User Re-Verification**
template, which ships a ready-made graph:

```json
{
  "reauth": {
    "type": "policy",
    "policy": "face_reauth",
    "config": {
      "sensitivity": "balanced",
      "failure_action": "manual_review",
      "on_missing_enrollment": "manual_review"
    },
    "on_pass": "auto_decision",
    "on_fail": "review"
  }
}
```

The workflow must be document-free (`document.enabled: false`, `selfie: true`).
The **Returning User Face Verification** policy is available on the Starter plan
and above.

### external_ref is required

When you create the session, `external_ref` **must** be set to the identifier of
the person you want to re-verify — the same `external_ref` used when that person
was originally verified. The policy uses it to look up whose facial records to
compare against.

Because this is an authentication check rather than a fraud screen, it **fails
closed**:

* A session created with **no `external_ref`** fails and routes to review.
* A session whose `external_ref` **doesn't match anyone** you've verified fails
  and routes to review.

### Configuration

| Field                   | Values                                             | Description                                                                                                                                                                                                                                                            |
| ----------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sensitivity`           | `conservative`, `balanced` (default), `aggressive` | How close the selfie must be to the stored records to pass. `conservative` demands the strongest match (fewest false accepts, more legitimate users sent to review); `aggressive` is the most permissive; `balanced` sits between them.                                |
| `failure_action`        | `manual_review` (default), `fail`                  | What happens when the faces don't match closely enough. `manual_review` routes the session to a reviewer; `fail` ends it as failed.                                                                                                                                    |
| `on_missing_enrollment` | `manual_review` (default), `skip`, `fail`          | What happens when the person exists but has no stored facial record to compare against — for example an identity imported and marked verified through the status API. `manual_review` routes to a reviewer, `skip` passes the check through, `fail` fails the session. |

### Result and identity status

The pass/fail outcome rides the normal session result — there is no separate
event or billing SKU; a re-verification session is billed like any other
verification. A **failed re-verification never changes the person's verified
status**: the result lives on the session, and the verified identity stays
verified. You decide what to do with the outcome — block the sensitive action,
require a fallback, or send it to your own review queue — by gating on the
session result.

## Rollout percentage

Deploy workflow changes gradually with percentage-based rollout:

```json
{
  "rollout_percentage": 25
}
```

25% of new sessions will use this workflow version; the remaining 75% continue
using the previous version. The routing is deterministic per session ID, so the
same session always gets the same version.

## Default workflows

Each organization has one default workflow per environment (sandbox and live).
When a session is created without a `workflow_id`, the default workflow is used.

## Workflow API

### Create a workflow

```bash
curl -X POST https://api.withverifa.com/api/v1/workflows \
  -H "X-API-Key: vk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Standard KYC",
    "description": "Document + face match + AML screening",
    "graph": { ... }
  }'
```

### List workflows

```bash
curl https://api.withverifa.com/api/v1/workflows \
  -H "X-API-Key: vk_live_your_key_here"
```

### Publish a workflow

```bash
curl -X POST https://api.withverifa.com/api/v1/workflows/workflow_abc123/publish \
  -H "X-API-Key: vk_live_your_key_here"
```

### Set as default

```bash
curl -X POST https://api.withverifa.com/api/v1/workflows/workflow_abc123/set-default \
  -H "X-API-Key: vk_live_your_key_here"
```

## Validation

The workflow engine validates graphs before publishing:

* All nodes referenced in routing must exist
* The entry node must exist and be a `start` node
* Policy nodes must have `on_pass` and `on_fail` routes
* Conditional nodes must have at least one route with a default fallback
* No cycles (the graph must be a DAG)
* No duplicate policies (unless explicitly allowed)
* Every path must reach a terminal node

Invalid workflows cannot be published.

## Related

* [Verifications & Checks](/core-concepts/verifications-checks) — Available check
  types for workflow nodes
* [Sessions](/core-concepts/sessions) — How sessions trigger workflow execution
* [Cases](/core-concepts/cases) — What happens when a workflow routes to needs_review