Mika Pozo.
← index
In productionFlagship 01 — Autonomous BDR

The difference between a tool operator and an architect: a state-of-the-art autonomous BDR, built for chaos.

An autonomous WhatsApp BDR that runs a full NEPQ sales conversation end to end — cold outreach to booked meeting, no human in the loop. Running in production.

65–75%cold replyBDR v4
~32%bookingof replies
~2 minfirst responsemedian
20 → 90n8n nodesdemo → system
the 30-second version
01The problem

There are hundreds of autonomous email SDRs. There are zero autonomous WhatsApp BDRs — and the reason is instructive.

Email outreach is the CLI era of sales: you type a command, you wait, the interface never surprises you. That's why it automates cleanly — hundreds of autonomous SDRs exist. WhatsApp is the conversation that answers back. Audio, images, a “quem é você?”, a partner who grabs the phone mid-thread — none of it scripted. The only bots that survive that today talk to warm leads, where the questions are predictable and an FAQ covers most of them. A cold BDR has no such luxury: no intent, no map, pure chaos. So the category didn't exist. And in a country where ~99% of business runs on WhatsApp, with competitors fielding four BDRs by hand, that missing category was the whole problem — and, solo, mine to solve alone.

02The thesis, applied

Here's the part nobody wants to hear: in a cold thread, you have almost nothing. No intent, no context, a stranger who owes you zero seconds. So you stop guessing what they want — guessing is the amateur move — and you use the one thing the chaos can't take from you: their own words. The lead names a pain, and the agent doesn't paraphrase it. It quotes it back, verbatim. That's when the room changes. But it still doesn't get to pitch. It has to earn it — and “earn it” isn't a vibe, it's a condition in the code: the bridge to a meeting fires only when the pain is named and the desired outcome is projected (dor_nomeada AND desejo_projetado). No named pain, no bridge. The bot can't manufacture urgency it never heard. That single gate is the whole thesis — the line between a script that pushes and a conversation that qualifies itself.

(To be precise about what this is and isn't: I capture the signal and it shapes the copy at the point of generation. It's not a system that learns on its own. That part is still me.)

03The build

GTM Engineering, stripped of the hype, is still GTM — the intersection of marketing, sales, and product. So the first thing I built wasn't the plumbing; it was the conversation logic. Sales psychology, objection handling, real neuro-persuasion technique, translated into something a machine could run. The assumption underneath was the one everyone makes on this kind of work, and it felt airtight: if a bot could apply what a good human SDR already knows, how could it fail? I was very wrong.

What building this taught me the hard way is that you spend far more time designing for the imperfect case than the perfect one. The perfect reply — lead names the pain, asks for the call — needs almost no code. Two kinds of imperfection eat the months. There's the conversation: the voice note at 11pm, the “quem é você?”, the partner who grabs the phone mid-thread, the half-yes, the objection dressed as a question. And there's the system underneath it: silent failures that swallow an error without a trace, an API call that fires twice and double-messages a lead, race conditions, a connection that drops mid-send. A good share of the refactors were never about making the bot smarter — they were about the things that quietly broke. That gap is the entire distance between a demo and a system. It's also, concretely, why the first version ran on ~20 n8n nodes and the current one runs on ~90.

The motion starts before the lead ever replies. A cadence opens cold — and it opens with a question, not a pitch, because in a channel this personal a question is the only thing that earns a response. When the lead answers, that message hits a single webhook, and the first decision the system makes isn't what to say — it's who should say it. A router reads the lead's number and their conversation state (bdr_threads.nepq_state) and sends the thread down one of three paths: the cold BDR, the warm SDR, or silence, if they're already booked or closed. One door, deterministic triage.

one door, deterministic triagefig.01 / router
WhatsApp insingle webhook
W-Routerphone + nepq_state
BDRcold
SDRwarm
silentbooked / closed

On the BDR path, the conversation runs a four-stage NEPQ arc — ack_clarify → probe_dor → solution_awareness → ponte, three turns if it can, five at most. An LLM agent writes the reply, but it never runs alone: a deterministic layer sits in front and resolves the moves I can predict — a clean yes, a decision-maker check, a booking intent — with plain regex, so the model is only invoked when the conversation is genuinely ambiguous. That's the 20-to-90-nodes lesson in practice. Most of those nodes aren't intelligence; they're the handling of every imperfect path the intelligence shouldn't be trusted with. AI is a phenomenal partner when you know exactly the output you want — and a liability the moment you let it decide on its own.

the nepq arc — a state machine, not a scriptfig.02 / state machine
ack_clarify
probe_dor
solution_awareness
[ dor_nomeada ∧ desejo_projetado ]
ponteaccepting state

The bridge is a guarded transition— it can't fire on empty desire. No named pain, no bridge; the agent can't manufacture urgency it never heard.

Everything that goes out clears a preflight gate written in SQL: no send unless the instance is healthy, the contact hasn't opted out, and the thread isn't flagged for reconciliation — blocked messages requeue, they don't disappear. And because leads talk the way people actually talk here, audio is transcribed and images are read by vision before the conversation logic ever sees them, so a voice note is just another turn.

preflight gate — the SQL that guards every sendsql
1-- runs before every outbound message
2select t.thread_id
3 from bdr_threads t
4 join wpp_instances i on i.id = t.instance_id
5 where i.status in ('active','ramping') -- healthy
6 and t.opted_out is false -- consent
7 and t.compliance_ok is true
8 and t.needs_reconcile is false; -- not mid-recovery
9-- blocked → requeue(interval '7 days'), never dropped
Reconstructed from the live gate — same conditions, same tables. The health field it reads is the exact one the kill-switch writes; the loop closes.

Under it: n8n, Evolution API, Supabase, Twenty CRM, a classifier and a NEPQ agent on the LLM side, Cal.com and Google Calendar for booking, Reacher for email health, Telegram for ops. No Redis — state lives in Postgres and Twenty, and adding a cache nobody needed would've been architecture for its own sake.

the operational spine

None of that matters if the system quietly dies at 2am and I hear about it from a client. WhatsApp punishes automation harder than email — a number that sends too fast, or trips the wrong pattern, gets banned, and a banned number is dead weight. So the largest part of those ~90 nodes isn't conversation at all. It's the layer that keeps the thing alive without me watching it.

A monitor runs every 15 minutes and reads each number's health straight from the message log. If a number's ban rate crosses 5% — with a floor of 50 sends a day, so a quiet number can't trip it on noise — it suspends that number for 72 hours automatically, no human in the loop, and tells me after the fact. When a suspension expires, the number doesn't jump back to full volume; it ramps — 30, then 50, then 75, then 100 messages a day, three days per step — the way you'd warm a cold number by hand, except it never forgets.

the layer that keeps it alive without me watchingfig.03 / spine
W7 kill-switchban rate > 5% → 72h suspend
ban detectionevolution error → flag + alert
W8 error handlerevery failure fingerprinted
W9 sentinelconnection poll → human call
instance health15-min loop
preflight gateSQL — send or requeue

Every workflow reports to a single error handler. When anything fails — a ban, a dropped Evolution call, an agent error — it gets fingerprinted by type and severity, written to a table with the execution URL so I can open the exact failure, and, if it's high or critical, it pings me on Telegram. Nothing fails silently. That one decision — make every error announce itself — is what turned “why did this lead go cold?” from a mystery into a link I click.

The sender has its own reflexes: if Evolution returns “disconnected” or “banned” mid-send, it flags the number and alerts me on the spot, independent of the monitor. And it doesn't type like a robot — it shows “typing…” for a beat scaled to the message length, then waits a random 7 to 15 seconds between messages, because the fastest way to get a number banned is to behave like software.

Some calls I keep for myself. A sentinel polls each number's connection state every few minutes, and when a connection looks degraded but not clearly dead, the system doesn't guess — it sends me two Telegram buttons, “suspend” or “keep,” and waits. Full autonomy where the signal is unambiguous; a human where it isn't.

the system it lives in

This WhatsApp BDR was never a standalone bot — it's one channel in a two-channel cadence. The same leads are worked over email and WhatsApp in a coordinated sequence, which is why deliverability matters as much as conversation: Reacher verifies every address and watches domain health, so the email side stays out of spam while the WhatsApp side does the talking.

And every thread writes back to the CRM, because a conversation nobody can see later isn't an asset. When a lead enters, the system creates the Person, Company, and Opportunity in Twenty; it logs the NEPQ call briefing — likely pain, the hook, the discovery questions — so whoever takes the meeting walks in prepared; and it moves the record's state as the thread advances. CRM hygiene isn't an afterthought here — it's the difference between a bot that sends messages and a system a sales team could actually inherit.

One thing email never forces you to solve: WhatsApp is bound to a physical phone, not a login, so every number is its own island. All of them feed into a single Chatwoot inbox — one screen where a human can watch, or step into, any conversation across every instance. (That same unified history is what the agent reconciles from when a number drops.) And above it all sits the Apex dashboard: the control room for the BDR — instance health, ban status, what's in flight, what needs a human — so I'm never flying the system blind.

the booking mechanism

When a lead does say yes, a booking subroutine takes over. It proposes real slots — business hours, a two-hour lead time, a seven-day window — and either books the meeting on Cal.com straight from a plain-text confirmation or sends two links. A human approval step re-checks the slot against Google Calendar before the event is created, so it can never double-book me.

04What broke / trade-offs

The version history is the least flattering and most useful part of this. The first version replied at nearly 69% and closed nothing — the conversation logic could open a door and had no idea what to do once it was inside. So I did what everyone does: I added. More rules, more branches, more “personalization.” Reply dropped to around 34%, and closing didn't move — over-engineering the perfect path only made the imperfect ones worse.

The turn came when I stopped adding and started listening. I began pulling the buyer's own language from everywhere my ICP already talks — the communities they live in, surveys, intake forms, on top of the real conversations, meetings, and objections I already had — and fed that verbatim back into the conversation logic. Mirror the lead's own words; gate the pitch on named pain. Reply climbed back to 65–75%, and the version that finally applied the original thesis booked around 32% of the leads that replied.

Now, capped operationally as a solo founder, I'm changing the premise on purpose. Instead of asking for a meeting cold, the current version leads with value — a lead magnet — to test whether reciprocity turns a cold lead into an engaged one, and lets me raise the ticket. That's the experiment running now.

None of this came clean, and the failures that taught me the most were the unglamorous, technical ones.

the unglamorous fixes — what the refactors actually were
backup job threw after an env var was renamed — died on startup, silent
a month of measurement lost before I noticed
+every job reports to the global error handler — nothing fails silently
on suspension, the agent went blind to the human side of the thread
replied as if half the conversation never happened
+reconcile the other source into SQL memory — agent sees the whole thread
a race double-fired sends — the same lead messaged twice
+Postgres advisory lock — a message can't go out twice

Every one of these now reports to a single error handler — because the first time a lead went cold for a reason I couldn't see, I decided nothing would ever fail silently again.

05The numbers
the reply rate tells the whole story, version by version
  1. v1

    ~69% reply · 0 close

    Great at starting conversations, useless at finishing them. The logic could open a door and freeze inside it.

  2. v2–v3

    ~34% reply · ~10–12% close

    I over-engineered the perfect path; the imperfect ones got worse.

  3. v4

    65–75% reply · ~32% booking

    The conversation intelligence from the original thesis, applied. Named base: 275 leads (179 confirmed-send to 206 including inbound-only).

  4. v5

    the lead-magnet pivot · live now

    Leading with value to turn cold into engaged and raise the ticket. Measured from day one.

and the constants underneath

  • ~2-min average first response — seconds when the number is healthy; the average carries the instance drops from a live test.
  • Progressive intent scoring— every reply, pain probe, and consequence raises the lead's score (+2 / +3 / +3), so the pipeline sorts itself.
  • ~20 → ~90 nodes — the distance from demo to system, almost all of it spent on the imperfect paths.
  • R$0 infra — built and run at cost, solo.

provenance

reply & intent scoring — derived from message logsbooking & first-response — documented outside the logs, shown as proof, not asserted

The BDR is almost a state-of-the-art of who I am — an intersection of a dozen disciplines.