Personal GTM Engine
An outbound engine that finds B2B startups likely to need GTM automation, enriches them in Clay, verifies their founders, drafts a grounded first message and waits for my approval in a private console.
- Role
- Solo: design, build, operation
- Period
- September to October 2026
- Status
- Running on demand on my laptop (n8n in Docker), with the review console on Vercel
- Stack
- n8n, Google Sheets, n8n Data Tables, Neon Postgres, Next.js, Vercel, Clay, Tavily, Groq, Gemini, Discord, YC directory, Hacker News API
- Links
- Repository
The problem
I needed clients for remote GTM automation work, and I wanted the search itself to prove I can do the work. Most outbound tools solve volume. My problem was the opposite: with free tiers only and a reputation to protect, every account, every contact and every sentence had to be right before it reached a person.
What I built
A set of n8n workflows that started on one shared Google Sheet, so I could inspect and correct anything between steps. Clay now does the enrichment it is best at, and drafts and my decisions live in Postgres behind a private review console.
- Engine: find and filterWrites contacts_queue
- Contact finderWrites contact_results
- Engine: draft and checkWrites outreach_queue
- Digest
- My reviewI approve, edit or reject each draft in the console, and send it myself.
Google Sheets and Neon PostgresSheets holds accounts, contacts, the queues and the Clay hand-offs, mirrored into Postgres. Postgres holds config, drafts and every review decision.
- Discover and filter. Companies come from Y Combinator's public directory, Hacker News hiring threads and a seed list. Plain rules run before any AI call: domain, team size, industry, keywords, timezone and seen-before.
- Score and enrich. Every account gets an explainable 0 to 100 score with its breakdown. A single website fetch detects the CRM and tooling the company actually uses. Accounts that score high enough go to Clay for structured research and, for the best ones, a recent trigger.
- Find the right person. Founders are read from the company's YC page and accepted only if that page lists the same website. Companies outside YC go to a search with company and location gates. Clay then looks for a verified work email for one founder per qualified account.
- Draft, check, review. A fallback chain of three Groq models and Gemini writes a LinkedIn note, a longer message and three follow-ups from facts only. A rule-based validator rejects bad drafts and asks for one repair. Discord pings me with a link, and I approve, edit or reject each draft in the console. Nothing is sent by the system.
- 234 Timezone outside UTC+0 to UTC+4Allowed UTC offsets: 0 to 4
- 86 Company no longer activeThe YC directory must list the company as active
- 27 Team size outside 2 to 150Team size between 2 and 150 people
- 5 Listed twice across directory tagsEach company is counted once per run
- 2 No website listedA website is required: accounts are keyed by their domain
- 234 Timezone outside UTC+0 to UTC+4Allowed UTC offsets: 0 to 4
- 86 Company no longer activeThe YC directory must list the company as active
- 27 Team size outside 2 to 150Team size between 2 and 150 people
- 5 Listed twice across directory tagsEach company is counted once per run
- 2 No website listedA website is required: accounts are keyed by their domain
| Step | Companies |
|---|---|
| YC companies in the run | 377 |
| Rejected: timezone outside UTC+0 to UTC+4 | 234 |
| Rejected: company no longer active | 86 |
| Rejected: team size outside 2 to 150 | 27 |
| Rejected: listed twice across directory tags | 5 |
| Rejected: no website listed | 2 |
| Moved on to scoring | 23 |
The same system on screen
The overview diagram is redrawn for reading. These are the workflows as they run, the sheet they write to and a draft as it reaches me.





Review moved off the sheet
Reviewing drafts in a spreadsheet became the bottleneck, and I could not risk breaking a working engine to fix it. So I did not rewrite it. A new sync workflow mirrors the sheet into Neon Postgres every 15 minutesSource: GTM Neon Sync schedule trigger (Every 15 Minutes), published version. 20 October 2026. and pushes my decisions back. Then the two tables that matter most for review moved over one at a time: the engine now reads its config from Postgres and writes new drafts straight there, each step behind a switch I can flip back.
The approval rules live in one Postgres function, not in the UI. A draft moves between pending, edited, approved, rejected, superseded and sent, and the database refuses every other transition: editing an approved draft sends it back for approval, and only an approved draft can be marked sent. Every action writes an audit row with the values before and after. Each table names who owns which field, and a console change carries a version stamp, so a change made while a sync runs goes out on the next run instead of being marked done by mistake (ADR-S10-3).
The console is a small Next.js app on Vercel's free plan, behind a single-owner password, and it works on my phone. Before the cutover the schema passed 47 checks against the real outreach queueSource: Sprint 10 guide, re-verification of 2026-10-17 (schema.sql applied twice, 58 outreach rows). 17 October 2026.. In production, a second sync in a row writes 0 new rowsSource: sync_ops id 37, n8n execution 233 (48 accounts, 60 contacts, config in_sync, no warnings). 20 October 2026..
Clay as the enrichment layer
At first Clay only wrote company summaries: three separate AI columns per account, and a person pasting the results back into the sheet. I rebuilt it as the enrichment worker. n8n still decides which rows go to Clay and what comes back (ADR-082).
One research call per account. A single structured column replaced the three, on the cheapest model that held up in a pilot: one structured AI call per account at 1.0 credit, down from 2.8Source: clay_ops C0 and C1 rows, Clay History. 14 October 2026.. Run conditions keep Clay off accounts below the score threshold: 10 low-score accounts skipped at 0 costSource: Accounts v2 table, Clay History. 14 October 2026..


Verified emails, not guesses. A Work Email waterfall with Conservative validation looks up one founder per qualified account, so a catch-all address never counts as found, and a guessed address is never marked verified without a validator check. It returned verified work emails for 19 of 20 founders, about 0.5 credit eachSource: clay_ops C2 rows, Clay History. 14 October 2026..

Every credit measured. With every Clay batch metered by n8n through Clay's Public APISource: clay_ops tab, workflows/clay_meter.json. 14 October 2026., the cost of a module is the difference between two rows in a log, not an estimate. Results come back through a CSV contract and n8n imports, with no hand pasting, and a second import writes 0 rowsSource: workflows/gtm_clay_bridge.json, workflows/clay_email_import.json. 14 October 2026..

A reason to write now. The latest addition gives each hot account one recent, sourced trigger: a relevant job opening first, then a funding round, then news (ADR-S11-3). It travels from Clay through the sheet and Postgres into the drafting prompt and the console, and the validator accepts it as a fact a draft may quote. The first run limited the job search to GTM titles and found a trigger for 2 of 12 hot accountsSource: Sprint 11 guide, Step 7 result. 21 October 2026.. Funding did no better: a funding round for 2 of 12, both older than a yearSource: Sprint 11 guide, Step 7 result (Intellizence and PredictLeads columns in Accounts v2). 21 October 2026.. Provider coverage for small startups was the limit, not the build, so I widened the job search to sales roles and then any role while keeping the order strict. The final run covers 6 of 12 hot accountsSource: Accounts v2 trigger_fact; Clay Trigger Import executions 247 and 248 (written 6, identical on the re-run). 21 October 2026..
Two details mattered more than the providers. The news provider's date filter applies to when it first saw an article, not when the article was published, so the formula checks the article date itself. And the drafting validator rejects any currency sign, so a funding amount would have pushed every draft to the template: the import rewrites amounts as a number and a currency code, and the validator stays as strict as it was (ADR-S11-7).
Decisions that mattered
ADR-004Deterministic gates before LLM and paid calls
- Context
- The project must cost $0 (NFR-01).
- Decision
- Hard filters (industry, timezone, headcount and the others) run before any enrichment, LLM call, Apollo credit or Clay credit. The project review restates the order for outreach: score, caps, channel rule, validator, then Groq, then Gemini, then the template.
- Trade-offs
- Some companies that look interesting but have incomplete data get dropped. More code, but spend stays at a predictable $0. The alternative was LLM-first.
ADR-076YC directory before search for founder lookup
- Context
- A semantic search on a company name cannot tell two companies with the same name apart. The finder picked the wrong people for five companies with common-word names, Twenty among them.
- Decision
- Read the public YC company page first, and accept it only when the website it lists equals the account domain. Rejected: Apollo or Clay people search (they cost credits) and LLM verification of each profile (cost, non-deterministic).
- Trade-offs
- Only covers YC companies, and it depends on YC's page markup. The
yc_lookupconfig switch turns it off, and anything unresolved falls through to Tavily.
ADR-S10-2Strangler sync first, then cut over table by table
- Context
- The engine had about twenty Sheets nodes. Rewiring them all at once would break a working system for days.
- Decision
- A separate sync workflow mirrors Sheets into Neon and pushes console decisions back, so the engine runs unchanged on day one. Then config and the outreach queue move to Neon one at a time, each behind a reversible switch.
- Trade-offs
- A temporary extra workflow, and review decisions reach the sheet only on the next sync until the outreach cutover.
What broke, and what I changed
The search found strangers. A people search for a company called Twenty returned people who worked somewhere else, because a company name alone is ambiguous. It happened at 5 companies with common-word namesSource: manual review of contacts. 7 October 2026.. I replaced name matching with a lookup that is verified by website domain. On the next run, 10 of 10Source: YC Founders node stats. 7 October 2026. lookups resolved from YC pages, at no search cost.
The validator was too strict. In one run, 28 of 35 LLM calls were rejectedSource: ops tab llm_calls=35 llm_failures=28. 7 October 2026.. Most rejections were word counts a few words short, not bad content. I relaxed the length rules, kept the factual checks, and added plain-language repair hints. In the latest run, 8 of 8 drafts passed, with no template fallbackSource: outreach_queue rows 38 to 45. 8 October 2026..
The model pitched companies their own product. With only a homepage description to go on, it treated what a company sells as a problem the company has. I added a deterministic product-term check that runs before and after the model: words the company repeats about itself may appear only in the opening observation.
Results
- founders verified from YC pages, 0 search credits
- 19Source: contact_results, provider=yc_directory. 7 October 2026.
- founders with a verified work email from a Clay waterfall with Conservative validation (4 of 4 guessed emails confirmed, 11.7 credits in total)
- 19 of 20Source: clay_ops C2 rows, Clay History. 14 October 2026.
- drafts passed the validator in the latest run, no template fallback
- 8 of 8Source: outreach_queue rows 38 to 45. 8 October 2026.
- messages sent without my approval
- 0Source: design: no send path; send_status column. Ongoing.
Numbers come from the engine's own run log. Every run appends counters and error samples to an ops tab, now mirrored in Postgres, and debugging starts there. Clay costs come from the meter's log.

What I would do next
- Measure reply rate on approved, hand-sent messages and feed it back into scoring.
- Let an approval send the message, behind a suppression list, a country gate and a daily cap enforced in SQL, once the console loop has run for a while (ADR-S10-9).