outreach-desk
A self-hosted outreach desk. Claude writes the emails, Jev makes the calls, you approve every send.
Contacts, campaigns, follow-up sequences, reply triage, a pipeline board and analytics, in one
pipx install. It plugs into n8n, so leads can flow in from a Google Sheet and hot replies can
ping you on Slack.
Screenshots use fictional demo data.
Why
Most cold-email tools either write generic text and blast it, or leave you doing everything by hand. outreach-desk splits the work three ways:
| Who | What | |
|---|---|---|
| Writes | Claude (claude-opus-5-5) |
First emails and follow-ups, personalised only from facts you gave it about the recipient |
| Decides | Jev (TypeSafe's System One model) | Typed, fast decisions with calibrated confidence: is this draft safe to send, how good a fit is this lead, what kind of reply is this |
| Approves | You | Nothing is ever sent until you approve that exact draft |
When Jev isn't confident, the decision goes to you instead of guessing.
What it does
- Contacts: add by hand, import a CSV, or paste a LinkedIn bio and let Claude fill the form. Each contact has a facts field, and drafts may only personalise from those facts.
- Campaigns: two modes, networking (students and job-seekers writing to senior people)
and sales (B2B), with a sequence like
0, 4, 7(first email, then follow-ups 4 and 7 days later). - Review queue: every draft arrives pre-checked by Jev. Edit, approve, regenerate or discard.
- Sending: through your own mailbox over SMTP, threaded properly (
In-Reply-To/References), with a daily cap and a send window. - Replies: IMAP sync. Any reply stops the sequence immediately. Jev labels each reply
(
interested,meeting_request,referral,not_now,not_interested,out_of_office,unsubscribe,bounce) and moves the contact along the pipeline. Unsubscribes and bounces go on a do-not-contact list. Out-of-office replies don't stop the sequence. - Pipeline board:
new → contacted → replied → meeting → won / lost. - Analytics: reply rate and positive rate per campaign and per step, reply labels, and every decision logged with which backend answered and how long it took.
- API + webhooks: a bearer-key JSON API and HMAC-signed webhooks, used by the n8n node.
Quickstart
pipx install outreach-desk
outreach-desk # opens http://127.0.0.1:8000
Put your settings in ~/.outreach-desk/.env (or a .env in the folder you run it from):
ANTHROPIC_API_KEY=sk-ant-... # required: Claude writes the drafts
TYPESAFE_API_KEY= # optional: Jev makes the decisions (see below)
SMTP_HOST=smtp.gmail.com # optional until you want to send
SMTP_PORT=587
SMTP_USER=you@example.com
SMTP_PASSWORD=your-app-password # an app password, not your login password
IMAP_HOST=imap.gmail.com # optional: reply sync
FROM_NAME=Your Name
DAILY_CAP=30
SEND_WINDOW=8-19 # local hours
POSTAL_ADDRESS= # required for sales mode (CAN-SPAM footer)
Without SMTP settings you can still build contacts, campaigns and drafts; nothing will send. See .env.example for every option.
Docker / Docker Compose (with n8n)
docker compose up -d # outreach-desk on :8000, n8n on :5678 (both localhost-only)
The compose file reads .env if present and keeps data in named volumes.
How Jev is used
Jev is a System One model: you describe a decision as typed questions, and it answers each one
with a probability or a label plus its confidence (TypeSafe quotes 70–500 ms per call). outreach-desk asks three
sets of questions (decisions.py):
| Decision | Questions | What happens |
|---|---|---|
| Draft guardrail | Noul: uses a cliché? claims something about the recipient that isn't in their facts? unclear ask? · Score: tone fit |
Flags shown on the draft with their probability. Advice only: you still decide. A plain banned-phrase check runs too, so exact matches are never missed. |
| Lead fit | Score against the campaign's target · Choice seniority |
Contacts sorted by fit |
| Reply triage | Choice over the 8 reply labels |
≥ 70% confidence: routed automatically. Below that: it waits in the queue for you, though the sequence still stops |
No Jev key? It still works. Without TYPESAFE_API_KEY, the same questions go to TypeSafe's
open-source system-one-adapter,
which answers them with Claude Haiku 4.5 through the same interface. Expect it to be slower
(seconds, not milliseconds); the Analytics page shows which backend answered each decision and
how long it took, so you can compare.
n8n
n8n-nodes-outreach-desk adds two nodes to n8n:
- Outreach Desk: create, update or list contacts; enrol contacts in a campaign; list, approve (with edits) or reject drafts.
- Outreach Desk Trigger: starts a workflow on
draft.ready,email.sent,reply.classified(optionally only for certain labels) orcontact.stage_changed. It registers its own webhook when the workflow is activated, removes it when deactivated, and rejects any request without a valid signature.
Install it from Settings → Community Nodes in n8n. Ready-made workflows in
n8n/workflows/:
| Template | Flow |
|---|---|
| sheet-to-campaign | New Google Sheet row → contact → campaign |
| hot-reply-to-slack | Interested / meeting / referral reply → Slack message |
| won-to-hubspot | Contact moved to won → HubSpot contact |
API
Every endpoint needs Authorization: Bearer <key> (the key is on the Settings page) except
/api/health.
| Method | Path | |
|---|---|---|
GET |
/api/contacts?stage= |
List contacts |
POST |
/api/contacts |
{name, email, role?, company?, facts?} |
PATCH |
/api/contacts/{id} |
Any field, or stage |
GET |
/api/campaigns |
List campaigns |
POST |
/api/campaigns/{id}/enroll |
{contact_ids?: [...], emails?: [...]} |
GET |
/api/drafts |
Drafts waiting for review |
POST |
/api/drafts/{id}/approve |
{subject?, body?} |
POST |
/api/drafts/{id}/reject |
Discards it and stops that contact's sequence |
POST / DELETE |
/api/webhooks[/{id}] |
Subscribe {url, events}; the response includes the signing secret |
Webhook bodies are {"event", "data", "sent_at"} with
X-Outreach-Signature: sha256=HMAC_SHA256(secret, raw_body).
Safety rules
These are enforced in code and covered by tests:
- Nothing sends without approval, and an approved email that hasn't gone out yet is cancelled if the person replies first.
- Daily cap and send window for every mailbox.
- Do-not-contact list: unsubscribes, bounces and contacts you delete are never emailed again.
- Sales mode adds a postal address, an unsubscribe line and a
List-Unsubscribeheader, and won't send untilPOSTAL_ADDRESSis set. - Delete a contact removes their data (right to erasure) and keeps only their address on the do-not-contact list.
- The app listens on localhost with no login, and refuses form posts coming from other websites. Don't expose it to the internet without putting authentication in front of it.
You are responsible for who you email: anti-spam and privacy law (CAN-SPAM in the US, GDPR and PECR in the UK and EU) applies to you, not to this tool.
Deliverability tip: send from a secondary address or domain, warm it up, and keep the daily cap low.
Architecture
flowchart LR
subgraph desk[outreach-desk · FastAPI + SQLite + HTMX]
UI[Pages] --> Engine
API[JSON API] --> Engine
Scheduler[[every 60 s]] --> Engine
Engine --> Drafting[drafting.py]
Engine --> Decisions[decisions.py]
Engine --> Mail[mail.py]
Engine --> Hooks[webhooks.py]
end
Drafting -- writes --> Claude[(Claude API)]
Decisions -- decides --> Jev[(Jev / adapter)]
Mail -- SMTP / IMAP --> Mailbox[(Your mailbox)]
Hooks -- signed events --> n8n[(n8n)]
n8n -- API --> API
engine.py holds the rules (enrol, draft what's due, send what's approved, route replies). Every
function takes a session and a clock, so it is fully testable without a network.
Development
git clone https://github.com/owen-alderson/outreach-desk && cd outreach-desk
python -m venv .venv && . .venv/bin/activate
pip install -e '.[dev]'
pytest # 178 tests, no network: Claude, Jev, SMTP and IMAP are faked
cd n8n-node && npm install && npm run lint && npm run build
Releases: push vX.Y.Z to publish to PyPI, n8n-vX.Y.Z to publish the n8n node to npm (both
from GitHub Actions; the npm package carries a provenance statement).
The original single-email Streamlit version is kept at tag
v1-streamlit.
License
MIT
Metadata
Release files for outreach-desk 2.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| outreach_desk-2.0.1.tar.gz | 53.3 kB | With provenance | Details for outreach_desk-2.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| outreach_desk-2.0.1-py3-none-any.whl | Python 3 | none | any | Details for outreach_desk-2.0.1-py3-none-any.whl |
Total release size: 114.4 kB
Release files / outreach_desk-2.0.1.tar.gz
| Download URL | outreach_desk-2.0.1.tar.gz |
|---|---|
| Size | 53.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e373f77ccb257b3476c32679be199c561d973e5285b2c95725e3227b8734bfc0
|
|
BLAKE2b-256 checksum How to use checksums |
7298ff9dc24de878ed385a7560afe4a3ca29b1c59155c009ad5e6ebd6c1f5b1f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logRelease files / outreach_desk-2.0.1-py3-none-any.whl
| Download URL | outreach_desk-2.0.1-py3-none-any.whl |
|---|---|
| Size | 61.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fdac134dcd3e233619c2e1d74d8d9f05597829c9a4cad9519478ed9f57b7a4ab
|
|
BLAKE2b-256 checksum How to use checksums |
1446890c013509be4d4137c7f32e5d1e289eba2f0bcb4cdc622dbc8716326988
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logSecurity for outreach-desk 2.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for outreach_desk-2.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.