&Anindo Neel Dutta
HomeCase StudiesSpeakingBlogNotes
&Anindo Neel Dutta
HomeCase StudiesSpeakingBlogNotes
&Anindo Neel Dutta
HomeCase StudiesSpeakingBlogNotes
GO BACK
Project

Building an AI-Powered Outreach Engine in 6 Weeks

Timeline6-Week Sprint
Metric5 Personalized Leads per Run
Date1st Mar, 2026
Core Stack
Next.js
TypeScript
Trigger.dev
OpenAI

The Context

I built an autonomous outreach engine: a scheduled pipeline that discovers targets, enriches them from external APIs, ranks matches with structured LLM output, and ships personalized campaigns without human intervention. The deployment context was a global hiring platform, but the system pattern is domain-agnostic: prospecting, matching, and delivery wired into an existing product.

Inbound leads worked for the platform. Outbound didn't. Recruiters were still hunting founders on LinkedIn, matching candidates by hand, and writing cold emails one at a time. That manual loop was the first use case for the engine, not the definition of it.

I owned the system end to end: design the pipeline, wire it into the existing Next.js app, and ship it to production in six weeks. The goal wasn't a standalone cold email tool. It was an autonomous prospecting loop with email as the output layer: discover the right targets, rank against whatever signal existed, personalize outreach per lead, and let ops flip the engine on or off from the dashboard.

No new microservices, no greenfield repo, just a type-safe workflow built into the codebase the team was already running on Trigger.dev and PostgreSQL.


The Problem

The real leverage in personalized outreach is proactive matching at scale: find high-intent targets, pair them with the best-fit asset from your pool, and send something that feels specific, not spray-and-pray.

The manual workflow didn't scale:

  1. Operator finds a YC company that just hired a remote engineer
  2. Checks for an open role, picks the best match, writes a cold email
  3. Repeats five times a day

The product needed an autonomous prospecting system (discovery, enrichment, ranking, and delivery) while keeping humans in control of when the engine runs.

This is the same class of problem I write about in The Anatomy of a Production-Ready MVP: one core loop, end to end, with enough structure to trust in production.


What I Delivered

An end-to-end outreach engine, not a recruiting feature bolted onto a dashboard:

  • Scheduled orchestration: a Trigger.dev cron (every 12 hours) that runs discovery → enrichment → ranking → delivery without manual intervention
  • Signal-based targeting: company search filtered by funding stage, size, and a custom intent signal: recent remote hires from target regions
  • Dual ranking paths: GPT-4o structured output against job descriptions or existing team profiles when no job post is indexed
  • Personalized campaigns: Instantly integration with per-lead template variables and separate sequences by context
  • Ops control surface: dashboard toggle to activate/deactivate the schedule via API, no redeploy required
  • Dedup infrastructure: Fiber exclusion lists so the same target never re-enters the funnel

Architecture

The engine is four layers wired through a single orchestrator:

Rendering diagram
LayerResponsibility
DiscoveryFind ~5 high-intent targets per run, enrich with jobs, founders, and recent hires
MatchingBatch-rank every asset in the pool in parallel, pick the top match per target
DeliveryCreate and activate Instantly campaigns with context-specific copy
ControlDashboard toggle + Trigger.dev schedule management

Each enrichment step is fault-tolerant. Missing job posts or founder emails don't crash the run; they change which ranking mode and email template fire downstream.


Engineering Highlights

Signal over volume

Five companies per run, not five hundred. Each one gets full enrichment, full LLM ranking, and a personalized email. I optimized for targeting quality, not inbox flooding.

The key filter: companies that recently hired at least one engineer from India or LATAM in the last six months. That signal means they're already comfortable with remote hiring from target regions, warm before the first email lands.

Two ranking paths, one orchestrator

Not every company has a public job post. I built two modes chosen at runtime:

Mode A: rank against the job description when title and description exist.

Mode B: rank against recent employee profiles when they don't. The LLM scores how well a candidate fits the team the company already has.

// src/trigger/outbound.ts, runtime ranking strategy
if (hasJobData) {
  batchResult = await outboundRankApplicantTask.batchTriggerAndWait(
    applicants.map((applicant) => ({
      payload: {
        applicantId: applicant.id,
        jobDescription: jobDescription!,
        jobTitle: jobTitle!,
      },
    }))
  );
} else if (employees.length > 0) {
  batchResult = await outboundRankApplicantByEmployeesTask.batchTriggerAndWait(
    applicants.map((applicant) => ({
      payload: {
        applicantId: applicant.id,
        employees,
        companyName,
      },
    }))
  );
}

This turned "no job post, skip this company" into a viable angle: "Here's a candidate similar to [Engineer You Just Hired]."

Graceful degradation as a feature

No job post? Rank against employees. No founder email? Skip silently. No applicants in the pool? Exit early. The cron never crashes on partial data; it just produces less output.

Dedup is baked into the discovery step, not bolted on later:

// src/trigger/outbound.ts, exclusion list after each run
await fiberRequest("/exclusions/companies/add-to-list", {
  listId: process.env.FIBER_EXCLUSION_LISTID,
  companies: companies
    .map((company) => ({ domain: company.domains?.[0] }))
    .filter((c): c is { domain: string } => !!c.domain),
});

Without this, a twice-daily cron would eventually spam the same founders.

Batch parallelism for LLM calls

Ranking N applicants × M companies sequentially would be too slow. Trigger.dev's batchTriggerAndWait fans out ranking tasks and waits for all of them; the orchestrator stays simple while compute scales.

The scheduled entry point:

// src/trigger/outbound.ts
export const outboundEngine = schedules.task({
  id: "outbound-engine",
  cron: { pattern: "0 */12 * * *" },
  run: async () => {
    const companies = await companyData();
    const applicants = await loadApplicantPool();

    for (const company of companies) {
      // enrich → rank → queue lead
    }

    await activateInstantlyCampaigns(leads);
  },
});

Stack Choices

ComponentChoiceWhy
App shellNext.js + TypeScriptEngine integrated into existing dashboard, API routes, and typed boundaries
OrchestrationTrigger.dev v3Cron schedules, parallel batch tasks, retries, observability
Resume poolPostgreSQLShared candidate pool with PDF text extracted once, reused across rankings
Company dataFiber APIStructured company/people/job search + email enrichment
MatchingOpenAI GPT-4oStructured output with explainable match scores
Email deliveryInstantly APICampaign creation, lead injection, send scheduling

Same philosophy as DocPilot: pick managed services and clear layer boundaries over custom infra you don't need on week one.


In Practice

Every 12 hours (when enabled), the engine:

  • Discovers ~5 US startups showing remote hiring intent from target regions
  • Enriches each with jobs, founders, emails, and team profiles
  • Ranks the entire resume pool against each company in parallel
  • Emails founders with a link to the best-matched candidate
  • Marks those companies as contacted so they never re-enter the funnel

Recruiting was the deployment. The pattern (ingest a pool, discover targets, rank with LLMs, deliver personalized outreach on a schedule) applies to sales prospecting, partnership outreach, or any workflow where signal quality beats volume.

An operator uploads assets to the pool. The engine handles the rest.


Why This Matters for Founders

Most early-stage teams treat outbound as a manual ops problem or over-engineer it into a distributed system before they've validated the loop. The middle path is an autonomous prospecting engine inside your existing product: scheduled jobs, enrichment APIs, LLM ranking, delivery integrations, and an ops surface to turn it on or off.

This is what I mean when I say AI workflow engineering and outbound automation systems on my homepage: take a messy human loop, translate it into a reliable autonomous pipeline in weeks, and ship it with production-ready boundaries (typed APIs, fault-tolerant jobs, graceful degradation, ops controls) without enterprise bloat.

If you're building a SaaS product where automation is the moat (prospecting, matching, enrichment pipelines, background jobs at scale), this is the kind of systems work I take on. Recruiting was one use case. The skill is designing engines that run without you.


Related: DocPilot case study · The Anatomy of a Production-Ready MVP

Need a validation-ready MVP shipped in weeks?

Get in touch