On this page
Every workflow 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:
- It gathers the run's data: the verification result, the resolved entity and global identity, any screening results, and the Device Intelligence signals.
- It walks the graph from the start node, evaluating each branch and executing each action.
- It lands on a terminal status node, which sets the outcome.
- It records the full path it took (the trace) and fires the
workflow.run.completedwebhook.
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 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 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.
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) |
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. The key-people roll-up is listed in full in Key people & ownership.
questionnaire.<key>: the answers to any questionnaire the flow asks, keyed by question.
The workflow.run.completed webhook
When a run reaches a terminal outcome, you receive:
{
"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 awebhookaction 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 alongsideworkflow.run.completed, withsource: "workflow", whenever the verdict moves the verification's top-line status (for example fromprocessingtoin_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.
See Webhooks 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, 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.