Skip to main content

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.

CI PyPI npm License: MIT

outreach-desk demo: review queue, guardrail flags, approval, pipeline, analytics

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) or contact.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-Unsubscribe header, and won't send until POSTAL_ADDRESS is 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)

Source distribution for outreach-desk 2.0.1
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)

Table of built distributions (wheels) for outreach-desk 2.0.1
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

2.0.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page