Process automation
Van Make.com naar Temporal: een recruiter-bot opnieuw bouwen
Zes Make.com-scenario's, één ops-lead die om 06:00 wakker werd van een Bullhorn-token dat niemand had ververst. Zo brachten we het terug tot één Temporal-workflow.

Om 06:11 op een dinsdag in maart opende de operations lead van een Eindhovens recruitmentbureau met zestien mensen haar telefoon en zag dezelfde Telegram-melding als de dinsdag ervoor. Een Make.com-scenario was blijven hangen bij stap zeven van veertien. Het Bullhorn access token was midden in de run verlopen, de retry-route had gevuurd zonder het refresh token te roteren, en twaalf kandidaatrecords zaten nu in een queue waar niemand eigenaar van was. Ze loste het op vanuit haar keuken in twintig minuten. Daarna mailde ze ons.
Hieronder staat het draaiboek waarmee we zes Make.com-scenario's vervingen door één Temporal-workflow en een Hatchet worker pool. Het is geen afrekening met Make.com. Make past prima bij tweestaps-automatiseringen die een niet-engineer op een zondagmiddag in elkaar zet. Het past slecht bij waar dit team in was uitgegroeid.
De zes bots die we aantroffen
Voor we iets aanraakten, brachten we elk scenario in kaart en elke plek waar state werd bewaard. Het team had:
- Een cv-intake bot die uit een Workable-webhook trok en in Bullhorn postte.
- Een LinkedIn Recruiter outreach-bot die InMail-templates afvuurde op een opgeslagen zoekopdracht.
- Een kandidaat-scoring bot die een LLM aanriep op geparste cv-tekst en wegschreef naar een custom Bullhorn-veld.
- Een interview-scheduler die Cal.com en Outlook-agenda's koppelde over twee seat licenses.
- Een wekelijkse rapportgenerator die Bullhorn-stats in Notion trok en als PDF exporteerde.
- Een AVG-scrubber die de database uitkamde op verwijderverzoeken.
Er was ook een zevende scenario waar niemand het over had. Het ververste het Bullhorn-token elke negen minuten en schreef de nieuwe waarde weg naar een Google Sheet die de andere zes scenario's uitlazen. Die sheet was het single point of failure van de hele stack. Als iemand per ongeluk een cel aanpaste, viel elke bot stil.
Waar Make.com bezweek op deze schaal
Er stapelden zich drie problemen op. Het eerste was state. Zes losse scenario's betekende zes losse retry policies, zes error routes, zes manieren om een record halverwege kwijt te raken. Er was nergens een parent-proces dat kon zeggen: "deze kandidaat wordt verwerkt, begin geen tweede pass."
Het tweede was het auth-model van Bullhorn. De Bullhorn REST API geeft kortlevende access tokens uit (standaard tien minuten) en roteert het refresh token bij elke uitwisseling. Als twee scenario's tegelijk proberen te verversen, krijgt één van beide een ongeldig refresh token terug en zit je vast tot een mens een nieuwe plakt. De Google Sheet moest die race voorkomen. Meestal lukte dat. Meestal was het probleem.
Het derde was het wekelijkse LinkedIn Recruiter-quotum. Elke seat heeft een vast InMail-budget dat reset op de facturatiedag van die seat, niet op een algemene maandag. De outreach-bot hield het verbruik bij door rijen in een Google Sheet te tellen. Door mislukte sends die nooit werden verrekend, klopte de telling altijd een paar af. Als het quotum op was, kreeg de bot 429's en begon de scoring-bot, die er stroomafwaarts aan hing, lege scores naar Bullhorn te schrijven.
De operationele kosten op Make liepen tegen de €420 per maand en stegen. De technische schuld lag hoger.
De splitsing tussen Temporal en Hatchet
We kozen twee open-source engines die één Postgres-instance delen en verschillende rollen spelen.
Temporal verzorgt de langlopende, duurzame, stateful onderdelen. Eén workflow per kandidaat, geschreven in TypeScript, leeft zolang de kandidaat actief is. Hij overleeft worker restarts, version upgrades en process kills. Token brokers en rate-limit governors draaien als aparte langlopende workflows die andere workflows queryen voor de actuele state.
Hatchet draait de per-event fan-out: cv-parsing, LLM-scoring, PDF-rendering, webhook-acks. Het start sneller dan een Temporal-workflow en is goedkoper voor een one-shot taak die binnen dertig seconden klaar is. Hatchet komt met een eigen Postgres-queue, dus Redis of RabbitMQ hadden we niet nodig.
Eén Postgres-database, twee services, één set back-ups. De hele control plane draait op één Hetzner VPS voor minder dan €40 per maand.
De Bullhorn-tokenrefresh overleven
De ene wijziging die de meeste rust opleverde, was het verplaatsen van Bullhorn-auth naar een aparte Temporal-workflow. We noemen het de token broker.
import {
proxyActivities, sleep, continueAsNew,
defineQuery, setHandler,
} from '@temporalio/workflow';
import type * as activities from './activities';
const { refreshBullhornToken } = proxyActivities<typeof activities>({
startToCloseTimeout: '60 seconds',
retry: { maximumAttempts: 8, initialInterval: '5s', backoffCoefficient: 2 },
});
export const getAccessToken = defineQuery<string>('getAccessToken');
type TokenState = { accessToken: string; refreshToken: string };
export async function bullhornTokenBroker(state: TokenState): Promise<void> {
setHandler(getAccessToken, () => state.accessToken);
for (let i = 0; i < 200; i++) {
state = await refreshBullhornToken(state.refreshToken);
await sleep('9 minutes');
}
await continueAsNew<typeof bullhornTokenBroker>(state);
}
Elke andere workflow die Bullhorn moet aanroepen, doet een Temporal query op de broker voor het actuele access token. Queries zijn goedkoop en worden door de workflow-engine geserialiseerd, dus er is geen race. De broker is het enige proces dat het refresh token ooit aanraakt, dus er is niemand om mee te racen.
De continueAsNew-call na 200 cycli is huishouden. Temporal-histories zijn begrensd, dus we sluiten een workflow run af en starten elke dertig uur of zo een verse met dezelfde state. De aanroeper merkt er niets van.
Alles wat bij elk gebruik roteert (refresh tokens, single-use API keys, OAuth PKCE-codes) heeft precies één eigenaar nodig. Als twee processen kunnen racen, wint er ooit één en zit je buiten.
De wekelijkse reset van LinkedIn Recruiter
Het InMail-quotum zit in een andere langlopende workflow die we de seat governor noemen. Eén per LinkedIn Recruiter-seat. Hij houdt het resterende quotum als workflow-state vast, biedt een reserveInmail-update die true teruggeeft als er een credit gereserveerd is, en luistert naar een wekelijks reset-signaal.
import {
defineUpdate, defineSignal, defineQuery,
setHandler, condition,
} from '@temporalio/workflow';
export const reserveInmail = defineUpdate<boolean>('reserveInmail');
export const resetQuota = defineSignal<[number]>('resetQuota');
export const remaining = defineQuery<number>('remaining');
export async function linkedinSeatGovernor(weeklyQuota: number): Promise<void> {
let credits = weeklyQuota;
setHandler(reserveInmail, () => {
if (credits > 0) { credits -= 1; return true; }
return false;
});
setHandler(resetQuota, (n) => { credits = n; });
setHandler(remaining, () => credits);
await condition(() => false); // draait tot de seat wordt opgeheven
}
De reset wordt afgevuurd door een Temporal Schedule, één per seat, op een cron die overeenkomt met de reset-dag van die seat. Geen Google Sheet, geen scheve teller, geen scoring-bot die nulls schrijft als het quotum opraakt.
Wil een worker een InMail sturen, dan doet hij de update en wacht op het boolean-antwoord. Geen credit? Dan parkeert het werkitem in een Temporal-timer tot volgende maandag en hervat automatisch. De ops-lead ziet het niet.
Migratievolgorde en rollback
We hebben de scenario's in deze volgorde verhuisd. De volgorde telt meer dan de technologiekeuze.
- Token broker eerst. Dit was de dragende fout. Tot die was opgelost, dreigde elke andere migratie dezelfde bug te erven.
- Cv-intake als tweede. Hoogste volume, kleinste blast radius als er een duplicaat doorheen glipte. We voegden een idempotency key toe op basis van kandidaat-e-mail plus binnenkomsttijd, zodat hetzelfde record veilig opnieuw kon worden afgespeeld.
- LinkedIn-outreach als derde. Vereiste dat de seat governor er stond. We draaiden hem vier dagen in shadow mode: de nieuwe workflow logde wat hij zou hebben verstuurd, terwijl het Make-scenario gewoon doordraaide. Toen beide drie dagen achter elkaar overeenkwamen op quotum en sendlist, schakelden we over.
- Scoring en scheduler parallel. Die raakten verschillende Bullhorn-velden en verschillende agenda's, dus gelijktijdige migratie was veilig.
- Wekelijkse rapportage en AVG-scrubber als laatste. Lage frequentie, makkelijk visueel te controleren.
Het rollback-plan was bewust saai. Elk Make.com-scenario bleef zeven dagen na go-live van zijn Temporal-vervanger als koude reserve staan. Cutover betekende het Make-scenario uitschakelen, niet verwijderen. Brak er in de eerste week iets, dan zetten we de schakelaar om en nam de oude versie binnen vijf minuten over.
Migreer de AVG-scrubber niet als eerste. Die verwijdert records by design. Heeft de nieuwe workflow een off-by-one in zijn query, dan ontdek je dat door een kandidaat kwijt te raken.
Resultaten voor het team
Drie cijfers vertellen het verhaal. De pages om 06:00 gingen van ruwweg één per twee weken naar nul in de laatste negen weken. De maandrekening daalde van zo'n €420 op Make plus een betaalde Google Workspace-seat als integratielijm, naar €38 voor de Hetzner VPS en €0 voor de Postgres die erop draait. Een nieuwe integratie toevoegen (we leverden twee weken na go-live een Greenhouse-connector op) ging van "plan een Make-rebuild en waarschuw iedereen" naar één middag, één pull request, één workflow-bestand.
De minder zichtbare verandering was dat de operations lead geen Make.com-beheerder meer hoefde te zijn. Haar werk is recruitment operations, geen scenario-runs babysitten. De pijplijn faalt nu hard zichtbaar op één dashboard als er echt een mens nodig is, en zachtjes in een retry-timer als dat niet zo is. Dat onderscheid was onmogelijk af te dwingen binnen een wildgroei aan losse scenario's.
Niets van dit alles vereist per se Temporal. Cadence, Restate, Inngest of een zorgvuldig zelfgebouwde Postgres-queue brengen je elk in de buurt. Wat de keuze wél vereist, is iemand in het team die comfortabel een workflow-bestand kan lezen en een replay kan doorlopen. Kan niemand in dienst of in piket dat, dan ruil je de ene wildgroei in voor de andere.
De audit van vijf minuten
Toen we deze pijplijn opnieuw bouwden voor het bureau in Eindhoven, viel het ons op hoeveel van het werk geen orchestration was maar token-hygiëne. We schreven uiteindelijk meer over session brokers dan over workflows. Daarom beginnen onze process automation-trajecten nu eerst met een auth-audit voordat we iets anders doen.
Open elke automatiseringstool die jouw team beheert. Zoek voor elk de plek waar de auth-tokens liggen. Is dat een spreadsheet, een Notion-pagina of een opmerking in een scenario, dan is dát vandaag jouw token broker. Bepaal wie er eigenaar van is, voor volgende dinsdag 06:00.
Kern
Alles wat bij elk gebruik roteert (refresh tokens, single-use keys, PKCE-codes) heeft precies één eigenaar nodig, anders sluit je jezelf vroeg of laat buiten.
FAQ
Waarom Temporal én Hatchet, en niet één engine?
Temporal verzorgt langlopende, stateful workflows zoals token brokers en rate-limit governors. Hatchet draait snellere, goedkopere one-shot taken zoals cv-parsing. Eén Postgres, twee engines, duidelijke rollen.
Werkt hetzelfde draaiboek ook voor HubSpot of Salesforce in plaats van Bullhorn?
Ja. Elke OAuth-provider die refresh tokens bij gebruik roteert, vraagt om hetzelfde single-owner broker-patroon. HubSpot, Xero en Salesforce gedragen zich onder gelijktijdige load identiek.
Hoe lang duurde de migratie van begin tot eind?
Voor het team van zestien zo'n zes weken. Drie weken scoped rebuild, één week shadow runs, twee weken gefaseerde cutover met de Make-scenario's warm als fallback.
Kan een niet-engineer dit onderhouden zodra het draait?
Dagelijks beheer wel, via de Temporal Web UI en het dashboard. Wijzigingen in workflow-code vragen nog iemand die TypeScript leest en vóór deploy een replay-test kan draaien.