Decisioning

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:

  1. It gathers the run's data: the verification result, the resolved entity and global identity, any screening results, and the Device Intelligence 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 webhook.

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

Node types

NodeWhat it does
BranchEvaluates a condition over the field namespace and takes the true or false edge.
ActionA side effect: tag the run, fire a workflow.action webhook, or open a case for an investigator. Actions never stop the flow.
Screening / key-peopleWaits for AML screening (or KYB key-people verification) to resolve before continuing, up to a timeout.
StatusTerminal. 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:

OutcomeEntity dispositionMeaning
approveAPPROVEDOnboard the customer.
declineREJECTEDReject the customer.
reviewUNDER_REVIEWA 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

FieldExample values
verification.statusVERIFIED, FAILED, NOT_FOUND, ERROR
verification.reasonCodeselfie_mismatch, document_expired, … (see Errors)
verification.assuranceLevelchip, gov_db, document (how strongly it passed)
verification.country / .idType / .nationality / .gender
verification.dataMatch / .facialMatchtrue / false
verification.nameMatch / .dateOfBirthMatchtrue / false, null when not compared. Read these when your workflow records identity mismatches instead of failing on them
verification.facialConfidence0100
verification.facialMatchSourcegov_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.documentPhotoNoFacetrue 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.ageinteger, when the date of birth is known
verification.documentExpired / .documentExpiresInDays
verification.cameraSuspecttrue when capture-integrity signals flag injection
verification.poaStatus / .poaNameMatch / .poaDocumentAgeDaysProof-of-Address result
verification.nfcStatus / .nfcAuthentic / .nfcDataMatchNFC 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:

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.

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.