Workflow Builder
A workflow is a candidate journey you define once: outreach, screening, branching and follow-up. Popp runs it for every candidate who enters.
Build workflows visually in Popp, or through the API. The Workflow Configuration guide covers the configuration JSON, and the API reference covers the endpoints.
How workflows work
A candidate enters through an entry pathway and moves from node to node. Each node's outcome decides which node comes next, until the run finishes.
Key concepts
| Concept | Description |
|---|---|
| Workflow | Your candidate journey: its entry pathways, nodes and routing |
| Version | A published snapshot of a workflow. It never changes. |
| Entry pathway | A way candidates enter: a source, such as the API, paired with the node they start on. A workflow can have several. |
| Run | One candidate's path through a published workflow |
| Node | One part of the graph, such as a screening conversation or a branch. Its id names it within one version. |
| Slug | A node's permanent name, the same in every version of the workflow |
| Outcome | What a node produced for a run, such as a score or how a conversation ended |
| Routing | The rules that pick the next node from a node's outcome |
| Journey | The nodes a run has left, in order |
Drafts and publishing
A new workflow is an unpublished draft. Publish it to start accepting runs. Popp checks the configuration when you publish and lists every problem it finds. See Lifecycle for each workflow status.
Run lifecycle
A run's status is RUNNING until the run finishes. It finishes once, in one of three ways, and Popp sends the matching workflow event:
status | The run finished because | Webhook |
|---|---|---|
COMPLETED | It left a node, and the node's routing names no node for the event it left by. | WORKFLOW_RUN_COMPLETED |
FAILED | A node hit an error the run can't recover from. errorCode says why. | WORKFLOW_RUN_FAILED |
CANCELLED | It was cancelled with Cancel workflow run, or Popp cancelled it without contacting the candidate. cancellationCode says why Popp did. | WORKFLOW_RUN_CANCELLED |
A finished run stays finished. Only a run waiting at an analysis node can move to a newer version of the workflow. Every other run stays on its version.
An analysis node with nothing after it holds the run instead of completing it, and the run stays RUNNING there. A publish that connects a node after it can move the run on. Popp cancels the run instead, without contacting the candidate, when:
- a publish removes the analysis node (
ANALYSIS_STEP_REMOVED); - the workflow is archived (
WORKFLOW_ARCHIVED); - before the run moves on, its ATS application has been rejected or declined (
ATS_APPLICATION_REJECTED), or is in a stage or on a job the live version doesn't select (ATS_STAGE_NOT_SELECTED).
An analysis node with nothing after it has the details. Reading a run covers the fields that show where a run is.
Capabilities
Entry pathways
A candidate enters a workflow through an entry pathway: a source paired with the node the candidate starts on. A workflow can have several. Two take candidates from your own systems:
- API pathway: call
POST /v1/workflows/{workflowId}/runswith one or more candidates, by candidate profile ID, external ID or inline contact details. Each candidate gets a separate run, starting at the node the API pathway names. - ATS pathway: connect the workflow to one ATS job, and its applicants enter as they apply. Find the job's
atsJobIdwithGET /v1/ats/jobsand put it in an ATS entry pathway. To feed several vacancies, create one workflow for each job. An ATS job connected to a workflow can't also be connected to another workflow, a campaign or an analysis.
Popp also starts runs itself, from Search & Match (Popp's own sourcing pool) and from spreadsheet uploads in Popp. You can't start those runs through the API.
See Entry for the pathway shape.
Candidate tags
Inline contacts[] on POST /v1/workflows/{workflowId}/runs (and the by-external-id variant) accept tags (plain strings) and tagsObject (key/value pairs). Each tagsObject entry becomes a key:value tag (no space after the colon), added after the plain tags:
{
"contacts": [
{
"firstName": "Jane",
"lastName": "Smith",
"email": "[email protected]",
"phoneNumber": "+14155550123",
"externalId": "cand-98765",
"tags": ["nurse", "night-shift"],
"tagsObject": {
"recruiter_id": "1234567890",
"recruiter_name": "John Doe",
"recruiter_email": "[email protected]"
}
}
]
}- Tags apply only to inline
contacts[]:candidateProfileIdsandcandidateProfileExternalIdscarry no tags. Sendingtags, or atagsObjectwith entries, at the top level of the body (outsidecontacts[]) returns400 Bad Request. Anull,[]or{}there is ignored. - Both fields are optional.
nullis the same as leaving the field out. - Popp trims each tag (and each
tagsObjectkey and value) and collapses repeated whitespace, but keeps casing and punctuation. AtagsObjectentry with a blank key or a blank value produces no tag. - Tags that differ only in casing or repeated whitespace are stored once, with the first spelling sent. Other differences make separate tags:
Recruiter:Jane DoeandRecruiter: Jane Doeare two tags. - Popp drops, rather than rejects, blank tags, tags over 250 characters (for
tagsObject, the wholekey:value), and any tag that would take a contact's tags past 4,096 bytes (the UTF-8 size of the tag list as a JSON array). The run still starts. - Tags are added to your organization's tag catalog shortly after the run is created.
- A malformed shape, such as a non-string tag or
tagsObjectvalue,tagsthat is not a list, ortagsObjectthat is not an object, returns400 Bad Requestfor the whole batch, the same as a contact with neitheremailnorphoneNumber. - The run-start response does not echo
tagsortagsObject.
Node types
| Node type | configuration.nodes[].type | What it does |
|---|---|---|
| Screening outreach | screeningOutreach | Screens the candidate in a conversation over SMS, WhatsApp or email, asking a fixed set of questions. Use it to test fit: pass/fail, scoring or structured answers. |
| Engagement outreach | engagementOutreach | Starts a nurture or follow-up conversation on the same channels. Its messages can't use {{JOB_TITLE}}, {{EMPLOYER_NAME}} or {{LOCATION}}. Use it once fit is established. |
| Condition | condition | Branches the run on what it has collected so far: answers, scores or candidate attributes. |
| Analysis | analysis | Scores each applicant against requirements you write, from their CV and the screening answers on their ATS application. Needs an ATS pathway. See analysis. |
| Add to job | createAtsApplication | Adds the candidate to a stage in an ATS job, which can start a workflow, campaign or analysis connected to that job. Only runs from an ATS pathway or Search & Match can reach it. See createAtsApplication. |
| Update ATS application | updateAtsApplication | Writes the outcome to your ATS: moves the run's application to an interview stage, or rejects it with a reason. A run that did not enter through an ATS pathway skips the node and carries on. See updateAtsApplication. |
| Scheduling outreach | schedulingOutreach | Sends the candidate a booking link over SMS, WhatsApp or email, and completes when they book. Nothing can come after it. See schedulingOutreach. |
Each node emits a routing event when it completes, and you connect routing events to the next nodes. The Workflow Configuration guide covers the routing rules.
Monitoring runs
To have Popp push progress to you, subscribe to workflow events. They tell you when runs start, when nodes complete and when runs finish.
To pull progress instead, list a workflow's runs with GET /v1/workflows/{workflowId}/runs, filtered by status, candidate or candidate name. Each run in the list has its status and currentNodeSlug, but not its state or journey. It also carries the candidate's name (candidateProfileName, candidateProfileFirstName, candidateProfileLastName), so you don't need a call per run to look the candidate up.
The name fields:
- Are recorded when the run is created. If the candidate is renamed later, the run keeps the name it was enrolled under, and the
candidateNamefilter matches that name. Fetch the candidate profile for the current name. - Can be
null, on runs created before the fields existed or when the candidate profile has no name. Fall back tocandidateProfileId.
Reading a run
GET /v1/workflow-runs/{runId} returns one run: where it is, how it got there, its context and its failure detail. The API reference describes each field.
Beta. state, finishReason, cancellationCode, journey, currentNodeSlug and lastFailedNodeSlug may change before general availability.
statussays whether the run is still going or has finished.statesays what the run is doing now, such aswithCandidatewhile it waits for the candidate, oranalysedwhile it holds a score at an analysis node.statusisRUNNINGin everystatebutfinished.finishReasonandcancellationCodesay why a finished run finished.journeylists the nodes the run left, in order, each with the version it ran on and how it left. The last entry of a finished run shows where it ended. The list can be empty: a run that finished before Popp recorded journeys, or that ended before it entered a node, has no entries.currentNodeSlugandlastFailedNodeSlugname the nodes incurrentNodeIdandlastFailedNodeIdby slug.
Cancellation codes
Popp sets cancellationCode when it cancels a run itself, without contacting the candidate. It is on the run and on the WORKFLOW_RUN_CANCELLED webhook. A run cancelled with Cancel workflow run has none.
cancellationCode | Popp cancelled the run because |
|---|---|
ATS_APPLICATION_REJECTED | The run's ATS application was rejected or declined before the run moved on from an analysis node. |
ATS_STAGE_NOT_SELECTED | Before the run moved on from an analysis node, its ATS application was in a stage the live version doesn't select, or the live version no longer selects its job. |
ANALYSIS_STEP_REMOVED | A publish removed the analysis node the run was waiting at. |
WORKFLOW_ARCHIVED | The workflow was archived while the run was waiting at an analysis node. |
CANDIDATE_ERASED | The candidate's profile was erased. Popp cancels every run of an erased candidate that hasn't finished. |
Example: healthcare nurse onboarding
Cedar Health Recruitment places Registered General Nurses (RGNs) into NHS trusts and care homes. Every candidate needs an active NMC PIN, an in-date DBS, mandatory training certificates and right-to-work documents before their first shift. The firm runs one workflow for every RGN applicant its ATS sends through the API, and the workflow sends each way the screening conversation can end to its own follow-up.
- API pathway: the firm's ATS posts new RGN applicants to
POST /v1/workflows/{workflowId}/runs. - Screening outreach (availability and NMC): Popp starts an SMS conversation about NMC registration, region preference and shift availability. A candidate without a PIN is asked whether they are registering. Those who say no end the conversation early and go to a nurture list. Those who are mid-registration continue the screen.
- Branch on how the screen ended: the screening node sends each ending to a different node:
- Interested and screened: continue to a region-aware compliance node.
- Not interested or not registering: go to a long-term nurture list.
- SMS delivery failed: retry the same screening over email.
- Opted out: end the run with no further messages.
- Route by region: an answer route on the region question sends candidates whose answer mentions London to a London compliance pack, because some Greater London trusts require extra clinical references. Everyone else gets the standard pack.
- Engagement outreach (compliance pack): emails the candidate the list of documents to send back. Cedar Health's compliance team handles the reply.
- Engagement outreach (booking confirmation): once compliance is complete, a final SMS confirms the candidate is shift-ready, and the bookings team takes over.
Worked examples has two complete versions of this workflow: a linear one for a first integration, and one that branches on how the screen ended and uses answer routing.
Workflow-managed campaigns
When you publish a workflow, Popp creates a campaign for it with isWorkflowManaged: true. Start conversations on that campaign with POST /v1/workflows/{workflowId}/runs or POST /v1/workflows/by-external-id/{externalId}/runs, not the campaign or conversation APIs.
These endpoints return 400 Bad Request for a workflow-managed campaign:
POST /v1/conversations(Create conversation)POST /v1/conversations/bulk(Bulk create conversations)POST /v1/scheduling/conversations(Create scheduling conversation)PUT /v1/campaigns/{id}(Update campaign)POST /v1/campaigns/{id}/stop(Stop campaign)
You can still act inside a workflow's conversations:
POST /v1/conversations/{id}/messages: send a message alongside the agent.POST /v1/conversations/{id}/hand-to-agent: hand a human takeover back to the agent.
Read endpoints (GET /v1/campaigns, GET /v1/campaigns/{id}, GET /v1/conversations, GET /v1/conversations/{id}, and so on) return workflow-managed campaigns and their conversations as usual.
Workflow-managed analyses
When you publish a workflow with an analysis node, Popp creates an analysis for that node with isWorkflowManaged: true. Its requirements come from the workflow. To use different requirements, publish a new version of the workflow (POST /v1/workflows/{workflowId}/publish). Candidates enter it only through workflow runs.
These endpoints return 400 Bad Request for a workflow-managed analysis:
POST /v1/analysis/candidates(Create analysis candidate)PUT /v1/analyses/{id}(Update analysis)POST /v1/analyses/{id}/archive(Archive analysis), even if the analysis is already archivedPOST /v1/analyses/{id}/update-requirements-and-reanalyse(Update requirements and reanalyse)
GET /v1/analyses doesn't list workflow-managed analyses. GET /v1/analyses/{id} still returns one, with its workflowNodeSlug.
Reading a workflow analysis
Beta. The workflow analysis routes below may change their fields and behaviour before general availability.
Each analysis node keeps one analysis across every version of the workflow, named by the node's slug.
When a published version changes the node's requirements, Popp re-scores every application the node has scored. Until the workflow acts on a run's score, a re-score updates the score the run holds. It moves the run on only when the run is held at the analysis node and its version now connects a node after it. For example, when the publish that connects a node after an analysis node with nothing after it also changes the requirements, each held run that moves to that version moves on once its re-score arrives.
| Route | Returns |
|---|---|
GET /v1/workflows/{workflowId}/analyses/{workflowNodeSlug} | The node's analysis: its label, whether the live version still has the node (isActive), the counts, the score bands, each requirement with its met and not-met counts, and whether a re-score is running (isRecalculatingScores) |
GET /v1/workflows/{workflowId}/analyses/{workflowNodeSlug}/applications | A page of the node's applications, one row per application with its effective score |
GET /v1/workflows/{workflowId}/analyses/{workflowNodeSlug}/applications/{candidateId} | One application: its effective score, each requirement outcome with its evidence, and when the workflow acted on the score |
GET /v1/workflows/{workflowId}/analyses/{workflowNodeSlug}/applications/{candidateId}/history | The application's score history, oldest first |
- The effective score. Until the workflow acts on a score, the effective score is the most recent score on the newest requirements. Once the workflow acts, the application keeps that score, and
analysisOutcomeActedOnAtsays when. The workflow acts when a run enters the first outreach, scheduling, add-to-ATS-job or update-ATS-application node after the analysis node, even if that node then fails. A run that left the analysis node byonAnalysisFailedacts on the failure the same way: from then on the application has no score, and later re-scores appear only in its history. A run held at an analysis node with nothing after it hasn't acted, so its application keeps taking the newest score. - History. Popp records every score when it is produced and never changes it afterwards, except by erasure. Each entry's
triggerisSCORED(the first score),RESCORED(a re-score that became the effective score),RESCORED_AFTER_ACTED_ON(a re-score after the workflow acted, kept as history only) orRESCORE_FAILED(a re-score that produced no score; the application keeps its score). An entry also records the workflow version, each requirement as it was worded and weighted, and the screening answers that were scored. - Counts. Each application on the node is awaiting analysis (
candidatesAwaitingAnalysisCount: no score and no failure yet), failed (candidatesAnalysisFailedCount: scoring failed, so it has no score), or scored, and a scored application either meets every mandatory requirement or misses one. Those four counts add up tocandidatesCount. An application whose latest re-score failed keeps its earlier score, so it counts as meeting or missing the requirements and in a band, and also incandidatesRescoreFailedCount.candidatesAnalysisErrorCountis the failed and re-score-failed applications together. - Score band splits. Each score band's
candidatesRequirementsMetCountandcandidatesRequirementsNotMetCountcount the applications scored in its range, whether or not a knockout branch lets them through, so across the bands they add up to the node's own met and not-met counts. The band'smandatoryRequirementsFiltersays which of them itscandidatesCountcounts:ANYwhen no knockout branch comes first, otherwiseMETorNOT_MET, the applications the knockout branch lets through. To list the band, filter applications onisAllRequirementsMettrueforMETorfalseforNOT_MET, beside the range. The band's ownisAllRequirementsMetsays the same asnull,trueorfalse, and is deprecated: readmandatoryRequirementsFilterinstead. - Scale. Scores are
totalScoreScaled, from 0 to 1, as in the analysis webhooks. A score band'sminScoreandmaxScoreare the whole-number scores (0-100) the workflow branches on. ItsminTotalScoreScaledandmaxTotalScoreScaledare thetotalScoreScaledrange the band counts. - Re-scores.
isRecalculatingScoressays whether a re-score is running. No progress count is given. Popp doesn't send re-score webhooks yet. Once it does, a re-score that changes an application's result sendsCANDIDATE_ANALYSIS_RESCOREDif the workflow hasn't acted on the application's score yet, orCANDIDATE_ANALYSIS_RESCORED_AFTER_ACTED_ONif it has. - Candidate ID. Each application row carries
candidateId. The analysis webhooks carry the same ID asdata.recordId. - Not found. The node routes return
404when the workflow has no analysis for the slug, for example before the node's first publish. The application routes return404for a candidate who isn't on the node's analysis, isn't in your organization, or has been erased. Another organization's workflow returns403. - Erasure. Counts and lists leave out erased candidates.
Related guides
- Workflow Configuration: the configuration JSON, node types, routing rules, validation errors and worked examples.
- Workflow Events: the webhooks sent as runs progress.
- Webhooks: subscribe an HTTPS endpoint and verify the
x-signatureheader. - Authentication: authenticate API requests with your Popp API key.
Updated 8 days ago
