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

ConceptDescription
WorkflowYour candidate journey: its entry pathways, nodes and routing
VersionA published snapshot of a workflow. It never changes.
Entry pathwayA way candidates enter: a source, such as the API, paired with the node they start on. A workflow can have several.
RunOne candidate's path through a published workflow
NodeOne part of the graph, such as a screening conversation or a branch. Its id names it within one version.
SlugA node's permanent name, the same in every version of the workflow
OutcomeWhat a node produced for a run, such as a score or how a conversation ended
RoutingThe rules that pick the next node from a node's outcome
JourneyThe 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:

statusThe run finished becauseWebhook
COMPLETEDIt left a node, and the node's routing names no node for the event it left by.WORKFLOW_RUN_COMPLETED
FAILEDA node hit an error the run can't recover from. errorCode says why.WORKFLOW_RUN_FAILED
CANCELLEDIt 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}/runs with 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 atsJobId with GET /v1/ats/jobs and 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[]: candidateProfileIds and candidateProfileExternalIds carry no tags. Sending tags, or a tagsObject with entries, at the top level of the body (outside contacts[]) returns 400 Bad Request. A null, [] or {} there is ignored.
  • Both fields are optional. null is the same as leaving the field out.
  • Popp trims each tag (and each tagsObject key and value) and collapses repeated whitespace, but keeps casing and punctuation. A tagsObject entry 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 Doe and Recruiter: Jane Doe are two tags.
  • Popp drops, rather than rejects, blank tags, tags over 250 characters (for tagsObject, the whole key: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 tagsObject value, tags that is not a list, or tagsObject that is not an object, returns 400 Bad Request for the whole batch, the same as a contact with neither email nor phoneNumber.
  • The run-start response does not echo tags or tagsObject.

Node types

Node typeconfiguration.nodes[].typeWhat it does
Screening outreachscreeningOutreachScreens 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 outreachengagementOutreachStarts 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.
ConditionconditionBranches the run on what it has collected so far: answers, scores or candidate attributes.
AnalysisanalysisScores 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 jobcreateAtsApplicationAdds 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 applicationupdateAtsApplicationWrites 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 outreachschedulingOutreachSends 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 candidateName filter 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 to candidateProfileId.

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.

  • status says whether the run is still going or has finished.
  • state says what the run is doing now, such as withCandidate while it waits for the candidate, or analysed while it holds a score at an analysis node. status is RUNNING in every state but finished.
  • finishReason and cancellationCode say why a finished run finished.
  • journey lists 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.
  • currentNodeSlug and lastFailedNodeSlug name the nodes in currentNodeId and lastFailedNodeId by 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.

cancellationCodePopp cancelled the run because
ATS_APPLICATION_REJECTEDThe run's ATS application was rejected or declined before the run moved on from an analysis node.
ATS_STAGE_NOT_SELECTEDBefore 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_REMOVEDA publish removed the analysis node the run was waiting at.
WORKFLOW_ARCHIVEDThe workflow was archived while the run was waiting at an analysis node.
CANDIDATE_ERASEDThe 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.

  1. API pathway: the firm's ATS posts new RGN applicants to POST /v1/workflows/{workflowId}/runs.
  2. 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.
  3. 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.
  4. 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.
  5. Engagement outreach (compliance pack): emails the candidate the list of documents to send back. Cedar Health's compliance team handles the reply.
  6. 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 archived
  • POST /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.

RouteReturns
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}/applicationsA 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}/historyThe 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 analysisOutcomeActedOnAt says 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 by onAnalysisFailed acts 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 trigger is SCORED (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) or RESCORE_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 to candidatesCount. 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 in candidatesRescoreFailedCount. candidatesAnalysisErrorCount is the failed and re-score-failed applications together.
  • Score band splits. Each score band's candidatesRequirementsMetCount and candidatesRequirementsNotMetCount count 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's mandatoryRequirementsFilter says which of them its candidatesCount counts: ANY when no knockout branch comes first, otherwise MET or NOT_MET, the applications the knockout branch lets through. To list the band, filter applications on isAllRequirementsMet true for MET or false for NOT_MET, beside the range. The band's own isAllRequirementsMet says the same as null, true or false, and is deprecated: read mandatoryRequirementsFilter instead.
  • Scale. Scores are totalScoreScaled, from 0 to 1, as in the analysis webhooks. A score band's minScore and maxScore are the whole-number scores (0-100) the workflow branches on. Its minTotalScoreScaled and maxTotalScoreScaled are the totalScoreScaled range the band counts.
  • Re-scores. isRecalculatingScores says 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 sends CANDIDATE_ANALYSIS_RESCORED if the workflow hasn't acted on the application's score yet, or CANDIDATE_ANALYSIS_RESCORED_AFTER_ACTED_ON if it has.
  • Candidate ID. Each application row carries candidateId. The analysis webhooks carry the same ID as data.recordId.
  • Not found. The node routes return 404 when the workflow has no analysis for the slug, for example before the node's first publish. The application routes return 404 for a candidate who isn't on the node's analysis, isn't in your organization, or has been erased. Another organization's workflow returns 403.
  • 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-signature header.
  • Authentication: authenticate API requests with your Popp API key.

Did this page help you?