How I built an AI prospecting agent on Temporal to take the admin off BDRs

AUTHORS
Jeff Long
PUBLISHED
Nov 09, 2026
DURATION
15 MIN
  • AI/ML
  • Durable Execution
  • Temporal Primitives
TL;DR
Kepler is an AI prospecting agent built at Temporal by former BDR, now Sr. Technical GTM Operations Manager, Jeff Long with the goal to remove the manual admin from outbound sales. It watches for buying signals, finds the right contact, verifies eligibility (opt-outs, do-not-contact lists, open deals, current employer), and drafts an Outreach sequence plus a call brief, so the rep only edits and sends. Rules decide what kind of email to write, the model only writes it, and plain rule checks review every draft. The scan runs on Temporal as a Workflow with Child Workflows, Activities, Timers, Queries, and Signals, so a crash never enrolls a prospect twice and a scan resumes where it stopped.

One of my first experiences while onboarding at Temporal was watching a rep write the first email in a sequence. It was what you might imagine: add the person to an Outreach sequence, tab over to Claude, generate, copy, tab back, paste, next person... and then doing this dozens of times all morning. Super fun stuff (not). And those are only two of the dozen or so tools a rep works in, with a human as the connective tissue between them all.

Outreach in one tab and a Claude project in the other, with the rep carrying every first email back and forth by hand.

I know that seat all too well. My first job in tech was as a BDR, and the job hasn’t changed much since: your account executives want research, product wants you pitching the new thing, marketing has a campaign going out, you’re building your own list from a bunch of tools. Those add up until your queue is bubbling over with things that were due yesterday, and you’re not selling the way you want to.

Everything a rep opens before one first email goes out, and the rep’s the one moving stuff between them.

I’ve spent every job since trying to take that work off reps. I’d rather spend forever automating something than do it twice. At Amazon, where the stack was basically Salesforce and optimism, that meant Excel macros, then teaching myself SQL and Python to build lead routing and scoring for over a hundred account executives. At Snowflake I got into AI, back when a model maybe got a prompt right one time out of five. At Temporal I built Kepler, named after the space telescope, since its whole job is watching for signals.

The mechanics of the BDR AI agent#

Kepler begins by watching for a signal. That might be a job post that mentions Temporal, or a developer starting to try it. Sometimes the signal is the news instead, an outage or an AI launch. The signals come from Common Room, Reo.dev and the open web through Exa, and a Claude model decides which ones are worth acting on. As an example, let’s say the job post is Acme’s (Acme and Sam, who comes up next, are both fictitious examples).

Kepler’s next step is to find the people. Acme’s on one rep’s account list, so whatever comes out of this lands with that rep. Kepler looks for someone senior who builds or runs the kind of systems Temporal is for. Say that’s Sam Patel, who leads platform engineering. If Sam isn’t in Salesforce yet, Kepler finds him through our data vendors and verifies his email.

Then it checks whether Sam should be contacted at all. Is he opted out or on an account executive’s do-not-contact list? Has he met with us in the last year, or is he already in a sequence, or part of an open deal? If so, Kepler skips him. It also double-checks that he still works at Acme, which we learned the hard way after an email bounced on someone who’d left his company a year earlier.

Now, up until this point Kepler has only been reading, building a list of who it wants to reach and why. Once that’s done, Sam gets added to a Salesforce campaign named after the signal that found him, and his emails and call steps land in the rep’s Outreach queue.

One made-up account, from the signal all the way to send. The first three steps are all reads, and Kepler doesn’t touch Salesforce or Outreach until step 4.

The first email is all about Sam’s work and asks him one question. Each email after that adds something new on the same problem, like a customer story that fits or a quickstart if he’s a developer, and the ask comes in the last one. The rep also gets a call brief with the signal, any past contact and the company’s latest news.

The sequence, with the rep’s call brief next to it.

They’re still drafts, so the rep gives each one a read and makes it theirs before it goes out, and there’s no more tabbing back and forth to get there.

How the emails get written#

This is where I got stuck. A template is safe, but it always reads like a template. A model writing freely sounds more like a person, but it gets things badly wrong. The first two versions of Kepler were one and then the other, and getting past these issues took longer than everything else combined.

My first version called itself personalized and it was still a template. The third email was the same for everyone in a batch, and the model that wrote them told me the batch was a win. I only caught it because I was reading the drafts myself. So I let the model write freely, and it made claims about Temporal that weren’t true. Then I overcorrected with a safety pass that banned every specific, and the drafts went flat.

I took a step back and counted what our reps’ best hand-written emails did compared to Kepler’s drafts. The drafts said “I”, “we” and “Temporal” way more than “you” and “your”, and most opened with the sender introducing themselves. One of our reps told me the intros made everything feel like a sales email, and Gong’s research on cold email says pitching can cut your replies by more than half. The drafts read like a machine because they were mostly about us.

So we split the job. A rulebook decides what kind of email each person gets, and the model writes it. The sender changes the ask: a BDR saves a short call with the account team for the last email, an account executive can offer a meeting earlier, and a commercial rep with no BDR behind them offers to be a direct resource. The reader changes the story: a developer gets a problem told from their side of the work plus a quickstart, and a leader gets engineering time and what it costs their customers. And what they already know changes the offer: someone new hears it’s open source and free to start, someone already trying it hears about free Temporal Cloud credits, and a customer hears that teams at their company already use Temporal.

The rulebook picks the approach for each person before the model writes anything.

A batch also shouldn’t read like the same email over and over. The model can only use facts from an approved list, each with a source, and only customer stories marketing has cleared. Each fact has a few wordings, and the angle rotates so three people at one company don’t get the same email.

What the model gets handed for one account. Rules fill in every piece before it writes, and the pieces rotate across the people at that company.

After the model writes, every draft goes through rule checks, which is regular code with no AI in it. A subject line that says Temporal, or a sender introducing themselves, stops the draft. “I hope”, “just wanted to”, “no worries” and the “it isn’t X, it’s Y” line get flagged, because that last one is a glaring sign of model writing. And an email never mentions the activity that triggered it, since “Saw you reading our docs” would come off creepy.

Building it with Claude#

I didn’t go to school to be a software engineer. My degree is in business. With Claude I don’t have to smash my head against SQL or Python errors to get what I need. I explain what I want, we whiteboard it, and I can go back and forth on an idea without feeling like the person asking a stupid question.

Claude wrote most of the code: the vendor clients, the checks, the tests, the Workflow. I picked what Kepler does, wrote the rules it has to obey, read batch after batch of drafts, and said whether it worked. The BDRs and commercial reps who ran it told me when a draft was off, and Hayley Broder on our marketing team was in the trenches with me on the signals and the content.

I trusted Claude with the code, and with anything I could run in practice mode, which doesn’t actually write anything. The drafts I read myself until the rulebook was in place. And the rep always hits send.

Why the scan runs on Temporal#

We call one run across every rep’s accounts a scan, and a full scan takes most of a day. I work at Temporal, so of course it runs on Temporal now, but I’ll admit I was a novice at building on it. At first a script I wrote kept an eye on the scan by writing a heartbeat file every minute, and a run could die while the file still said “running.” Crashes meant restarting by hand, the scan stalled whenever my laptop slept, and the two 35-minute waits for Salesforce and Outreach to sync were just sleeps.

When I moved it over, I wrapped it instead of rewriting it. Each step became an Activity, which is Temporal’s word for one piece of work it runs and can retry on its own, and the Workflow decides the order and the waits. Zscaler described the same move in this series, wrap, don’t rewrite. Here’s the scan, condensed from the real code (it’s split across two files in Kepler, but these are the real settings):

RETRY = RetryPolicy(
    initial_interval=timedelta(seconds=5), backoff_coefficient=2.0,
    maximum_interval=timedelta(seconds=60), maximum_attempts=100,
    # Don't retry these. A person needs to look at them.
    non_retryable_error_types=["StepInterrupted", "StepLaunchFailed"],
)

@workflow.defn(name="KeplerScan")
class KeplerScan:
    @workflow.run
    async def run(self, scan: ScanIn) -> dict:
        # Find and qualify: one Child Workflow per kind of signal, in order.
        stages = await workflow.execute_activity(
            "list_stages", scan, start_to_close_timeout=timedelta(minutes=2),
            retry_policy=RETRY)
        for n, group in enumerate(stage_groups(stages), start=1):
            await workflow.execute_child_workflow(
                "KeplerPlayGroup", group,
                id=f"{workflow.info().workflow_id}/{n}-{group['key']}")

        # Write: Salesforce campaigns, drafts, Outreach sequences.
        await workflow.execute_activity(
            "run_go", scan, retry_policy=RETRY,
            start_to_close_timeout=timedelta(hours=4, minutes=15),
            heartbeat_timeout=timedelta(seconds=90))

        # Wait for Outreach and Salesforce to sync. This is a durable Timer.
        await workflow.sleep(timedelta(minutes=35))
        ...  # two read-only sweeps, prune, verify

Each kind of signal gets its own Child Workflow, run one at a time in a set order, so the strongest signal reaches a person first and nobody gets two Kepler sequences. The write step gets up to four hours and fifteen minutes, but it has to heartbeat every 90 seconds, so if the Worker dies Temporal knows within a minute and a half. And each 35-minute wait is now one line, workflow.sleep, which survives a Worker or server restart.

One actual scan from October, start to finish. The rows near the bottom are the kinds of signal, and each one is a Child Workflow. Every bar above them is a step, and the orange bar is the 35-minute wait as a Timer.

Picking up where it stopped#

The thing I worried about most was a crash in the middle of a scan. A full scan runs most of a day, and losing hours of finished work to one crash would hurt. On Temporal a new Worker picks up at the step that was running, and every step that already finished stays finished.

There’s one thing Temporal asks of you in return. Activities are at-least-once: if a Worker dies partway through one, Temporal runs it again. So every write in Kepler checks what’s already there before it acts, and if the answer isn’t clear, it holds the person for me instead of guessing.

Each step’s script records how it ended, so a retry checks that record before starting anything. If the script died without finishing, the retry stops and asks me. That’s the StepInterrupted on the do-not-retry list above. Here’s what that check looks like on a write, putting Sam in a sequence:

# Trimmed from enroll.py: putting Sam in the rep's sequence.

# Check first. In a sequence now, opted out, bounced, or had a
# Kepler sequence before? Hold him and write nothing.
prospect = safety.clear_to_enroll(client, sam["email"])

# Write down "about to enroll Sam" before touching Outreach. The note
# can only be created once. If it's already there, an earlier try may
# have landed, so hold him and don't guess.
safety.claim_write(STORE_DIR, sam, scope)

prospect_id = prospect["id"]
enrollment = client.enroll(prospect_id, sequence_id, mailbox_id)

# Then read it back, and only count it if Outreach agrees on the
# person, the sequence and the rep's mailbox.
state = client.get(f"sequenceStates/{enrollment['data']['id']}")
if relationships(state) != (prospect_id, sequence_id, mailbox_id):
    raise safety.Hold("enrollment readback mismatch")

The four steps around one write. If a crash hits anywhere in there, the retry can tell what already happened, and anything unclear gets held for me.

Temporal also took over things I used to do by hand. A Query tells me where a scan is, which the heartbeat file never could. A Signal tells it to finish the step it’s on and stop, without interrupting a write. And since I change the Workflow while scans are running, changes go in behind workflow.patched, and a test replays recorded scan histories before anything merges.

Before going live I tried to break it on purpose. I killed the Worker during a Timer and restarted the server, and the Timer still fired right on time. A new Worker picked up where the old one left off, no step ran twice, and every full scan since has run on Temporal.

Where it is and what’s next for this agent#

Kepler is live for the BDRs and the commercial reps, and when a scan can’t finish, it tells me where it stopped. It’s still new, so I’m not going to make claims about results yet, and honestly I don’t think meetings are the point anyway. Reps were booking meetings before Kepler and still are. I always meant for Kepler to take the admin off reps so they have more time to sell. It does that for everyone at once, faster than anyone could by hand.

Next up is a Slack alert with a Prospect button that kicks all of this off for one account, then the rest of the go-to-market team that works out of Outreach, account executives included.

If you’re an ops person who wants to build one#

I’d start from the grind you remember. I knew the BDR day because I’d lived it, and that told me which decisions to take off the rep’s plate first.

Then push past what’s comfortable. Off-the-shelf tools can do a lot of what Kepler does, and they’re the easy choice. Building it ourselves paid off on the parts a tool makes you work around, like checks that span several systems and a rulebook that knows who’s sending. It took longer, sure, but I’d take that control over speed, because that’s where the quality came from.

The “agent” part of Kepler is really just two pieces, the model that picks signals and the one that writes. Everything else is plumbing. Build the plumbing first, with a practice mode that doesn’t write anything and a cap on spend. Then start small, like one signal and ten people with one person sending.

You can get a system eighty percent of the way there now, and the last twenty percent is where almost all your time goes, mostly on small bugs and learning how each vendor’s API behaves. A lot of people stop there, but a sales team can’t run on something that works eighty percent of the time.

I spent a lot of early mornings this summer and most of my weekends on that last twenty percent, and I had help from the reps who ran it and from marketing, who cleared every customer story.

I’m proud of this one. If you’re building something like it, make all the mistakes you need to. Mine made Kepler better. And today the same rep I watched in week one can spend the morning talking to people instead of carrying work between tools.

Talk with the team about your build. We’re always around and ready to chat in our community Slack.

Temporal Cloud

Ready to see for yourself?

Sign up for Temporal Cloud today and get $150 in free credits.

Build invincible applications

It sounds like magic, we promise it's not.