Decisioning
Build with

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
Build with

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 — a retried or re-processed verification never decides twice.

Node types
Build with

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
Build with

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.

Field reference
Build with

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.facialConfidence0100
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.

business.* / keyPeople.* — KYB

business.companyStatus, business.nameMatch, business.addressMatch, business.ageYears, keyPeople.anyFlagged, keyPeople.allCleared, keyPeople.kycComplete, keyPeople.kycFailed, and more — for business workflows.

questionnaire.<key> — the answers to any questionnaire the flow asks, keyed by question.

The workflow.run.completed webhook
Build with

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 — never treat it as one.

See Webhooks for signing, retries, and the full event list.

Notes
Build with

  • 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.