GTM EngineArchitecture
Phase 0 · Registry · v0.1

A GTM engine that keeps the human in the loop.

A stateful, multi-stage outreach pipeline: it ingests a company dossier and a chosen angle, assembles a previewed email cadence and LinkedIn sequence, creates campaigns without launching, then polls the sending tools and enforces suppression across both channels. The engine owns state, sequencing and safety. The LLM only writes copy. The tools do the sending.

Explore the architecture
10pipeline stages
v1create & preview only
2channels, one stop rule
1SQLite source of truth
01

What this is

Three cards keep the scope honest. If a request pushes work into the right-hand card, it is out of scope for v1.

◎

It is a state machine

A deterministic owner of state and sequencing. It ingests evidence, selects pointers, assembles a cadence, previews it, creates campaigns, polls the tools, enforces suppression and routes human tasks.

⊘

It is not an autonomous SDR

It never sends without approval, never writes replies, is not a CRM and is not a full web app in v1. HubSpot stays the CRM; the Context Archive is read-only history.

◷

Where it stands

Phase 0 is nearly done: company registry, domains, versioned dossiers, Context Archive sync, audit log, a stdlib control site and Apollo enrichment. Angles, planning and sends are next.

built & working in progress planned
02

System at a glance

Evidence flows into one engine process backed by SQLite. The engine talks to the sending tools and reads their state back; it never assumes a send happened.

03

Three planes, never mixed

Ingress, engine and tools each have one job. v1 needs no public ingress because everything is polled; webhooks are an optional later fast path.

Plane 1 · optional

Ingress

  • Receive, authenticate, persist, acknowledge.
  • No business logic.
  • v1: none — five-minute polling.
  • Later: Cloudflare Worker or Tunnel.
Plane 2 · all decisions

Engine

  • Python and SQLite on the VM.
  • All state, copy, suppression and tasks.
  • Idempotent and replayable.
  • One instance; a lock row per run.
Plane 3 · sending only

Tools

  • Instantly and HeyReach send.
  • The engine reads their state back.
  • Suppression is executed in the tool first.
  • Then recorded locally.
Latency is explicit. v1 promises a 5–10 minute stop path (tool delivery plus the next cron), not “instant”. The status surface shows the last successful poll and the age of the oldest unprocessed event.
04

Pipeline stages

Each stage is a command the runner can call and a row or table it can write. Stages 1–3 exist today; enrichment is partial; the rest is the build ahead.

STAGE 01
Intake
Create company, mint or link a UID, normalize domains, pull Notion properties and fixed sections.
built
STAGE 02
Context
Fetch archive accounts and events, attach aliases, upsert events, track freshness and domain conflicts.
built
STAGE 03
Dossier
Validate and version the dossier JSON; store the evidence paths used by pointer selection.
built
STAGE 04
Enrich
Apollo → Hunter → profile crawl → Hunter. Accept verified and catch-all; drop leads with no email.
partial
STAGE 05
Angle
Load a versioned angle, audience gates, persona band and pointer rules; warn or reject by company type and geography.
planned
STAGE 06
Select pointers
Required first, then optional by weight until the step budget. Drop pointers with no evidence; never invent.
planned
STAGE 07
Generate copy
LLM fills templates from selected pointers and proof only. Validate digit and entity claims, plus length limits.
planned
STAGE 08
Plan & preview
Assemble email and LinkedIn steps, resolve variables, show dropped pointers and request human approval.
planned
STAGE 09
Create
Upsert shared campaigns and leads in Instantly and HeyReach. Store external ids. Never activate in v1.
planned
STAGE 10
Poll & suppress
Ingest events, execute stops in both tools, create tasks, update the audit trail and status surface.
planned
05

Data model · schema v2

One SQLite database is the single source of truth. It is gitignored, backed up nightly, and grows additively as phases land.

TableKeyHoldsStatus
metakeySchema version and engine metadata.built
companiesuidOne row per company: name, primary domain, country, motion, uid mode.built
domainsdomainEvery domain or alias mapped to exactly one UID; is_primary marks the entered domain.built
dossiers(uid, version)Versioned JSON evidence for the company. Re-import bumps the version.built
context_accountsuidRaw account summary, including the legacy guardrail block that is stored but ignored.built
context_eventsevent_idArchive events upserted on id; incremental cursor per UID.built
local_eventsautoAppend-only audit log: company created, domain linked, dossier set, schema migrated.built
peopleperson_idHumans keyed by normalized email and LinkedIn URL; history survives a company change.planned
campaignscampaign_idAngle and sequence pins, scope, channel, external ids per tool.planned
leads(campaign_id, person_id)Membership, step, send state, suppression state, per-lead variables.planned
taskstask_idManual steps and reviews: company, person, campaign, step, due date, payload, allowed actions.planned
assetsasset_idIntel registry: kind, path or URL, checksum, approved. Per company, not per lead.planned
06

Identity

Companies are anchored to domains, never names. People are anchored to both email and LinkedIn so either channel can stop the other.

Company UID
v1 provisionalTMP-000001, sequential, minted in one transaction. Human-readable, never external truth.
v2 permanentco_ plus the first ten characters of a base32 hash of the primary domain, stored in a column so it can migrate.
AnchorThe domain entered into the engine stays primary. Archive domains attach as aliases and never rename the company.
ConflictA domain owned by another UID is reported, never silently moved. Merge is a deliberate command.
Person
KeyNormalized email and LinkedIn URL, both nullable but at least one required.
LinkBelongs to a company UID but keeps history if the company changes.
StopA meeting or reply on either channel stops both channels for that person.
ErasureDelete the person; tombstone the email in a blocklist so it can never be re-added.
Domain normalization
lowercase → strip scheme, path, port, userinfo, leading "www.", trailing dot
reject anything that is not [a-z0-9.-]+ containing a dot
one domain → exactly one UID
07

Campaign & sequence model

Campaigns are shared per angle × variant × channel × EU split. Company-specific content is injected as per-lead variables and tags, not by cloning campaigns.

Naming · reconciliation depends on it
oe:{angle}:{channel}:{variant}:{scope}

oe:noa-1:email:ev1:global
oe:noa-1:email:ev1:eu        # EU-HQ companies
oe:noa-2:linkedin:lv1:global

Today’s catalog fits in roughly 16 campaigns (2 angles × 2 EU splits × 4 variants), not one per company. A company is a tag or list plus custom variables.

Sequence file · email
{
  "id": "email-v1", "version": 1,
  "channel": "email", "tool": "instantly",
  "delay_unit": "calendar_days",
  "steps": [
    { "n": 1, "action": "auto", "template": "pitch",       "delay_days": 0 },
    { "n": 2, "action": "auto", "template": "offer_intel", "delay_days": 2 },
    { "n": 3, "action": "auto", "template": "send_intel",  "delay_days": 1,
      "requires": ["intel_asset"] },
    { "n": 4, "action": "auto", "template": "drop_off",    "delay_days": 3 }
  ],
  "stop_on": ["reply", "meeting", "bounce", "unsubscribe"]
}
RuleBehaviour
DelaysCount from the previous step’s send for that lead, in US business days. US federal holidays are skipped.
Manual actionsaction: manual creates a task and does not advance the lead until confirmed.
requiresA hard gate. An automated step needing intel cannot activate until the asset exists and is approved.
Version pinsA campaign stores angle id and version plus sequence id and version. Editing an angle never mutates an in-flight campaign.
PacingEight leads per company per day, no total cap. Stop only on lead exhaustion, reply, meeting, pause or campaign end.
Variant choiceAngles are saved; a campaign may also use a one-off angle. Reusing the same company within 60 days warns and needs a written reason.
08

Suppression & stop rules

Actions are executed in the tools first, then recorded locally. Every action is idempotent. Legacy archive guardrails are ignored and stored only.

SignalSourceAction
reply_receivedInstantlyClassify; stop the lead on both channels; create a review task.
lead_interestedInstantlyStop both channels; positive-response row; task.
lead_not_interested / neutralInstantlyStop the lead on both channels.
auto_reply_receivedInstantlyStop; pause if a return date is known.
lead_out_of_officeInstantlyPause; resume task after the return date.
email_bouncedInstantlyStop; delete from campaign; invalidate and blocklist the email; try one alternate.
lead_unsubscribedInstantlyStop; global blocklist; never re-add.
lead_wrong_personInstantlyStop; flag for data quality.
lead_meeting_bookedInstantlyCompany-level stop: pause that company’s campaigns; if shared, delete all its leads; stop both channels.
HeyReach reply / connection replyHeyReachStop that lead on both channels; create a review task.
Meeting · any channelManual command or Telegram buttonSame company-level stop.
Context declined / excludedContext API · legacyIgnored by default — stored raw, never a gate.
Kill switch. One command pauses every engine-owned campaign in both tools and sets a global paused flag. Nothing resumes without an explicit command. Unsubscribe and footer work is still a legal blocker for real sends (CAN-SPAM / GDPR).
09

Tasks, intel & human-in-the-loop

Every manual step is a first-class object. A manual action blocks the next step until a human confirms it.

Task engine

States: pending → in_progress → done | cancelled, plus snoozed and overdue.

TypePurpose
create_intelProduce a one-pager, demo video, intel image or table.
send_linkedin_manualManual LinkedIn step; the lead does not advance until confirmed.
confirm_manual_sendThe only way a manual step advances.
review_replyClassify and answer a reply; sequences already stopped.
review_draftApprove generated copy before push.
fix_enrichmentMissing or ambiguous email or identity.
enforce_suppressionManual stop that must be applied in a tool.
resume_oooResume after a return date.

Intel is a launch gate

Intel is produced and approved by a human. A sequence with requires: ["intel_asset"] cannot activate until the asset exists.

  • Registry per company, not per lead: kind, path or URL, checksum, approved.
  • Email: attachment or hosted link, recorded in the plan.
  • LinkedIn: manual by definition — the task carries the URL, exact text and asset link.
  • Task created at plan time, due 48 hours before the step.
  • Assets live on the VM or in exports with a nightly backup; never next to inputs.
Copy safety: the proof library is the only source of claims. The model may not invent a number; a validator rejects digits or entities that are absent from the evidence set.
10

Surfaces

One engine, four ways to touch it. The engine is always the single writer; surfaces call it, never write state directly.

SurfaceWhat it doesStatus
CLI / runnerJSON on stdout, errors on stderr. Commands cover init, UID mint, status, company add / link-domain / list / show, dossier set / show, and context sync / show.built
Control siteServer-rendered dashboard, companies, company detail, angles, campaigns and settings. Basic auth, CSRF, loopback by default. Reads SQLite; writes through the engine CLI.built
TelegramLong polling. Task notifications with inline confirm and review buttons. No public URL needed.planned
Status surfaceOne page refreshed from the tools: key validity, campaign counts, bounce rate, last poll, oldest unprocessed event, overdue tasks and spend versus cap.planned
Engine contract · engine.json
{
  "name": "outreach-engine",
  "entry": ["{python}", "run.py"],
  "output": "json",
  "commands": { "company add": { "costs": null, "writes": "state" }, … },
  "state": { "path": "state/outreach.db", "idempotent": true },
  "secrets": ["CONTEXT_API_KEY", "INSTANTLY_API_KEY", "HEYREACH_API_KEY", "NOTION_TOKEN", …],
  "ports": { "site": { "bind": "127.0.0.1", "port": 8787 } }
}
11

Hosting & runtime

One always-on VM runs the engine and cron. This architecture page is hosted separately on Cloudflare Pages, with the copilot as a serverless function.

Default · v1

  • One small always-on VM runs Python, SQLite and cron every five minutes.
  • No public ingress required; polling is the safety net.
  • Nightly backup of the database; run artifacts are pruned.
  • Secrets live outside the repo; only names appear in config.
Why not Cloudflare Workers for the engine? It would mean a TypeScript rewrite of the state machine and no SQLite file. Python Workers are beta with a restricted runtime. Cloudflare stays optional edge and dashboard — and now hosts this public architecture page.
Deployment shape
cron */5  ──▶  engine poll / drain / tick
                 │
                 ├─ SQLite ledger + state machine
                 ├─ run artifacts (pruned)
                 ├─ Instantly   ◀──▶ create + poll
                 ├─ HeyReach    ◀──▶ create + poll
                 ├─ Context Archive (read-only)
                 ├─ Notion (intake)
                 └─ Telegram (long polling)

public demo:
Cloudflare Pages ── static page
                 └─ /api/chat ──▶ LLM provider
12

Phases · the build order

Smallest end-to-end path first. Each phase has a definition of done; nothing advances on intent alone.

Phase 0 · current

Registry

Companies, domains, dossiers, Context Archive sync, audit log, tests and control site. Done when the engine check passes and tests are green.

nearly done
Phase 1

Dossier + angle + preview

Notion import, archive intake, angle load, sequence plan, message preview, export JSON and CSV. Still no network writes.

planned
Phase 2

Instantly create + poll + suppress

Create campaign, add leads, never activate without an explicit command; ingest events; execute stops; kill switch. Done when a simulated reply removes a lead within ten minutes.

planned
Phase 3

HeyReach + tasks + Telegram

Sequences and lists, manual-step gating, confirm buttons, cross-channel stop. Done when a manual LinkedIn step cannot advance without confirmation.

planned
Phase 4

Always-on host + status surface

Dashboard, reconciliation and status. Done when the engine survives the laptop being closed and the dashboard matches the database.

planned
After v1

Deferred on purpose

Autonomous sending, reply writing, CRM replacement, a full web app, cold calling and webhook ingress. Revisit only after the email and LinkedIn path is stable.

later
13

Non-negotiable guardrails

These are the rules that keep an efficient pipeline from becoming an unsafe one.

Human approval before push

Preview shows every step, resolved variables, limits and any pointer dropped for missing evidence. v1 creates campaigns but never launches.

No fabricated claims

Every pointer cites a dossier path or proof id. Missing evidence means the pointer is dropped, not invented. A digit and entity validator rejects surprises.

Suppression is cross-channel

A reply or meeting on one channel stops both. A meeting pauses all of that company’s active campaigns, across angles.

Tools are the sender of record

The engine never assumes a send happened. It creates, then reads state back. External ids are stored so re-runs upsert.

Inputs are read-only

Run artifacts go to the engine’s own run folder; persistent state goes to the state folder. Nothing writes next to the user’s source files.

EU is gated twice

Company HQ in the EU, EEA or UK routes to the EU campaign. Deanonymization-derived pointers are dropped for EU or unknown visitors. Unknown fails closed for the pointer, not the campaign.

14

Source of truth

This page is a public reading aid. When it disagrees with the engine docs, the docs win and this page is fixed in the same commit. The repository link will be added after v1 ships.

DocumentOwns
DESIGN.mdDecisions log, data model, event-to-action table and open questions.
README.mdHow to run, state schema, control site, tests and docs index.
engine.jsonThe machine contract: commands, costs, state, secrets and ports.
ANGLE-SPEC.mdAngle questionnaire and pointer / evidence rules.
NOTION-INTAKE.mdNotion template contract and intake system.
OPEN-TASKS.mdHuman tasks, credentials and blockers.