Skip to main content
A pipeline view needs three reads that do not join for you: the people, their attempts at a job, and the ordered stages that job runs. Count the wrong one and a person applying to three roles becomes three people.

What you’ll use

Models - candidate, application, job interview stage
Before you start
  • The connector is an ATS connector. A connector token works for one API category, so an HRIS token on /api/ats/v1/* returns 403 - see Authentication.

Steps

1

Read candidates

Filters: ids, remote_id, email_addresses, first_name, last_name, company and tag. A candidate exists once no matter how many roles they go for, and email_addresses, phone_numbers, locations, urls and tags are all lists.Result: People, with duplicates already removed by the ATS.
2

Read applications

Result: One record per candidate-job pair.
3

Count the right model for the question

A candidate who applies to three roles is one candidate and three applications. Count applications to report pipeline headcount, and you’ll overcount by however many people applied more than once - which, if a company is hiring across several similar roles, can be a lot.Result: Numbers that actually mean what you think they mean.
4

Expand rather than fetching in a loop

expand returns related records inline. List them with commas, no spaces.
One expanded request beats many follow-up calls, which is what burns through the per-connector rate limit on a large pipeline - see Errors & issues.Result: Related records inline, at the cost of a bigger response.
5

Read the stages for a job

Each stage carries name, job and stage_order. Filter by job - read them without one and you get every stage of every job, which isn’t a pipeline.Result: One job’s process, in order.
6

Order by stage_order, never by name

Stage names are just the customer’s own words - “Phone Screen”, “Screen”, “HM Screen” and “Recruiter Call” might all be the same step at different customers. Sort by name, or match names against a fixed list, and it breaks the moment a customer renames a stage.Result: A sequence you can actually compare positions in.
7

Resolve each application's position

Expanding current_stage skips a lookup per application. Join it against the ordered list from the last step to get a position instead of just a label.Result: Where each application sits, in a way you can compare.
8

Treat rejection as separate from stage

An application that’s ended carries rejected_at and a reject_reason, and its current_stage shows wherever it stopped - not a “rejected” stage.
Count applications by stage without excluding rejected ones, and every stage looks more full than it really is.Result: Active pipeline kept separate from closed applications.
9

Detect movement rather than polling for it

Subscribe to connector_data_modified and re-read the applications that changed - see Choose Webhook Events.Change events carry IDs, not records - they tell you something changed, not what. To know an application moved, compare current_stage against your last stored copy.Result: Real stage transitions, not just “something changed.”

Frequently Asked Questions

A connector token only works for one API category. An HRIS connector called on /api/ats/v1/* returns 403 even with valid credentials.
Some ATS platforms create a second candidate record instead of a second application when someone applies again, especially after a long gap or through a different source. Match on email_addresses to catch it - see Record identity.
Both are free-form and specific to each customer. Map them per connector rather than assuming a standard set.
job-interview-stages was read without the job filter. Stages belong to one job each, so an unfiltered read is every process at once.
Rejected applications are being counted. current_stage shows where an application stopped, not whether it’s still moving - filter on rejected_at.