Workflow Events
Workflow Events
Workflow events notify you about the lifecycle of Workflow Builder runs in your organization — when a run starts, when individual nodes complete, when contact resolution succeeds or fails, when a workflow is published, and when a run reaches a terminal state.
All events follow the standard webhook envelope (event, eventId, eventTimestamp, data). The envelope-level eventTimestamp records when the event was delivered to the envelope and is always present. Inside data, every workflow event carries recordId and organizationId, plus a stable set of correlation fields — the parent workflowId, the executing workflowVersionId, the candidateProfileId, your workflowExternalId and candidateProfileExternalId, and a timestamp recording when the event was emitted. A few of these correlation fields are intentionally omitted on specific event types — those exceptions are noted in the table below.
Available events
| Event | recordId is | Description |
|---|---|---|
WORKFLOW_RUN_STARTED | Workflow run ID | A workflow run has begun executing |
WORKFLOW_RUN_COMPLETED | Workflow run ID | A workflow run reached the end of its graph successfully |
WORKFLOW_RUN_FAILED | Workflow run ID | A workflow run failed and could not continue |
WORKFLOW_RUN_CANCELLED | Workflow run ID | A workflow run was cancelled (via API or by archiving the workflow) |
WORKFLOW_NODE_COMPLETED | Workflow run ID | A node in a run completed and produced an outcome |
WORKFLOW_CONTACT_RESOLVED | Workflow run ID | An inline contact was resolved to or created as a candidate profile |
WORKFLOW_CONTACT_RESOLUTION_FAILED | A stable contact dedup key | An inline contact could not be resolved into a candidate profile |
WORKFLOW_CONTACT_RESOLUTION_SKIPPED | The existing workflow run ID | An inline contact was skipped because they already had an active run |
WORKFLOW_PUBLISHED | Workflow ID | A workflow was successfully published |
Common correlation fields
Every workflow event's data block contains the following fields in addition to the per-event payload below:
| Field | Type | Description |
|---|---|---|
recordId | string | Identifier of the primary record for this event (see "Available events" above) |
organizationId | string | Your organization ID |
workflowId | string | null | Identifier of the parent workflow (absent on WORKFLOW_PUBLISHED — the workflow ID is in recordId) |
workflowVersionId | string | null | Identifier of the workflow version that produced this event (absent on WORKFLOW_CONTACT_RESOLUTION_FAILED and WORKFLOW_CONTACT_RESOLUTION_SKIPPED) |
workflowExternalId | string | null | Caller-supplied external identifier on the workflow, when set |
candidateProfileId | string | null | Identifier of the candidate profile (absent on WORKFLOW_CONTACT_RESOLUTION_FAILED and WORKFLOW_PUBLISHED) |
candidateProfileExternalId | string | null | Caller-supplied external identifier on the candidate profile, when set on the run-lifecycle events (WORKFLOW_RUN_* and WORKFLOW_NODE_COMPLETED). Not carried on the four contact-resolution / publish events — WORKFLOW_CONTACT_RESOLVED, WORKFLOW_CONTACT_RESOLUTION_SKIPPED, WORKFLOW_CONTACT_RESOLUTION_FAILED, or WORKFLOW_PUBLISHED (those events expose the contact's externalId directly in their per-event payload instead) |
timestamp | string | null | ISO 8601 timestamp recording when the event was emitted. Carried on the run-lifecycle events (WORKFLOW_RUN_* and WORKFLOW_NODE_COMPLETED); absent on the four contact-resolution / publish events. Use the envelope-level eventTimestamp if you need a guaranteed timestamp on every event. |
The per-event sections below show the additional fields each event type carries.
Node outcomes
Run-terminal events (WORKFLOW_RUN_COMPLETED, WORKFLOW_RUN_FAILED, WORKFLOW_RUN_CANCELLED) and WORKFLOW_NODE_COMPLETED carry a NodeOutcome shape that describes what each node produced. This is the canonical place to read what happened in a run.
{
"nodeId": "screen-availability",
"nodeType": "SCREENING_OUTREACH",
"enteredAt": "2026-04-28T10:30:00.000Z",
"completedAt": "2026-04-28T10:42:11.000Z",
"routingEvent": "onComplete",
"outcome": {
"kind": "conversation",
"conversationId": "conv_abc123",
"status": "PASSED_CRITERIA",
"completedAt": "2026-04-28T10:42:11.000Z",
"answers": [
{
"slug": "nmc-pin",
"answer": "Yes I'm registered",
"answerState": "IS_YES",
"trafficLight": "GREEN",
"outcome": "PASSED",
"score": 100
}
]
}
}NodeOutcome fields
| Field | Type | Description |
|---|---|---|
nodeId | string | Identifier of the node within the workflow configuration |
nodeType | string | The workflow node type. One of: SCREENING_OUTREACH, ENGAGEMENT_OUTREACH, CONDITION |
enteredAt | string (date-time) | When execution entered this node |
completedAt | string (date-time) | When execution left this node |
routingEvent | string | The routing event the node emitted (e.g. onComplete, onConditionResolved) |
outcome.kind | "conversation", "condition", "empty" | Discriminator for the per-node-type payload |
Conversation outcome (outcome.kind: "conversation")
outcome.kind: "conversation")Emitted by screeningOutreach and engagementOutreach nodes.
| Field | Type | Description |
|---|---|---|
conversationId | string | Messaging service conversation ID — correlate with the Conversations API |
status | string (enum) | Workflow-domain summary of how the conversation ended (see outcome enum) |
completedAt | string (date-time) | When the conversation completed (optional) |
answers | array | Per-question answers given under this node, keyed by question slug. Empty when no questions were answered (e.g. opted out before any reply). |
WorkflowOutreachOutcome enum
WorkflowOutreachOutcome enumThe conversation status field uses a workflow-domain enum that is intentionally separate from the public Conversations API's conversationStatus — the workflow domain owns its own outcome contract. Correlate the two by conversationId, not by status string.
| Value | Meaning |
|---|---|
PASSED_CRITERIA | The candidate met the screening criteria the conversation was designed to test for. |
FAILED_CRITERIA | The candidate did not meet the screening criteria. |
COMPLETED | The conversation ran its course or was closed without a specific pass/fail signal. |
NOT_INTERESTED | The candidate explicitly declined. |
OPTED_OUT | The candidate opted out (e.g. SMS STOP). |
DELIVERY_FAILED | Delivery failed across the available channels. |
Answer fields
Each entry in answers[] represents a single question outcome:
| Field | Type | Description |
|---|---|---|
slug | string | Stable question identifier from the workflow definition |
answer | string | null | Raw answer text from the candidate |
answerState | string | null | Classification of the answer by the conversation engine. One of: IS_YES, IS_NO, IS_ANY, INCLUDES, DOES_NOT_INCLUDE, DOCUMENT_UPLOADED, DOCUMENT_NOT_UPLOADED, NEEDS_CLARIFICATION |
trafficLight | string | null | Traffic-light evaluation of the answer. One of: GREEN, AMBER, RED |
outcome | string | null | Final outcome of the question. One of: PASSED, FAILED, NOT_COMPLETED, NEEDS_REVIEW, PENDING |
score | number | null | Preferred-answer score as integer percentage 0–100 (only set when the question had hasPreferredAnswer: true) |
documentS3Key | string | null | S3 key of the uploaded document (when the question accepted a document and was answered over email) |
documentTwilioMediaSid | string | null | Twilio media SID of the uploaded document (WhatsApp/SMS channel) |
classifiedDocumentType | string | null | Document type classified by the conversation engine when a document was uploaded (e.g. cv, passport, dbs-certificate) |
Condition outcome (outcome.kind: "condition")
outcome.kind: "condition")Emitted by condition nodes.
| Field | Type | Description |
|---|---|---|
targetNodeId | string | null | Identifier of the node selected by the matched branch (or by else) |
targetNodeLabel | string | null | Display label of the selected node, when available |
The expression that produced the match is intentionally not surfaced — workflow definitions are private.
Empty outcome (outcome.kind: "empty")
outcome.kind: "empty")Reserved for forward-compatibility. None of the node types available today (screeningOutreach, engagementOutreach, condition) produce empty under normal conditions — your runtime parser should accept it but you should not expect to see it. It exists to (a) leave room for future node types that do not carry structured outcome data (e.g. timed waits) and (b) act as a defensive fallback if the engine cannot resolve outcome data for a node it traversed. Payload carries only the kind discriminator.
Run-level outcomes
Run-terminal events (WORKFLOW_RUN_COMPLETED, WORKFLOW_RUN_FAILED, WORKFLOW_RUN_CANCELLED) carry a stable outcome field that summarises how the run ended:
| Value | Meaning |
|---|---|
completed | The run reached the end of its graph successfully |
opted_out | The run terminated because the candidate opted out |
delivery_failed | The run terminated because delivery failed across the available channels |
cancelled | The run was cancelled (via API or by archiving the workflow) |
failed | The run failed with an unrecoverable error |
Branch on this enum rather than parsing free-text — values are stable.
WORKFLOW_RUN_STARTED
Triggered when a workflow run begins executing.
Payload
{
"event": "WORKFLOW_RUN_STARTED",
"eventId": "evt_abc123",
"eventTimestamp": "2026-04-28T10:30:00.000Z",
"data": {
"recordId": "run_xyz789",
"organizationId": "org_123456",
"workflowId": "wf_abc123",
"workflowVersionId": "wfv_def456",
"workflowExternalId": "cedar-rgn-screening",
"candidateProfileId": "cp_abc123",
"candidateProfileExternalId": "ats_42",
"timestamp": "2026-04-28T10:30:00.000Z"
}
}This event carries no fields beyond the common correlation fields.
WORKFLOW_RUN_COMPLETED
Triggered when a workflow run reaches the end of its graph successfully. Carries the full traversal in nodes[].
Payload
{
"event": "WORKFLOW_RUN_COMPLETED",
"eventId": "evt_abc123",
"eventTimestamp": "2026-04-28T10:45:00.000Z",
"data": {
"recordId": "run_xyz789",
"organizationId": "org_123456",
"workflowId": "wf_abc123",
"workflowVersionId": "wfv_def456",
"workflowExternalId": "cedar-rgn-screening",
"candidateProfileId": "cp_abc123",
"candidateProfileExternalId": "ats_42",
"timestamp": "2026-04-28T10:45:00.000Z",
"startedAt": "2026-04-28T10:30:00.000Z",
"completedAt": "2026-04-28T10:45:00.000Z",
"finalNodeId": "engagement-booking-confirm",
"outcome": "completed",
"truncated": false,
"nodes": [
{
"nodeId": "screen-availability",
"nodeType": "SCREENING_OUTREACH",
"enteredAt": "2026-04-28T10:30:00.000Z",
"completedAt": "2026-04-28T10:42:11.000Z",
"routingEvent": "onComplete",
"outcome": {
"kind": "conversation",
"conversationId": "conv_abc123",
"status": "PASSED_CRITERIA",
"completedAt": "2026-04-28T10:42:11.000Z",
"answers": [
{
"slug": "nmc-pin",
"answer": "Yes I'm registered",
"answerState": "IS_YES",
"trafficLight": "GREEN",
"outcome": "PASSED",
"score": 100
}
]
}
},
{
"nodeId": "engagement-booking-confirm",
"nodeType": "ENGAGEMENT_OUTREACH",
"enteredAt": "2026-04-28T10:42:11.000Z",
"completedAt": "2026-04-28T10:45:00.000Z",
"routingEvent": "onComplete",
"outcome": {
"kind": "conversation",
"conversationId": "conv_def456",
"status": "COMPLETED",
"completedAt": "2026-04-28T10:45:00.000Z",
"answers": []
}
}
]
}
}Additional fields
| Field | Type | Description |
|---|---|---|
startedAt | string | ISO 8601 timestamp when the run began executing |
completedAt | string | ISO 8601 timestamp when the run reached its terminal state |
finalNodeId | string | Identifier of the last node the run traversed (optional) |
outcome | string | Run-level outcome — see Run-level outcomes |
truncated | boolean | True when the nodes[] traversal was truncated due to size limits |
nodes | array | Ordered list of NodeOutcome objects describing each step |
WORKFLOW_RUN_FAILED
Triggered when a workflow run cannot continue due to an unrecoverable error. The traversal up to and including the failing node is included in nodes[]. Failure detail beyond errorCode and lastFailedNodeId is intentionally not surfaced — raw exception text may carry internal IDs and is not safe to expose to customer endpoints.
Payload
{
"event": "WORKFLOW_RUN_FAILED",
"eventId": "evt_abc123",
"eventTimestamp": "2026-04-28T10:45:00.000Z",
"data": {
"recordId": "run_xyz789",
"organizationId": "org_123456",
"workflowId": "wf_abc123",
"workflowVersionId": "wfv_def456",
"workflowExternalId": "cedar-rgn-screening",
"candidateProfileId": "cp_abc123",
"candidateProfileExternalId": "ats_42",
"timestamp": "2026-04-28T10:45:00.000Z",
"startedAt": "2026-04-28T10:30:00.000Z",
"completedAt": "2026-04-28T10:45:00.000Z",
"finalNodeId": "screen-availability",
"outcome": "failed",
"errorCode": "DOWNSTREAM_SERVICE_ERROR",
"lastFailedNodeId": "screen-availability",
"truncated": false,
"nodes": [
{
"nodeId": "screen-availability",
"nodeType": "SCREENING_OUTREACH",
"enteredAt": "2026-04-28T10:30:00.000Z",
"completedAt": "2026-04-28T10:45:00.000Z",
"routingEvent": "onDeliveryFailed",
"outcome": {
"kind": "conversation",
"conversationId": "conv_abc123",
"status": "DELIVERY_FAILED",
"completedAt": "2026-04-28T10:45:00.000Z",
"answers": []
}
}
]
}
}Additional fields
| Field | Type | Description |
|---|---|---|
errorCode | string | Stable failure category. See values below. |
errorClass | string | Coarser-grained classifier than errorCode — DATA or SYSTEM. Optional; present only when the run failed via the retry-exhausted infrastructure path (typically alongside errorCode: "SYSTEM_FAILURE"). Lets consumers branch retry-vs-give-up without parsing errorCode. Absent on in-engine node-executor failures. |
lastFailedNodeId | string | Identifier of the node where the run failed (optional) |
errorCode values
errorCode values| Value | Meaning |
|---|---|
EXECUTOR_TIMEOUT | A node exceeded its execution time limit (e.g. waiting for a candidate reply past the configured window). |
INVALID_NODE_CONFIG | Runtime detected a node configuration that is invalid for execution. Usually indicates a workflow-version issue that publish-time validation did not catch. |
DOWNSTREAM_SERVICE_ERROR | A downstream service the engine relies on (messaging, scheduling, analysis) returned an error and the run could not continue. |
EXECUTOR_FAILED | A node executor failed for a reason that does not fit the more specific timeout / config / downstream classes. Investigate via lastFailedNodeId. |
SYSTEM_FAILURE | Retry was exhausted on a transient infrastructure-class issue. Typically emitted with errorClass: "SYSTEM" — safe to retry the run via the API once the underlying issue is resolved. |
Plus the run-terminal fields (startedAt, completedAt, finalNodeId, outcome, truncated, nodes).
WORKFLOW_RUN_CANCELLED
Triggered when a workflow run is cancelled — via the Cancel Workflow Run endpoint or as a side effect of archiving the parent workflow.
Payload
{
"event": "WORKFLOW_RUN_CANCELLED",
"eventId": "evt_abc123",
"eventTimestamp": "2026-04-28T10:45:00.000Z",
"data": {
"recordId": "run_xyz789",
"organizationId": "org_123456",
"workflowId": "wf_abc123",
"workflowVersionId": "wfv_def456",
"workflowExternalId": "cedar-rgn-screening",
"candidateProfileId": "cp_abc123",
"candidateProfileExternalId": "ats_42",
"timestamp": "2026-04-28T10:45:00.000Z",
"startedAt": "2026-04-28T10:30:00.000Z",
"completedAt": "2026-04-28T10:45:00.000Z",
"finalNodeId": "screen-availability",
"outcome": "cancelled",
"cancellationReason": "Candidate withdrew via ATS",
"truncated": false,
"nodes": [
{
"nodeId": "screen-availability",
"nodeType": "SCREENING_OUTREACH",
"enteredAt": "2026-04-28T10:30:00.000Z",
"completedAt": "2026-04-28T10:45:00.000Z",
"routingEvent": "onComplete",
"outcome": {
"kind": "conversation",
"conversationId": "conv_abc123",
"status": "COMPLETED",
"completedAt": "2026-04-28T10:45:00.000Z",
"answers": [
{
"slug": "nmc-pin",
"answer": "Yes I'm registered",
"answerState": "IS_YES",
"trafficLight": "GREEN",
"outcome": "PASSED",
"score": 100
}
]
}
}
]
}
}Additional fields
| Field | Type | Description |
|---|---|---|
cancellationReason | string | Reason supplied to the Cancel endpoint, when one was provided |
Plus the run-terminal fields.
WORKFLOW_NODE_COMPLETED
Triggered when a node in a workflow run completes and produces an outcome. Use this event for fine-grained progress tracking; for end-of-run summaries, prefer the run-terminal events which include the full traversal.
Payload
{
"event": "WORKFLOW_NODE_COMPLETED",
"eventId": "evt_abc123",
"eventTimestamp": "2026-04-28T10:42:11.000Z",
"data": {
"recordId": "run_xyz789",
"organizationId": "org_123456",
"workflowId": "wf_abc123",
"workflowVersionId": "wfv_def456",
"workflowExternalId": "cedar-rgn-screening",
"candidateProfileId": "cp_abc123",
"candidateProfileExternalId": "ats_42",
"timestamp": "2026-04-28T10:42:11.000Z",
"nodeId": "screen-availability",
"nodeType": "SCREENING_OUTREACH",
"enteredAt": "2026-04-28T10:30:00.000Z",
"completedAt": "2026-04-28T10:42:11.000Z",
"routingEvent": "onComplete",
"outcome": {
"kind": "conversation",
"conversationId": "conv_abc123",
"status": "PASSED_CRITERIA",
"completedAt": "2026-04-28T10:42:11.000Z",
"answers": [
{
"slug": "nmc-pin",
"answer": "Yes I'm registered",
"answerState": "IS_YES",
"trafficLight": "GREEN",
"outcome": "PASSED",
"score": 100
}
]
}
}
}Additional fields
The per-node fields (nodeId, nodeType, enteredAt, completedAt, routingEvent, outcome) are flat on the event payload — they are not wrapped in a node object. The shape is otherwise identical to a single entry in a run-terminal event's nodes[] array, so consumers can reuse the same parser. Receivers subscribed to both per-node and run-terminal events should deduplicate by nodeId and the run's recordId.
WORKFLOW_CONTACT_RESOLVED
Triggered when an inline contact supplied to Start Workflow Runs is resolved to or created as a candidate profile, ahead of the run starting. The recordId is the new run's ID, so this event correlates with the matching WORKFLOW_RUN_STARTED that follows.
Payload
{
"event": "WORKFLOW_CONTACT_RESOLVED",
"eventId": "evt_abc123",
"eventTimestamp": "2026-04-28T10:29:55.000Z",
"data": {
"recordId": "run_xyz789",
"organizationId": "org_123456",
"workflowId": "wf_abc123",
"workflowExternalId": "cedar-rgn-screening",
"workflowVersionId": "wfv_def456",
"versionNumber": 4,
"candidateProfileId": "cp_abc123",
"externalId": "ats_42",
"email": "[email protected]",
"phoneNumber": "+447700900000",
"isNewProfile": false,
"contactIndex": 0
}
}Additional fields
| Field | Type | Description |
|---|---|---|
versionNumber | integer | Numeric version of the workflow this run will execute against |
externalId | string | null | The external identifier supplied with the inline contact (when set) |
email | string | null | Contact email |
phoneNumber | string | null | Contact phone in E.164 format |
isNewProfile | boolean | True when a new candidate profile was created; false when an existing profile matched |
contactIndex | integer | Zero-based index of this contact within the original Start Workflow Runs request |
WORKFLOW_CONTACT_RESOLUTION_SKIPPED
Triggered when an inline contact resolves to a candidate who already has an active run on this workflow version. The duplicate run is not started; the event's recordId points at the existing run so consumers can correlate.
Payload
{
"event": "WORKFLOW_CONTACT_RESOLUTION_SKIPPED",
"eventId": "evt_abc123",
"eventTimestamp": "2026-04-28T10:29:55.000Z",
"data": {
"recordId": "run_existing_xyz",
"organizationId": "org_123456",
"workflowId": "wf_abc123",
"workflowExternalId": "cedar-rgn-screening",
"candidateProfileId": "cp_abc123",
"externalId": "ats_42",
"email": "[email protected]",
"phoneNumber": "+447700900000",
"existingRunStatus": "RUNNING",
"contactIndex": 0
}
}Additional fields
| Field | Type | Description |
|---|---|---|
existingRunStatus | string | Status of the run that already exists for this candidate (RUNNING or COMPLETED) |
externalId | string | null | External identifier supplied with the inline contact |
email | string | null | Contact email |
phoneNumber | string | null | Contact phone in E.164 format |
contactIndex | integer | Zero-based index of this contact within the original Start Workflow Runs request |
WORKFLOW_CONTACT_RESOLUTION_FAILED
Triggered when an inline contact cannot be resolved into a candidate profile after the start-run queue retry budget is exhausted. The recordId is a stable contact dedup key (not a workflow run ID — no run was created). The detail-type itself communicates the failure; raw error text is intentionally not surfaced.
Payload
{
"event": "WORKFLOW_CONTACT_RESOLUTION_FAILED",
"eventId": "evt_abc123",
"eventTimestamp": "2026-04-28T10:29:55.000Z",
"data": {
"recordId": "contact_dedup_abc123",
"organizationId": "org_123456",
"workflowId": "wf_abc123",
"workflowExternalId": "cedar-rgn-screening",
"externalId": "ats_42",
"email": "invalid@example",
"phoneNumber": null,
"contactIndex": 0
}
}Additional fields
| Field | Type | Description |
|---|---|---|
externalId | string | null | External identifier supplied with the inline contact |
email | string | null | Contact email |
phoneNumber | string | null | Contact phone in E.164 format |
contactIndex | integer | Zero-based index of this contact within the original Start Workflow Runs request |
This event does not carry candidateProfileId, candidateProfileExternalId, or workflowVersionId — no candidate profile was resolved and the failure may have occurred outside the scope of a specific version.
WORKFLOW_PUBLISHED
Triggered when a workflow is successfully published — the working draft becomes the new live version. The recordId is the workflow ID. Use this to mirror workflow versions into your own audit log.
Payload
{
"event": "WORKFLOW_PUBLISHED",
"eventId": "evt_abc123",
"eventTimestamp": "2026-04-28T09:00:00.000Z",
"data": {
"recordId": "wf_abc123",
"organizationId": "org_123456",
"workflowExternalId": "cedar-rgn-screening",
"workflowVersionId": "wfv_def456",
"versionNumber": 4,
"publishedByUserId": "user_abc123",
"publishedAt": "2026-04-28T09:00:00.000Z"
}
}Additional fields
| Field | Type | Description |
|---|---|---|
versionNumber | integer | Numeric version assigned to the new published version |
publishedByUserId | string | Identifier of the user that published this version |
publishedAt | string | ISO 8601 timestamp when the version became live |
This event does not carry candidateProfileId, candidateProfileExternalId, or workflowId (the workflow ID is in recordId).
What's supported
- Workflow runs are triggered via the API:
POST /v1/workflows/{workflowId}/runswithtriggerSource: "API". - Three node types are available:
screeningOutreach,engagementOutreach, andcondition. See the Workflow Configuration guide. - Failures surface via
WORKFLOW_RUN_FAILED'slastFailedNodeIdanderrorCodefields — there is no separate per-node failure event.
Reliability
- Workflow webhooks follow the same delivery model as other Popp webhooks: HMAC-SHA256 signature verification (see Authentication), at-least-once delivery with retries, and a dead-letter queue on persistent failure.
- Use
eventIdfor idempotency — events may be redelivered. - Run-terminal events (
WORKFLOW_RUN_COMPLETED/_FAILED/_CANCELLED) are the canonical record of what happened in a run. Per-node events (WORKFLOW_NODE_COMPLETED) are useful for live progress UIs but should not be the source of truth. WORKFLOW_CONTACT_RESOLUTION_FAILEDfires exactly once per terminally-failed contact, after the start-run queue retry budget is exhausted.
Example: handling workflow events
type WorkflowOutreachOutcome =
| 'PASSED_CRITERIA'
| 'COMPLETED'
| 'FAILED_CRITERIA'
| 'NOT_INTERESTED'
| 'OPTED_OUT'
| 'DELIVERY_FAILED';
type RunOutcome =
| 'completed'
| 'opted_out'
| 'delivery_failed'
| 'cancelled'
| 'failed';
interface AnswerOutcome {
slug: string;
answer?: string;
answerState?: string;
trafficLight?: string;
outcome?: string;
score?: number;
documentS3Key?: string;
documentTwilioMediaSid?: string;
classifiedDocumentType?: string;
}
interface NodeOutcome {
nodeId: string;
nodeType: 'SCREENING_OUTREACH' | 'ENGAGEMENT_OUTREACH' | 'CONDITION';
enteredAt: string;
completedAt: string;
routingEvent: string;
outcome:
| { kind: 'conversation'; conversationId: string; status: WorkflowOutreachOutcome; completedAt?: string; answers: AnswerOutcome[] }
| { kind: 'condition'; targetNodeId: string | null; targetNodeLabel: string | null }
| { kind: 'empty' };
}
interface WorkflowRunTerminalPayload {
recordId: string;
organizationId: string;
workflowId: string;
workflowVersionId: string;
workflowExternalId?: string | null;
candidateProfileId: string;
candidateProfileExternalId?: string | null;
timestamp: string;
startedAt: string;
completedAt: string;
finalNodeId?: string;
outcome: RunOutcome;
truncated: boolean;
nodes: NodeOutcome[];
errorCode?: 'EXECUTOR_TIMEOUT' | 'INVALID_NODE_CONFIG' | 'DOWNSTREAM_SERVICE_ERROR' | 'UNKNOWN';
lastFailedNodeId?: string;
cancellationReason?: string;
}
function handleWorkflowWebhook(payload: {
event: string;
eventId: string;
eventTimestamp: string;
data: any;
}) {
switch (payload.event) {
case 'WORKFLOW_RUN_STARTED':
markRunStarted(payload.data.recordId);
break;
case 'WORKFLOW_RUN_COMPLETED':
case 'WORKFLOW_RUN_FAILED':
case 'WORKFLOW_RUN_CANCELLED': {
const data = payload.data as WorkflowRunTerminalPayload;
// Branch on the stable run-level outcome enum, not on the event name.
persistRunOutcome(data.recordId, data.outcome, data.nodes);
break;
}
case 'WORKFLOW_PUBLISHED':
mirrorWorkflowVersion(payload.data.recordId, payload.data.workflowVersionId);
break;
}
}Updated about 5 hours ago