engribet/automation
← All projects

Recruitment Pipeline with Airtable and n8n

A staffing-agency data model in Airtable, kept fed with live listings from the Remotive API by an n8n workflow — every write is an upsert, so the import can run on repeat without ever creating a duplicate.

Running in production
Status
Complete
Trigger
Manual, on demand
Nodes
13
Writes
Upsert (Airtable)

The problem

A staffing agency's pipeline has a real relational shape: clients have job openings, candidates apply to openings (often more than one), and a successful application becomes a placement. Modeling that badly — flat tables, free text where a relationship belongs — makes the data unreliable the moment more than a handful of records exist.

On top of the data model, job openings need to stay current. Re-typing listings from a job board by hand does not scale and drifts out of date immediately. This project is both halves: a properly related Airtable base, and an n8n workflow that keeps one of its tables fed automatically — safely, since re-running an import is something that will happen by accident sooner or later.

The data model

Clients Job Openings Applications Placements Candidates
Applications is the junction table: a candidate can apply to several openings, and an opening receives several applications — a many-to-many relationship that a direct link cannot represent cleanly.

Every relationship is a linked-record field rather than a copied-in name, so a client's details live in exactly one place. Applications exists as its own table specifically because Candidates and Job Openings are many-to-many — collapsing that into a direct link would lose per-application data like applied date and stage. Placements is kept separate from Applications because a placement is a business event (a start date, a fee), not just an application status.

Fields are typed rather than left as free text wherever a structured type fits: single selects for status and category, native date fields for posting and applied dates, and a Record Source field (Sample / Remotive) on Clients and Job Openings that keeps hand-entered data and API imports separable in views and rollups. A rollup on Clients counts open positions per client; a lookup surfaces the client through the chain from Application → Job Opening without needing to click through.

Airtable can only hold "one placement per opening" by convention — any linked field accepts multiple records, so nothing stops a second one. In a relational database the same rule is a single UNIQUE constraint that the database enforces outright. That gap, and how the rest of the schema would translate to Postgres — keys, constraints, indexes — is written up in the README linked below.

How the import works

Search Keyword one editable value Fetch Remotive Jobs retry 3× / 5s, 15s timeout Valid API Response? jobs array present? no Stop and Error yes Clean, Dedupe & Filter strip HTML, match keyword Upsert Clients match: Client Name Upsert Job Openings match: External Job ID
A validation gate and retries defend against a dead or lying API before anything reaches Airtable; every write is an upsert, so re-running is always safe.
The Recruitment Pipeline workflow open in n8n, showing all 13 nodes from the Execute workflow trigger through Search Keyword, Fetch Remotive Jobs, Valid API Response, Split Out, Clean & Dedupe Jobs, Edit Fields, Filter Jobs by Keyword, Unique Companies, Upsert Clients, Attach Client IDs and Upsert Job Openings, with a Stop and Error branch off the validation node.
The full workflow open in n8n — the diagram above simplifies a couple of steps into single boxes; this is the real canvas.

A single editable keyword

One Search Keyword node holds the search term and feeds both the Remotive API call and a downstream filter — change it in exactly one place, not two. That filter turned out to matter more than expected:

Discovery: Remotive's public API currently ignores its own documented search parameter — querying it with a nonsense term returned the same listings as a real one. The workflow still sends the parameter (correct per the docs, and it will start working again if Remotive restores server-side filtering), but the keyword is also enforced client-side against the job title, category, tags and description before anything reaches Airtable.

Deduplication, two layers

Within a single API response, a Code node drops any repeated job id before it goes further. Across separate runs, both Airtable writes use the native create-or-update operation rather than plain create: Job Openings match on Remotive's External Job ID, Clients match on company name. A second run of the same import updates existing records instead of duplicating them — verified by comparing the Airtable record IDs returned by two consecutive runs and finding them identical.

Error handling

Three mechanisms, deliberately layered for two different failure modes. The fetch node retries up to three times with a five-second wait and a fifteen-second timeout, which catches a dead or unreachable API. A validation gate then checks that the response actually contains a usable jobs array before anything touches Airtable, which catches an API that answers 200 OK with the wrong shape of data — a page that fails politely instead of one that fails loudly, or worse, silently writes nothing at all.

What I would add next

Source

Full write-up (schema reasoning, the Postgres/SQL comparison, setup steps), the n8n workflow export, the Airtable base, and a short walkthrough video:

Airtable baseOpen (read-only)
n8n workflowDownload JSON
Full write-upREADME
WalkthroughWatch on Loom (5 min)