⭢ POST DETAIL
⌃+G

How I Built A Custom GTM Agent

March 20, 202617 min read

How I Built an AI Agent That Runs My Entire Startup's Go-To-Market Loop

I recently created a experimental beta product called VentureScope - an AI-powered deal flow management and due diligence platform for venture capital firms. Like most early-stage founders, I was spending an enormous amount of time on the mechanics of sales: researching investors, writing outreach emails, following up, reading replies, drafting responses, and trying to close the feedback loop on what our users actually wanted.

So I built an AI agent to do all of it.

Not a chatbot. Not a CRM plugin. A fully autonomous, self-improving AI system that discovers investors, sends outreach, reads inbound replies, drafts responses for my approval, processes user feedback into GitHub PRs, and updates itself every single day.

I call him Conrad.


What Conrad Is

Conrad is a Cloudflare Worker, a serverless TypeScript runtime that runs at the network edge, powered by Anthropic's Claude models. Conrad has access to it's own Gmail, Google Sheets, Google Drive, GitHub, and a suite of third-party APIs. He runs on a cron schedule, responds to webhooks in real time, and can be reached via a Telegram bot or a custom admin dashboard.

He is my AI head of business development.

The architecture is what I call a hub-and-spoke multi-agent system:

  • Strategist (hub): Claude Opus 4.6. The CEO. All tasks route through here first. It delegates, reviews, and makes final calls.
  • Executor (spoke): Claude Sonnet 4.6. Handles company research, outreach drafting, and proposals.
  • Ops (spoke): Claude Sonnet 4.6. Manages the CRM, pipeline, monitoring, and autonomous cron cycles.
  • Coder (spoke): OpenAI gpt-5-3-codex. Handles all code implementation tasks, including writing code to the VentureScope GitHub repo.

Everything is backed by Cloudflare's infrastructure: D1 (SQLite at the edge) for structured data, KV for fast key-value lookups, R2 for document storage, Vectorize for semantic memory, and Durable Objects for the real-time WebSocket dashboard.

Conrad runs three main loops every day.


Loop 1: The Outbound Investor Outreach Pipeline

This loop runs Monday through Friday, fully autonomously. By 6 AM Eastern each morning, a fresh batch of investor contacts is waiting in a Google Sheet for me to trigger a send.

Autonomous Discovery (9:00 AM UTC)

Conrad starts with the discovery cycle. Using Exa.ai, a neural search engine built for AI applications, Conrad runs a series of semantic queries against the web:

  • "seed-stage VC firms investing in B2B SaaS"
  • "venture capital partners focused on fintech and enterprise software"
  • "early stage investors interested in AI tools for financial services"

These aren't keyword searches. Exa uses a neural model trained to understand intent, so "B2B SaaS investor" returns actual venture capital firms rather than blog posts about B2B SaaS investing.

Each batch of results lands in a discovery_candidates table in D1 with a status of raw. Conrad's running a funnel.

Email Enrichment

Raw candidates are just names and domains, so Conrad passes each one to Hunter.io, which returns a verified email address, a name, a confidence score, and a job title for the most senior person at that firm.

Candidates without a verified email (or with confidence below 50%) are dropped. The rest move to enriched.

Qualification Scoring

Conrad scores every enriched candidate against a weighted rubric:

SignalWeight
Verified email+0.30
Has a name+0.15
Title matches investor keywords (partner, principal, director…)+0.20
Focus area keywords match (fintech, SaaS, B2B, enterprise…)+0.05 each

Score ≥ 0.5 → qualified. Score < 0.5 → discarded.

Qualified candidates are promoted to the contacts table with warmth: cold, ready for outreach.

Sheet Prep (10:00 AM UTC = 6:00 AM EDT)

Conrad queries the contacts table for the freshest, highest-seniority, most-verified cold contacts and appends them to a Google Sheet. The selection logic ranks by title seniority (Managing Partner > General Partner > Principal > VP > Associate), then by Hunter confidence score, then by whether they have a firm name. Contacts already emailed in any prior batch are excluded via a sheet_outreach_log dedup table.

The daily limit is configurable via a KV key (sheet_prep:daily_limit), defaulting to 100.

After appending to the sheet, Conrad sends me a Telegram notification:

*Sheet prep complete* — 100 investors queued

Sample: ABC Fund, XYZ VC, +97 more

Run `sendEmails()` in the App Script to send today's batch.

The Human-in-the-Loop Send

I open Google Apps Script, click run on sendEmails(), and walk away. The script reads every row in the sheet where send_status = pending, composes a personalized email, sends it via Gmail's MailApp, and updates the status column to sent or error.

This is the one step I do manually - intentionally. I want to verify the batch looks right before the emails go out.

CRM Sync (2:00 PM UTC = 10:00 AM EDT)

At 2:00 PM UTC, Conrad runs the outreach sync. He reads back the Google Sheet, finds every row where send_status is now sent, looks up the corresponding contacts in D1, and updates them:

  • warmth: cold → contacted
  • last_contacted_at = now()
  • Logs an outreach touch in the outreach_log table

Now the CRM is accurate. Those contacts won't get selected again in tomorrow's sheet prep - and when they reply, Conrad already knows who they are.

One full outbound cycle, from anonymous domain to tracked outreach, takes approximately 18 hours. Zero humans involved after I click run on the App Script.


Loop 2: The Inbound Reply Pipeline

Every email that lands in Conrad's inbox is processed within seconds.

Gmail Push Notifications

Rather than polling the inbox on a schedule, Conrad uses Gmail's Pub/Sub push API. Google notifies Conrad's webhook (/api/webhooks/gmail) within seconds of a new email arriving. The Cloudflare Worker receives the notification, decodes the Pub/Sub envelope, and kicks off the inbound monitor immediately.

There's also a * * * * * (every-minute) cron trigger as a fallback, so if the push webhook misses something, it's caught within 60 seconds.

Incremental Inbox Scanning

The inbound monitor uses Gmail's history.list API rather than messages.list. The difference matters: history.list takes a historyId - a cursor Conrad stores in KV after every run - and returns only the messages that arrived since the last check. On a quiet morning, this API call costs almost nothing. On a busy day with 50 replies, it fetches exactly 50 new message IDs.

Classification

For each new message, Conrad classifies it using a rule-based system first (fast, free), then falls back to Sonnet for ambiguous cases:

ClassificationMeaning
replyA response to outbound outreach we sent
new_inquiryCold inbound from someone who found us
auto_replyOut-of-office, vacation responder
bounceDelivery failure, mailer-daemon
unsubscribeReply with just "unsubscribe"
feedback_formForwarded from support email (more on this below)

The rule-based classifier catches the obvious cases: Re: prefix → reply, mailer-daemon in sender → bounce.

Contact & Deal Matching

Conrad queries D1 to match the sender's email against the contacts and deals tables. If there's a match, the inbound record is linked - so when you look at the dashboard, you see:

Reply from Contact XYZ at ABC Capital Linked deal: ABC Capital (stage: engaged) Warmth: warming → engaged (just upgraded)

If there's no match, Conrad notes "unknown sender" and processes it as a new inquiry.

Warmth Upgrade

Any reply or new_inquiry triggers an automatic warmth upgrade on the linked contact:

cold → warming
contacted → warming
warming → engaged
engaged → hot

An investor who was cold this morning and just replied is now warming. The CRM updates itself in real time, without me touching anything.

Auto-Draft Reply

For every reply or new_inquiry, Conrad reads the full email body via the Gmail API and calls Sonnet to draft a reply. The prompt instructs Sonnet to:

  • Write under 150 words
  • Lead with value, not a pitch
  • Address what the sender actually said
  • Suggest a demo or call if they expressed interest
  • Write like a human, not an AI
  • Sign off as Conrad Reeve

The draft is stored in D1 with status: pending_approval. Nothing goes out without my approval.

Telegram Notification

Conrad sends me a Telegram message immediately - regardless of time zone or what I'm doing:

*New Reply* [HIGH PRIORITY]

From: XYZ Contact <XYZ@contact.com>
Subject: Re: I built a deal screening AI
Known contact: XYZ Contact at ABC Capital (warmth: engaged)
Linked deal: ABC Capital (demo_scheduled)

Preview: "Interesting timing - we've been evaluating tools like this.
Happy to take a look. What does the pricing look like?"

Draft Reply (pending approval):
"Hi XYZ, great to hear from you. 

We're offering early-access pricing to select users. I'd love to share the details on a quick call.

Are you free this week? — Conrad"

draft-approve-a3f9b21c
draft-reject-a3f9b21c

Approve and Send

I reply to Conrad's Telegram message with draft-approve-a3f9b21c. Conrad:

  1. Looks up the inbound email and linked draft by the short ID
  2. Calls gmailSend() with the draft body, in the same Gmail thread
  3. Updates inbound_emails.status → responded and drafts.status → sent
  4. Replies: *Draft sent* to XYZ@contact.com

Alternatively, I can open the admin dashboard, navigate to the Inbound tab, see the email card with the draft preview, and click Approve & Send there. The WebSocket gateway handles it and updates the UI in real time.

From investor reply landing in Gmail to my-approved response being sent: under 60 seconds if I'm at my desk, whenever I next check Telegram if I'm not.


Loop 3: User Feedback → GitHub PR

This is the loop I'm most proud of. It closes the gap between what VentureScope users want and what the codebase actually does - with a human approval gate at every step.

The Email Path

VentureScope's website has a feedback form. When a user submits it, the form sends an email to our support inbox. Gmail auto-forwards that email to Conrad.

Conrad's inbound monitor detects these before they reach the investor pipeline. The detection logic:

  • Sender check: Does the From: match support, noreply, feedback?
  • Subject check: Does the subject match patterns like "new feedback", "contact form", "form submission", "website message"?

If either matches, the email is routed to processFeedbackFormEmail() and never touches the inbound_emails table. The investor inbound view stays clean.

Body Extraction

Conrad reads the full email body and calls Sonnet to extract structured data from the form format:

json
{
  "feedback_text": "I'd love to see PDF export for deal reports. We share these with our LPs and having to screenshot everything is painful.",
  "submitter_name": "John Smith",
  "submitter_email": "john@acme.vc"
}

Sonnet understands that feedback form emails have fields like Name:, Email:, Message: in the body and knows how to extract them.

Acknowledgment

If Sonnet found submitter_email in the body - an actual email address, not the forwarding address - Conrad immediately sends an acknowledgment from its inbox:

Hi John,

Thank you for your feedback on VentureScope. We've received your message and our team will review it shortly.

Best,
Conrad
VentureScope

The user gets a human-feeling response within seconds of submitting the form.

Feedback Pipeline

The extracted feedback text is inserted into the vs_feedback D1 table with source: email_form. This kicks off a multi-stage pipeline:

Stage 1: Triage (next 09:00 UTC run)

Conrad calls Sonnet to classify and score the feedback:

json
{
  "category": "feature_request",
  "priority": "high",
  "triage_summary": "PDF export of deal reports for LP sharing",
  "triage_reasoning": "Core workflow pain point — affects sharing with LPs, which is a critical use case for VC firms",
  "affected_area": "reporting"
}

Stage 2: Implementation Plan

Conrad calls Sonnet again to generate a concrete plan - specific files, code changes, acceptance criteria. This plan is stored separately from the raw feedback text. The raw user feedback never reaches the Coder agent. Only the structured implementation plan does. This prevents prompt injection: a malicious user crafting a "feedback" message designed to get Conrad to write arbitrary code hits a dead end - the Coder only sees the sanitized, structured plan.

Stage 3: Telegram Approval Request

*New Feature Request* [HIGH]
PDF export of deal reports

Summary: Users want to export pipeline reports as PDFs for LP sharing.
Affected area: reporting
Estimated effort: ~4 hours

Implementation plan ready. Coder agent standing by.

vs-approve-f7a2d8e1
vs-reject-f7a2d8e1

I reply vs-approve-f7a2d8e1.

Stage 4: Coder Agent Execution

Conrad delegates to the Coder agent (OpenAI gpt-5-3-codex) via a direct API call. The Coder receives the implementation plan and a set of tools for reading and writing to the VentureScope GitHub repository:

  1. Creates a branch: feature/vs-f7a2d8e1-pdf-export
  2. Reads the relevant source files from GitHub
  3. Writes the implementation
  4. Opens a Pull Request with a structured description

Stage 5: PR Notification

*PR Ready for Review*

Feature: PDF export of deal reports
Branch: feature/vs-f7a2d8e1-pdf-export
PR: https://github.com/org/venturescope/pull/42

Review and merge when ready.

I review the code, merge the PR, and the feature ships.

From user feedback submitted on the website to a GitHub PR ready for review: fully autonomous, one human approval gate, typically complete within the same business day.


The Self-Improvement Systems

Conrad also runs five meta-systems that make him incrementally better over time.

Daily Retrospective (8:00 AM UTC)

Every morning, Conrad reviews the previous day's audit log - every email processed, every draft created, every outreach batch sent, every error encountered. He calls Sonnet with a prompt that asks: "What is one concrete thing that can be improved today?" He then implements that one thing - adjusting a prompt, fixing a pattern, updating a threshold - and logs what he did. The retrospective runs once per day via a KV flag and is deliberately limited to one change per cycle. Compounding small improvements every day is more reliable than big infrequent rewrites.

Agent Quality Review (7:00 AM UTC, Weekdays)

The Strategist runs on Opus 4.6, the most capable model in the system, and reviews recent outputs from the Executor and Ops agents. It looks for failures in tone, reasoning errors, missed context, or opportunities to do better. Corrective guidance is stored as semantic memories in Vectorize and recalled in future sessions. The agents are, in effect, continuously evaluated and coached by a more capable version of themselves.

Blocker Detection (9:00 AM + 2:00 PM UTC, Weekdays)

Five targeted D1 queries run twice a day looking for stuck processes:

  • Drafts pending approval for more than 48 hours
  • Campaigns with no sends after 24 hours
  • Experiments that haven't concluded after their scheduled end date
  • Deals with no activity in 14+ days
  • Inbound emails that were never triaged

Auto-resolvable blockers (like resetting a stuck experiment) are handled automatically. Others surface as Telegram alerts.

Cron Health Monitor (10:30 AM + 3:30 PM UTC, Weekdays)

Conrad monitors itself. A health check runs twice daily, querying the audit log to verify that each expected cron job actually fired. If the morning sheet prep didn't run, or the retrospective missed a day, the health monitor detects it and either auto-recovers or fires a Telegram alert. This was the first self-monitoring system I added after a cron expression bug — Cloudflare's day-of-week numbering starts at 1=Sunday, not Monday, so my "weekday" jobs were running Sunday through Thursday for two weeks before I noticed.

Memory Consolidation (9:00 AM + 2:00 PM UTC, Weekdays)

Raw interaction data - conversation snippets, email outcomes, deal notes - flows into Vectorize (Cloudflare's vector database) as embeddings. A consolidation cycle aggregates raw memories into tactical summaries ("investors from Tier 1 firms respond better to subject lines that reference specific portfolio companies"), and tactical summaries into strategic patterns ("outreach sent Tuesday–Wednesday morning has 23% higher open rate than other days"). These patterns are retrieved semantically during future interactions.


The Admin Dashboard

All of this is visible, and controllable, through a real-time admin dashboard built with Lit (a lightweight web components library) and served directly from the Cloudflare Worker.

The dashboard connects via WebSocket through a Cloudflare Durable Object, which maintains a persistent connection and broadcasts state changes in real time. Token authentication works via URL hash fragment (/admin/#token=YOUR_TOKEN) or a password input field.

  • Pipeline view: Kanban board of all active deals with 30-second polling and real-time WebSocket updates when Conrad moves a deal.

  • Inbound view: Every inbound email, classified, with sender/company/deal context, and draft reply preview. Approve & Send button wired directly to Gmail via the WebSocket gateway. Refreshes automatically when Conrad processes new emails.

  • Chat: Live streaming conversation with Conrad (Opus 4.6). Responses stream token by token, tool calls visible in real time.

  • Activity: Full audit log of every action Conrad has taken, with timestamps.

  • Agents: Status of all four agents (Strategist, Executor, Ops, Coder) and their active skills.


The Numbers

Everything runs on Cloudflare's infrastructure. Monthly cost at current scale:

ComponentMonthly Cost
Cloudflare Workers (compute)~$5
D1 database~$0 (free tier)
KV + R2 + Vectorize~$2
Anthropic API (all agents + crons)~$40–60
Exa.ai (discovery searches)~$3
Hunter.io (email enrichment)~$49 (plan)
Total~$100–120/month

For a system that handles investor discovery, outreach tracking, inbound reply drafting, user feedback processing, and autonomous code implementation, that's a low fixed cost.


What I Got Wrong the First Time

  • Rate limiting from the start. I added per-category rate limiting late in development. It should be in from day one, both to prevent abuse and to protect against Conrad accidentally hammering external APIs if something goes sideways in the tool loop.

  • Taint tracking earlier. The security layer that prevents external content (emails, web pages) from influencing which tools Conrad can call was an afterthought. I call it coarse-grained taint tracking: any external read blocks dangerous tools (like memory writes) for the entire run. It works, but designing the trust model upfront would have been cleaner.

  • The send limit should always be configurable. I hardcoded 100 in the first version. It reads from a KV key - a single command updates it without touching code.

  • Idempotency everywhere. Cron jobs run on schedule and things fail. Every autonomous job now checks a KV idempotency key at the start. sheet_prep:daily:2026-03-19 = 1 means "this already ran today - skip." This prevents double-sends if a cron fires twice or a deployment coincides with a scheduled trigger.

  • Check your cron expressions. Cloudflare's day-of-week numbering starts at 1=Sunday, not Monday. My "weekday" expressions (1-5) were running Sunday through Thursday. The cron health monitor I'd built eventually caught it, but only after two weeks of the wrong schedule. Verify your crons actually fired on the expected days, not just that they ran.


What Running This Looks Like Day-to-Day

At 6 AM Eastern, my phone buzzes: "100 investors queued." I scroll through the sample names - the sheet fills in overnight — verify it looks right, open the App Script, and run sendEmails(). That takes about 90 seconds of my attention.

When a reply comes in - sometimes within hours of the send - I get another Telegram message with the full context and a draft response. I read the investor's email, decide if the draft is on point, and approve it or send a quick edit. Under two minutes if the draft is good.

At the end of each day, I spend maybe 10 minutes in the admin dashboard reviewing what moved in the pipeline, what came in overnight, whether anything is waiting on me. Then I close the tab.

That's mostly it. The rest runs itself.


I spent about three weeks building Conrad, and I've been running him for two weeks now. My response time to investor replies is under 60 seconds. My VentureScope users get same-day acknowledgments and often same-day PRs on their feedback.

The loop is closed.


VentureScope is an experimental AI-powered deal flow management and due diligence platform for venture capital firms. Conrad is the experimental AI agent that runs its go-to-market motion.


Written by Marc17 min read

Discussion

Comments are powered by GitHub Issues. Join the conversation by opening an issue.

Add Comment via GitHub