Overview
I built an inbound lead qualification and routing system to automate inbound lead handling. Every form submission gets researched, qualified, routed to the right rep, and announced in Slack on its own, and a human only gets pulled in for the genuinely ambiguous calls.
< 2 min
Lead to routing
The problem
The leads were never the bottleneck. The research nobody had time for was.
Before this, every inbound demo request started with the same manual detective work. Someone had to open an enrichment tool, search the web, read about the company, and decide whether it was actually a fit. The fields people typed into the form couldn't be trusted, so the form was a starting point at best.
Then came the parts after the decision. If a lead passed, someone had to work out which rep should take it, keep the assignments roughly even, and update a spreadsheet by hand. None of it got announced automatically. Reps had to go dig through the CRM, and marketing had no view into what was coming in or how it was being judged.
So leads sat. Speed-to-lead is the one thing an inbound funnel can't afford to lose, and it was leaking at every step: the research, the routing, the handoff. All of it waiting on a person being free.
What I did
I broke the whole thing into three small automations that hand off to each other.
Tech stack
- HubSpot: the CRM and form source, and the system of record for each qualification result.
- Railway: hosts the small webhook server that bridges inbound events into the pipeline.
- Trigger.dev: durable orchestration for the three chained jobs, with retries, concurrency limits, and waitpoints.
- Claude Agent SDK: the research agent (with web search) that makes the qualification call and decision-making.
- Apollo: company enrichment that hands the agent structured facts up front.
- Google Sheets: the run log and audit trail, plus the routing state.
- Slack: team notifications and the human approve-or-reject gate.
- GitHub: source of truth for the code, and it auto-deploys the webhook server.
I built it as three separate jobs running on Trigger.dev, chained together. One qualifies, one routes, one notifies. Keeping them apart means each does a single thing well, and I can add a new path later without touching the parts that already work.
Getting the leads in
The form lives in HubSpot, which can fire a plain webhook the moment someone submits. The catch is that Trigger.dev's API wants an authenticated call, and a raw form POST can't add auth headers. So I stood up a small webhook server on Railway that sits in the middle: it catches the inbound POST, checks it, and starts the real job with the right credentials. That same little server later catches the Slack button clicks too.
The qualification engine
This is the step that used to be a person. First it enriches the company through Apollo to grab the structured facts fast. Then it hands everything to a research agent built on the Claude Agent SDK, with web search turned on, and asks it to go look the company up and judge it against the fit criteria. The agent returns a clean verdict with its reasoning. That gets written back to the CRM as the system of record, and every run gets logged to a Google Sheet, so there's an honest audit trail of what came in and why it scored the way it did. I kept the AI on the one job that actually needs judgment. Everything around it is plain, predictable code.
The human-in-the-loop gate
Some companies are honestly hard to call. Instead of forcing a guess, the agent is allowed to say "unsure," and when it does, the system drops the company, its reasoning, and an approve-or-reject pair of buttons into Slack. Here's the part I like: the job then just pauses. Trigger.dev holds the run open on a waitpoint, costing nothing while it waits, and resumes the second someone clicks. If no one answers, it re-pings during business hours until they do. A person spends five seconds on the handful of cases that need a person, and the machine takes the rest.
Routing and the announcement
Once a lead is qualified, a second job assigns it to the next rep by lowest current load, logs the assignment, and fires the third job: a dedicated Slack messenger that posts the notification with full context. Neither of these uses AI. The router is deterministic on purpose, and I serialized it so two leads landing at the same instant can't both grab the same rep. The whole path, form to Slack, runs in about thirty seconds and costs roughly two tenths of a cent in inference, because the agent runs on my Claude Max plan through a headless token instead of metered API calls.
Takeaways
By using the Claude Agent SDK, I was able to deploy an AI agent to handle research and reasoning without incurring API costs.
What I'm taking away from this is how little of a "smart" system should actually be smart. Exactly one step here needs judgment, so exactly one step got an agent. The routing, the logging, the notifications, the handoffs: all of that is boring deterministic code, and it's better for it. The model does the part only a model can do, and nothing else.
The other surprise was how much durable orchestration changes what counts as easy. "Wait for a human, even for two days, then pick up exactly where you left off" is normally a real engineering problem. With a waitpoint it was a few lines. That one primitive is what made the human-in-the-loop gate cheap enough to bother building.
Skills I picked up
- Durable workflow orchestration on Trigger.dev: chained tasks, concurrency limits, retries, and waitpoint tokens.
- Wiring an agentic AI step (Claude Agent SDK with web search and structured output) into a production pipeline.
- Event plumbing across systems: HubSpot webhooks, Slack interactivity, and a small Node bridge server to hold it together.
- API integration end to end: CRM writeback, third-party enrichment, and spreadsheet logging through service-account auth.
- Designing human-in-the-loop systems that only ask for a person when a person is actually needed.