How the engine actually got exercised for the first time, what it's built from underneath, and the open questions in folding it into Pitch and Volt AI.
The campaign that proved it out
Everything on this page is grounded in one real run: standing up an email-only outreach campaign for a set of manually-sourced leads, end to end, on this engine.
Two fields on the campaign that don't exist in the upstream project were added specifically for this: email_only, which is checked first, unconditionally, in every LinkedIn-touching scheduler function — verified to return early even with no session at all, before any LinkedIn code path can run — and direct_pitch, which swaps out the default discovery-question opener style for a direct, outcome-first one.
Leads didn't come from LinkedIn discover; email_only blocks that path by design. They came from public pages and a research list already compiled by hand, each resolved to an email at one of three honestly-labelled confidence tiers: verified (a published institutional address, sent straight to the ready-to-email state), pattern-guessed (a confirmed company email format applied to a specific person — stored, but deliberately held back from sending unverified), and not found — recorded as a dead end rather than papered over.
Before any of the drafted openers reached a real inbox, each was previewed — composed, not sent. The local model invented a client relationship that doesn't exist, naming a real company that is in fact a named competitor in this project's own proposal, not a client. A second draft claimed no upfront cost against a proposal with a real setup fee. Every one of 4 test drafts had a fabrication, an overclaim, or a formatting break. All send-ready emails in this campaign were hand-written afterward and checked line by line against the source material instead — which is the concrete argument for keeping first-send review on by default, not a hypothetical one.
Arming a campaign is one line, scoped to that campaign only — LinkedIn connect stays off globally either way:
campaign.coordination_enabled = True campaign.save(update_fields=["coordination_enabled"])
Every draft this produces still needs its own opener_reviewed flag set by a human before the send queue will pick it up — the same hand-check the incident above already argues for, just made structural instead of optional.
Architecture
Five apps, each owning one slice of the funnel. The LinkedIn app is fully wired and live-capable in this codebase — it's the email_only flag that keeps it dormant for an email-only campaign, not an absence of code.
Campaign config, the task scheduler, the daemon loop, prompt templates, the LLM agent layer.
The funnel's data model — who a lead is, what state their deal is in, and why.
SMTP send, IMAP reply-read, mailbox provisioning, email-finder integration.
Browser session, People Search, connect/check_pending — live-capable, blocked per-campaign by email_only.
The per-deal message thread — what the follow-up agent reads to decide what happens next.
| Flag | Scope | What it does |
|---|---|---|
| coordination_enabled | per-campaign | Master switch — off means fully dormant, on runs discover/email/follow-up for that campaign only |
| LINKEDIN_CONNECT_ENABLED | global | Connect/check_pending only ever plan when this is true, for every armed campaign — meant to stay off |
| email_only | per-campaign | Hard-blocks discover/connect regardless of the global flag |
| direct_pitch | per-campaign | Swaps the default discovery-question opener for a direct, outcome-first one |
Process flow
The deal's state machine — and the one branch that stays permanently closed for an email-only campaign.
email_only is what keeps it permanently unreachable here, checked before any session or LinkedIn code ever runs.In words: a lead is sourced → qualified → an email is resolved across the three confidence tiers above → a draft is composed → held for review → sent, gated by the campaign's activate switch → and any reply is read over IMAP and fed back to the follow-up agent, which keeps the thread moving or marks it done.
Folding into Pitch
Pitch's existing shape: an import, then Pitch Studio's research and prep, ahead of a human working The Floor. The question is where a qualify → email-find → draft → send engine like this one actually hooks in.
Pitch is Electron + Next.js + TypeScript. This engine is Django + Python. That mismatch is the real decision, not a detail — three ways to bridge it:
Separate backend, Pitch calls it
- The engine keeps running as its own service; Pitch calls a small internal API after import
- No rewrite — the qualifier, mailbox pacing, and reply loop keep working as-is
Port the pipeline logic into Pitch's own stack
- Re-implement qualify/find/draft/send/reply-read inside Pitch's existing process
- One runtime; the state machine and both safety flags port directly, losing nothing conceptually
Keep them separate, sync via export
- Pitch exports its list; the engine imports it as a seed — exactly how the campaign above was seeded
- Zero integration work, good first step to validate demand
Is the human-review gate a permanent feature, or a today-only workaround?
Today's answer is forced by the local model's reliability, per the Part 01 incident. A stronger model narrows what needs review; it doesn't obviously remove the gate for claims tied to a real commercial proposal.
Folding into Volt AI
Volt AI is a different shape of product — a desktop AI coworker with a knowledge graph, a built-in email client, background agents, and self-updating notes. It already has a working pattern for exactly this kind of integration.
Run on events or a schedule; connect to tools, browse, write code.
A note that keeps itself current against an objective plus a trigger.
A knowledge graph indexing email, meetings, and conversations.
Sorts and auto-drafts replies using work context — a native surface.
The groundwork already exists: existing tool domains in that codebase each export typed, agent-callable tools with a per-tool review-permission gate. A new domain for this engine, in the same shape, is the idiomatic way in — not a new integration mechanism.
Every existing tool domain there calls a hosted backend over HTTPS. This engine isn't that yet — it's a local app with a local database, not a running network service. A new domain needs either a small local HTTP surface bound to localhost, or a direct process call reusing the same inline commands already used to inspect and seed a campaign.
A concrete Live Note, pointed at a campaign instead of a clock:
---
live:
objective: |
Track the outreach campaign: how many deals are
Ready to Email, how many Emailed, and summarize any new
replies since the last check. Flag anything the send-tool
left waiting on review.
active: true
triggers:
cronExpr: "0 8 * * *" # every morning at 8am
---
# Campaign status
Should the send tool default to holding for review?
Given the Part 01 incident, yes by default — the same human-review discipline, enforced natively in the host's own tool-permission system instead of as a separate manual step.
Does the outreach mailbox become that product's own email surface, or stay separate?
Open. Merging them gives one inbox and one thread history; keeping them separate keeps this engine's pacing and review gate simpler to reason about in isolation.