
# Decisioning

Every [workflow](https://trust.myaza.co/documentation/workflows/markdown) can carry a **decision graph**: server-side logic that runs *after* a verification reaches a terminal state, branches on the verification, entity, identity, screening, and fraud data, and lands on a decision: **approve**, **decline**, or **review**. It's the difference between "the ID checked out" and "this customer is onboarded."

You build the graph in the workflow builder (a simple ordered rule list, or an advanced canvas). Decisioning is optional: a workflow with no graph simply completes and reports the raw verification result.

## How a run works

When a verification finishes, the engine starts a **run**:

1. It gathers the run's data: the verification result, the resolved [entity](https://trust.myaza.co/documentation/identity-hub/markdown) and global identity, any [screening](https://trust.myaza.co/documentation/screening/markdown) results, and the [Device Intelligence](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons) signals.
2. It walks the graph from the start node, evaluating each **branch** and executing each **action**.
3. It lands on a terminal **status** node, which sets the outcome.
4. It records the full path it took (the trace) and fires the [`workflow.run.completed`](#the-workflowruncompleted-webhook) webhook.

Runs are one-per-verification and idempotent, so a retried or re-processed verification never decides twice.

## Node types

| Node | What it does |
|---|---|
| **Branch** | Evaluates a condition over the [field namespace](#field-reference) and takes the `true` or `false` edge. |
| **Action** | A side effect: **tag** the run, fire a `workflow.action` **webhook**, or **open a case** for an investigator. Actions never stop the flow. |
| **Screening / key-people** | Waits for AML screening (or KYB key-people verification) to resolve before continuing, up to a timeout. |
| **Status** | Terminal. Sets the outcome: `approve`, `decline`, or `review`. |

## Outcomes

A status node's outcome writes the **entity's disposition** (the compliance decision on the person or business), but never overrides the verification's own result:

| Outcome | Entity disposition | Meaning |
|---|---|---|
| `approve` | `APPROVED` | Onboard the customer. |
| `decline` | `REJECTED` | Reject the customer. |
| `review` | `UNDER_REVIEW` | A human should look before you act. |

The outcome arrives on the [`workflow.run.completed`](#the-workflowruncompleted-webhook) webhook. What you *do* with it (grant access, hold the account, notify your ops team) is up to your backend.

Completion and its notification intent are committed together. Delivery retries
reuse the event ID and do not reapply an old disposition over a later decision.
Use `externalUserId` to correlate the customer, deduplicate the signed event and
read current state before lifting a restriction. Face-reuse findings arrive after
verification and can start a separate signal policy; see
[verification device and face findings](/documentation/webhook-risk-intelligence#verification-device-and-face-findings).

## Field reference

Branch conditions read a **closed namespace** of fields. Unknown paths are rejected when you publish (so a typo surfaces immediately rather than silently never matching), and a null value fails the comparison closed. Use the `exists` operator to test presence.

Operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `exists`. Conditions combine with `all` (AND), `any` (OR), and `not`.

**`verification.*`**: the check result

| Field | Example values |
|---|---|
| `verification.status` | `VERIFIED`, `FAILED`, `NOT_FOUND`, `ERROR` |
| `verification.reasonCode` | `selfie_mismatch`, `document_expired`, … (see [Errors](https://trust.myaza.co/documentation/errors/markdown)) |
| `verification.assuranceLevel` | `chip`, `gov_db`, `document` (how strongly it passed) |
| `verification.country` / `.idType` / `.nationality` / `.gender` | — |
| `verification.dataMatch` / `.facialMatch` | `true` / `false` |
| `verification.nameMatch` / `.dateOfBirthMatch` | `true` / `false`, null when not compared. Read these when your workflow records identity mismatches instead of failing on them |
| `verification.facialConfidence` | `0`–`100` |
| `verification.facialMatchSource` | `gov_record`, `chip`, `document`: which photo the selfie was compared with, null when no comparison ran. A `document` match used the photo printed on the ID, which is weaker evidence |
| `verification.documentPhotoNoFace` | `true` when the photo printed on the document was the only photo to compare the selfie with, no face could be read on it, and your workflow sends that case to review. Null unless the checks passed |
| `verification.age` | integer, when the date of birth is known |
| `verification.documentExpired` / `.documentExpiresInDays` | — |
| `verification.cameraSuspect` | `true` when capture-integrity signals flag injection |
| `verification.poaStatus` / `.poaNameMatch` / `.poaDocumentAgeDays` | Proof-of-Address result |
| `verification.nfcStatus` / `.nfcAuthentic` / `.nfcDataMatch` | NFC chip result |

**`entity.*` / `identity.*`**: the person across your org and the network

`entity.exists`, `entity.riskTier` (`LOW`/`MEDIUM`/`HIGH`), `entity.riskScore`, `entity.disposition`, `entity.status`, `entity.kycProvenance`, `identity.exists`, `identity.trustState` (`VERIFIED`/`FLAGGED`/`UNVERIFIED`, the cross-org network signal).

**`screening.*`**: sanctions / PEP / adverse media / crypto wallets

`screening.active`, `screening.pending`, `screening.anyMatch`, `screening.confirmed`, `screening.sanctions.status`, `screening.pep.status`, `screening.adverseMedia.status`, `screening.wallet.status` (null when no wallets are attached), `screening.maxMatchScore`.

**`device.*` / `ip.*`**: Device Intelligence

`device.matchedEntities` (multi-accounting count), `device.emulator`, `ip.country`, `ip.type`, `ip.datacenter`, `ip.countryMismatch`, `ip.velocity1h`.

**`email.*` / `phone.*`**: contact verification

`email.verified`, `email.disposable`, `email.freeProvider`, `email.domain`, `phone.verified`, `phone.country`, `phone.countryMismatch`, `phone.lineType` (`mobile`, `fixed_line`, `voip`, `toll_free`, and similar; null when the number range is unknown), `phone.virtual` (true only for a known VoIP range), `phone.matchedVerifications` (how many other verifications in your organisation verified the same number, a soft reuse signal) and `phone.attempts` (codes entered before the one that verified).

**`business.*` / `keyPeople.*`**: KYB

`business.companyStatus`, `business.nameMatch`, `business.addressMatch`, `business.ageYears`, `keyPeople.anyFlagged`, `keyPeople.allCleared`, `keyPeople.cddComplete`, `keyPeople.kycComplete`, `keyPeople.kycFailed`, `keyPeople.corporate`, `keyPeople.ownershipUnresolved`, and more, for [business workflows](https://trust.myaza.co/documentation/workflows/markdown#business-kyb-workflows). The key-people roll-up is listed in full in [Key people & ownership](https://trust.myaza.co/documentation/key-people/markdown#what-you-receive).

**`questionnaire.<key>`**: the answers to any [questionnaire](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons) the flow asks, keyed by question.

## The `workflow.run.completed` webhook

When a run reaches a terminal outcome, you receive:

```json
{
  "event": "workflow.run.completed",
  "data": {
    "runId": "wfr_01j9xyz…",
    "workflowId": "wf_AbC123dEf456",
    "workflowVersion": 3,
    "verificationId": "ver_01j9xyz456",
    "entityId": "ent_01j9…",
    "outcome": "approve",
    "tags": ["network-verified"],
    "environment": "PRODUCTION",
    "completedAt": "2026-07-12T12:00:00.000Z"
  }
}
```

Related events:

- **`workflow.action`**: fired by a `webhook` action node mid-graph (`{ runId, workflowId, verificationId, entityId, nodeId, note }`).
- **`workflow.run.failed`**: the engine could not finish a run (a broken graph or crash loop). This is an engine fault, **not** a decline, so never treat it as one.
- **`verification.status_updated`**: fires alongside `workflow.run.completed`, with `source: "workflow"`, whenever the verdict moves the verification's top-line status (for example from `processing` to `in_review`). The same event covers a decision made by a reviewer or by your backend, so one handler can keep your record current whoever decided. See [Status changes](https://trust.myaza.co/documentation/webhooks/markdown#status-changes).

See [Webhooks](https://trust.myaza.co/documentation/webhooks/markdown) for signing, retries, and the full event list.

## Notes

- The graph is **immutable per run**: a run is pinned to the workflow version and graph snapshot it started with, so re-publishing never changes an in-flight decision.
- A **screening** node waits only for the *first* resolution; later re-screens are handled by ongoing [monitoring](https://trust.myaza.co/documentation/monitoring-rules/markdown), not re-decisioning.
- For KYB, the key-people gate holds the **decision**, never the business verification itself. The registry result is terminal the moment it's done; the onboarding decision waits for the people.
