Skip to content

Add leads to a running campaign, a per-row import report, and lead screening - #124

Merged
ralyodio merged 1 commit into
mainfrom
feat/append-leads-report-screening
Oct 6, 2026
Merged

ralyodio merged 1 commit into
mainfrom
feat/append-leads-report-screening

Conversation

@ralyodio

@ralyodio ralyodio commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Explee cannot append to a campaign, and its import skipped rows without saying which or why. This brings both into OutreachGraph, plus the lead screen we ran by hand on the profullstack user export.

Append to an existing campaign

  • POST /api/v1/autogtm/campaigns/{id}/leads with leads (Explee's columns, plus name, updated_at) or csv (the file itself), up to 5,000 per call.
  • Skips people already in the campaign, already in another campaign of the same project (skip_project_duplicates, default true), and anyone on a suppress list or unsubscribed.
  • One code path with POST /campaigns/import (importIntoCampaign), which now also skips suppressed people and reports.
  • UI: "Add leads from a CSV" on the product page. CLI: og leads add <campaignId> <file.csv> --consent-source .... MCP: add_leads_to_campaign.

Per-row import report

  • contact_import_rejects gains outcome (rejected / skipped / flagged), so one table is the whole report. Every row that did not simply become a lead is stored with the reason and the column or check behind it (required column "email" is empty, already a lead in "Earlier list" in this project, on the suppress list "Do not contact"...).
  • Returned inline (first 500 rows), at GET /autogtm/campaigns/import/{task}/report and GET /contacts/imports/{id}/report, and as CSV with ?format=csv. The /import page and the product page link the download. og leads report <task> [--csv], MCP get_import_report.
  • The append route validates nothing per lead in the schema, so one bad row no longer 400s the whole request.

Lead screening

  • packages/domain/src/lead-screen.ts: generated names (Borer853), relay hosts (passmail, passinbox, passfwd, aleeas, simplelogin.*, slmail, mozmail, duck.com, addy.io/anonaddy, hidingmail, agentmail.to...), temp-mail domains (maildock.store, fake.legal, or one domain shared by 3+ leads where nobody names it as their company and the addresses are not names), bot/agent/test accounts, role inboxes.
  • Flagged at import, stored in lead_screens, and held back from cold outreach through the policy engine (personScreenedOut on the person_ineligible gate), so approval, autopilot, cadences and card generation all refuse the same person with the same reason. A follow-up on a conversation they opened is never blocked.
  • Override per lead (POST /autogtm/leads/{person_id}/screening {allow}), or for the whole import (allow_flagged). An allow survives a re-import. UI list with Allow buttons; og leads screened|allow|hold; MCP list_screened_leads, allow_screened_lead.
  • Webmail, relays and newsletter hosts are never taken as a company domain (isConsumerMailDomain); the API's private freemail list is gone.

Migration 0051 (both dialects): a new empty table and three ADD COLUMNs with constant defaults. No index on an existing table.

Tests

  • packages/domain/src/lead-screen.test.ts, policy gate test, apps/api/src/append-leads.test.ts (append, dedupe, suppress, report rows, CSV download, screened list, override, approval refused with the screening reason), CLI and MCP tests.

🤖 Generated with Claude Code

…reening

Append: POST /api/v1/autogtm/campaigns/{id}/leads takes up to 5,000 leads
(JSON or the CSV itself), skips people already in the campaign or its
project and anyone on a suppress list, and shares one code path with the
new-campaign import. UI on the product page, `og leads add`, MCP
add_leads_to_campaign.

Report: every row that did not simply become a lead is stored with what
happened (rejected, skipped, flagged) and the column or check behind it;
returned inline, readable at .../import/{task}/report, downloadable as CSV.

Screening: generated names (Borer853), relay mailboxes (passmail, aleeas,
simplelogin...), temp-mail domains (known, or one domain shared by 3+
unrelated leads), bot/agent/test accounts and role inboxes are flagged at
import and held back from cold outreach through the policy engine
(personScreenedOut), on every send path, until a human allows them.
Webmail is never taken as a company domain. Migration 0051.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit f0ad3e7 into main Oct 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant