Outreach Magic
Your agent goes blind after send. We sync Smartlead, Instantly, HeyReach, and 5 more sequencers into one local SQLite database your agent can query directly.
The full suite: Pair with lead-enrich for Serper-based person research and email-finder for waterfall email enrichment. Both work standalone or together.
CLI convention
All commands below use the pipeline CLI in this skill's
directory (run from the skill root, or use absolute paths from
):
bash
python3 scripts/pipeline.py <command>
Resolve install paths anytime:
bash
python3 scripts/pipeline.py paths
Optional config keys:
(share one DB across platforms),
,
for local development.
Platform install
Install from
outreachmagic/outreachmagic — pin a release tag; download and verify SHA256 before running locally:
bash
OM_VERSION=v1.1.0
INSTALL_DIR=$(mktemp -d)
curl -fsSL "https://github.com/outreachmagic/outreachmagic/releases/download/${OM_VERSION}/install.sh" -o "${INSTALL_DIR}/install.sh"
curl -fsSL "https://github.com/outreachmagic/outreachmagic/releases/download/${OM_VERSION}/SHA256SUMS" -o "${INSTALL_DIR}/SHA256SUMS"
grep ' install.sh$' "${INSTALL_DIR}/SHA256SUMS" | (cd "${INSTALL_DIR}" && shasum -a 256 --check)
bash "${INSTALL_DIR}/install.sh" --platform hermes --tag "${OM_VERSION}"
Installs outreachmagic, lead-enrich, and email-finder. Agent guide:
AGENTS-INSTALL.md.
Hermes profiles
- Real install: — profiles symlink only ()
- Verify:
- New profile:
bash install.sh --platform hermes --profile <name>
- Update: writes the global install; all profiles pick it up via symlink
Cursor
Install to
~/.cursor/skills/outreachmagic/
. Invoke with
or ask about your pipeline in plain English.
Claude Code
Install to
~/.claude/skills/outreachmagic/
. SKILL.md is the source of truth.
Environment variable:
— overrides the config file
. Set via
, shell profile, or CI/CD.
First-Time Setup (read this first)
On startup, always check if the agent is already connected by running:
bash
python3 scripts/pipeline.py version
Then check whether an agent key exists in the config:
bash
python3 scripts/pipeline.py pull
If
returns an error like "No agent key or token configured", the user needs to set up.
When setup is needed, run
— a browser opens on the user's machine for sign-in. Tell the user:
"I'm opening Outreach Magic sign-in — come back when you're done and I'll continue." Never paste secrets into chat.
Account access error: If login fails with an account error (
in CLI/API), tell the user there was a sign-in problem and direct them to
support@outreachmagic.io for help.
Common workflows (plain English)
| User says | You do |
|---|
| "Show my pipeline" | → |
| "Import my Sales Nav / Vayne CSV" | import-profiles --file … --workspace W --dry-run
first, then import (auto-syncs) |
| "Find emails for these leads" | import if needed → → batch-find --workspace W --yes
|
| "Export to Google Sheets" | → , then sheets export --workspace W --share-email …
|
| "Connect Smartlead / Instantly" | connections create --platform …
and share webhook URL |
pipeline.py whoami --json
returns account email, org, and plan.
creates the database under
. Dashboard API keys sync to
<skill_home>/config/agent_secrets.env
(next to
outreachmagic_config.json
). CSVs and exports live under
and
relative to your
workspace directory (where the agent runs commands). Set
in config to pin a fixed folder instead of cwd. Run
for resolved paths (
,
,
,
too).
If
returns auth errors after a revoked key, ask Outreach Magic to log in again.
That's it. Don't list other commands, don't offer alternatives. Just connect via login, done.
When setup is already done (pull succeeds or returns events), skip setup and go straight to showing data:
bash
python3 scripts/pipeline.py pull
python3 scripts/pipeline.py show
Network & privacy (Hermes / hub review)
- Default: All lead and pipeline data stays in local SQLite.
- Inbound only: imports webhook/agent events from (user- or cron-initiated).
- Outbound upload: Only when the user (or agent following user instruction) runs . Import and local edits never auto-upload.
- Update check: The CLI may query GitHub for a newer release tag (read-only, no lead data; at most once per hour). See SECURITY.md.
Version
One version for the whole skill. To see what is installed, always run:
bash
python3 scripts/pipeline.py version
The
line in this file is synced from
on install/update. If unsure, use the command above.
Updates are user-triggered. The CLI may print an update notice (at most once per hour) when a newer GitHub release exists. It never downloads or replaces scripts automatically. Install updates with:
bash
python3 scripts/pipeline.py update
# or
hermes skills update
Check without installing:
pipeline.py update --check
. Install a specific release:
pipeline.py update --tag v1.1.0
.
Install commands for each platform are in
Platform install above. After install, run
python3 scripts/pipeline.py login
.
When to Use
- You are about to send outreach (email, LinkedIn message, WhatsApp, etc.)
- You are researching a prospect and want to track them
- The user asks "show me my pipeline" or "how is outreach going"
- The user says "track this" followed by outreach details
- The user wants to connect a sequencer (paid — requires token)
- The user asks for campaign breakdowns or counts by campaign name
- The user asks for workspace inventory (counts by tag, LinkedIn connection accepted by sender)
- The user asks about connection status, webhook URLs, or platform health (, )
- The user wants to add or remove a platform connection (, )
Agent Behavior Rules (Important)
- For bulk enrichment (CSV, spreadsheet export, Apollo/Clay dump): use , not repeated .
- Reads (analytics, counts, time windows): use presets first — one query, max two attempts. See references/query-guide.md. Do not tour when the question is relay volume (use ).
- Writes: only mutation commands (, , , , personalize, tags, etc.). Never // via ad-hoc scripts.
- After any , explicitly report the exact number of new records imported.
- When the user asks about version, run (authoritative).
- When the user asks for message content, use on the specific lead.
- For copy-performance analysis (full subject/body on positive leads + winner), use .
- For all-time campaign totals (no time window), use or . For last N hours/days by campaign, use or — not .
- For tag counts or LinkedIn connection counts by sender, use
workspace summary --workspace <slug> --json
. On large workspaces (>2,000 leads), add for faster tag rollups. Do not use or custom Python to aggregate.
- Tags () and LinkedIn connection state (
workspace_lead_linkedin_status
, per sender) are different.
- When the user asks about connections, webhook URLs, or platform health, use or .
- When the user wants to connect a new platform, use
connect-platform --platform <id>
.
- If the user reports slowness, disk use, or pipeline oddities: run first (local, no network).
- Never run unless the user asked. Never run without explicit confirmation after .
- Before adding platforms or debugging vendor event types, run
pipeline.py platform-map --json
.
- Answer format for analytics: (1) human table, (2) preset name or SQL used, (3) freshness note (local DB; offer if they need latest relay data).
Which table? (read path)
| User intent | Use |
|---|
| Replies / engagement / campaign breakdowns (time window) | or + |
| Lead list with stage/sentiment | / |
| Tag counts, LinkedIn connected-by-sender | |
| Message bodies / copy winners | , |
| Workspace ingest audit (rare) | — not for volume analytics |
is a subset; full relay timelines are in
.
Fast analytics (preferred)
bash
python3 scripts/pipeline.py query engagement --workspace <slug> --since 48h --json
python3 scripts/pipeline.py query replies --workspace <slug> --since 7d --json
python3 scripts/pipeline.py query interested --workspace <slug> --since 48h --json
Advanced:
pipeline.py query --sql 'SELECT …' --params '["popcam |%"]' --json
(read-only).
Read commands print
data freshness on stderr (and
/
in
). If data is a few minutes old, tell the user before running
unless they asked for up-to-the-minute.
| User intent | Command |
|---|
| Reply/engagement counts in a time window | / query engagement --since … --json
|
| Lead rows / pipeline detail | (use ; avoid unless needed) |
| All-time totals | / |
| Fresh webhook events | or |
Pull policy (when to refresh)
Do not run before local time-window analytics (
,
, engagement by campaign) unless the user asks for latest/refresh or you need relay catch-up.
Run first when showing live pipeline activity the user expects to be current (
,
for “what just happened”), or when they say sync/refresh/latest.
bash
python3 scripts/pipeline.py pull
python3 scripts/pipeline.py pull --if-stale 5m # skip when last_pull is within 5 minutes
python3 scripts/pipeline.py pull --force # always network (ignore --if-stale)
If routing sync times out but you only need webhook events, use:
bash
python3 scripts/pipeline.py pull --skip-routing-sync
This fetches the latest events from the relay. Skip for offline/local analytics; use for fresh activity timelines.
Relay sync progress (stdout): When interpreting
/
output or
outreachmagic/logs/batch_sync.log
, use the log legend in
docs/relay-sync-progress.md. Short version:
- ↓ pull / ↑ push — cloud → local vs local → cloud
- Event, Lead, Workspace, Company — four streams (Lead = org lead core; Workspace = per-workspace lead overlay, not routing config)
- Pending banner: on counts/page estimate once per stream start, e.g.
[02:10] ↓ Event : ~12,400 pending (~13p @ 1000/p) ...
- Per page:
[02:11] ↓ Event : p13/62 — 1,000 this page, 13,000/62,400 (20%) ...
(pull, no )
- : backlog only (/stream, no ingest) — use before a large catch-up pull
- : webhook events only (recommended after events catch-up; avoids slow snapshot phase)
pull --reset-snapshot-cursors
: zero snapshot cursors before pull (after DB delete or hung pull left config ahead of local leads)
- : same as when you only need the event cursor advanced
- Push page:
[03:56] ↑ Event : p2/13 — ok 7.9s, 5,000 this page (10,000/62,093 (16%))
then
- : outer = lead-id walk; inner = HTTP pages inside one . Do not confuse them.
Relay sync limits: Same endpoints always —
and
. No separate bulk URLs.
- (upload): When local snapshots ≥ 2500, uses 5000 entries per ; otherwise routine batch size (default 200, max 500 per request).
- (download): 1000 rows/page for events and all snapshots (D1 + local ingest). Progress shows and after a one-time pending probe per snapshot kind. Use if snapshots are already synced.
- Filter downloaded data locally (, workspace queries) — the relay does not filter by date or workspace.
Workspace inventory
reads local SQLite only (fast, works offline). Use when the user asks for counts by tag or LinkedIn sender connection state. Optional
first if they need freshly synced tags/connection imports.
bash
python3 scripts/pipeline.py workspace summary --workspace <slug> --json
python3 scripts/pipeline.py workspace summary --workspace <slug> --json --tags-only
Example JSON keys:
,
,
(
,
),
(
,
,
),
. With
, LinkedIn keys are empty arrays/zero.
Companion attempt tags:
(lead-enrich),
/
(email-finder),
(MillionVerifier bulk — verification status still in
email_verification_status
). Found email is represented by
,
, and
email_verification_status
— not a separate tag.
Tag-only (same tag data as summary):
pipeline.py tag list --workspace <slug>
.
Free Tier
- Unlimited local tracking, enrichment, queries, and exports
- 1,000 webhook events / period (sequencer webhooks + cloud sync)
- 1 sequencer connection, single workspace
Pro Tier ($9/mo)
- 50,000 webhook and sync events / month
- All sequencers, multi-workspace routing
Agency Tier ($29/mo)
- 250,000 webhook and sync events / month
- All sequencers, unlimited workspaces, priority support
What counts
Only
webhook and sync traffic: sequencer webhooks and
uploads to the cloud (
one event per sync batch, not per lead). Local tracking (
,
), enrichment dedup, email finding, queries, and exports do
not count.
Over limit
Sequencer webhooks are always accepted. Over-quota events are
buffered and delivered on your next
or when the quota resets. Events are only rejected if the
buffer cap is reached. Check status with
.
Quick Start
bash
python3 scripts/pipeline.py version
python3 scripts/pipeline.py query engagement --workspace <slug> --since 48h --json
python3 scripts/pipeline.py show
python3 scripts/pipeline.py history --id 1
python3 scripts/pipeline.py history --email j@acme.com
python3 scripts/pipeline.py stats
python3 scripts/pipeline.py campaigns
python3 scripts/pipeline.py platform-map --json
python3 scripts/pipeline.py workspace summary --workspace <slug> --json
python3 scripts/pipeline.py copy-insights --lead-status interested --json
python3 scripts/pipeline.py import-profiles --file leads.csv
python3 scripts/pipeline.py agent-changes
python3 scripts/pipeline.py agent-changes --file changes.csv
Status and connection management
Dashboard-style status, connection management, and webhook URL generation — all from the CLI. These commands talk to the app API and do not require a local database.
bash
# Dashboard overview: plan, usage, per-platform health, routing
python3 scripts/pipeline.py status
# List all connections with webhook URLs and 30-day event counts
python3 scripts/pipeline.py connections
python3 scripts/pipeline.py connections --json
# Generate a webhook URL for a new platform
python3 scripts/pipeline.py connect-platform --platform smartlead
# Remove a platform connection (webhook URL stops working)
python3 scripts/pipeline.py disconnect-platform --platform smartlead
python3 scripts/pipeline.py disconnect-platform --platform smartlead --yes
Agent-created changes (cross-platform sync)
Show locally-created leads and events (not from relay) as JSON or CSV. Useful for transferring data between platforms (Cursor, Hermes, Claude Code).
bash
# JSON to stdout (pipe to relay push or save to file)
python3 scripts/pipeline.py agent-changes
# CSV file (import-profiles compatible)
python3 scripts/pipeline.py agent-changes --file local_changes.csv
# Filter to a specific workspace
python3 scripts/pipeline.py agent-changes --workspace leadgenph
# Include all leads (not just locally-created)
python3 scripts/pipeline.py agent-changes --all
Push to relay for cross-platform sync:
bash
python3 scripts/pipeline.py sync
pushes pending lead snapshots (profile,
,
, HQ/location, tags, mailmerge, workspace status, LinkedIn connection flags) plus local events, and
quarantine resolutions (
/
) to the relay. Large backlogs use
5000 entries per automatically (see relay sync limits above). At the end of the same command it may POST aggregate local DB health to the portal (file size, row counts, top tables — throttled ~6h). Skip with
. Other machines run
after a DB reset to restore everything that was synced.
Quarantine (multi-workspace)
Unmapped webhook events land in
. Resolve locally, then
so other machines and
stay consistent.
No campaign id/name: New webhook events are quarantined automatically. Legacy rows in
with
are backfilled on
into quarantine and
auto-skipped (no workspace mapping possible). Use
to inspect event details if needed. Manual:
quarantine backfill-no-campaign
.
bash
python3 scripts/pipeline.py quarantine list [--json] [--status all]
python3 scripts/pipeline.py quarantine skip --id QUEUE_ID
python3 scripts/pipeline.py quarantine skip --campaign-id CAMPAIGN_ID
python3 scripts/pipeline.py quarantine skip --reason REASON
python3 scripts/pipeline.py quarantine assign --id QUEUE_ID --workspace WORKSPACE_SLUG
python3 scripts/pipeline.py quarantine replay
python3 scripts/pipeline.py sync
- — ignore junk/test events on the relay (permanent after sync).
- — route future pulls to a workspace (ingested on next , not immediately).
- — bulk re-ingest pending rows locally after adding rules (no relay resolution).
Relay stores resolutions in D1 (
). The first event page of each
requests them (
include_queue_resolutions=1
); later pages reuse the in-memory map.
counters: is
when
≥ 2500 (else
).
= imported/local leads with no relay pull history (normal after CSV; data is still in the shared DB).
= rows waiting to push — run
.
= agent-originated events not yet on relay.
Local database health
bash
python3 scripts/pipeline.py db-health
python3 scripts/pipeline.py db-health --json
python3 scripts/pipeline.py db-health --full
Read
,
(each has a
),
, and
. Cloud copy:
→
after user has run
.
Archive a workspace (local only)
bash
python3 scripts/pipeline.py archive --workspace acme_corp --dry-run
python3 scripts/pipeline.py archive --workspace acme_corp --output ~/archives/acme_corp.db
python3 scripts/pipeline.py archive --workspace acme_corp --output ~/archives/acme_corp.db --purge
Fresh DB + full CSV round-trip:
bash
pipeline.py import-profiles --file nace.csv --workspace acme_corp --import-batch-id nace-2026
pipeline.py sync
# new machine:
pipeline.py init && pipeline.py pull --full
File-based transfer (no server):
bash
# Machine A: export
pipeline.py agent-changes --file changes.csv
# Machine B: import
pipeline.py import-profiles --file changes.csv --overwrite
Workspace inventory
Counts by tag and LinkedIn connection accepted/pending per sender.
Local DB only — no relay call;
in output shows data freshness.
bash
python3 scripts/pipeline.py workspace summary --workspace <slug> --json
python3 scripts/pipeline.py workspace summary --workspace <slug>
python3 scripts/pipeline.py tag list --workspace <slug>
Campaign breakdown
Relay imports auto-populate campaign names from webhook payloads (Smartlead, PlusVibe, etc.).
bash
python3 scripts/pipeline.py campaigns
python3 scripts/pipeline.py campaigns --json
also includes a campaign section. Use
when the user only wants counts by campaign name.
PlusVibe webhooks (status + sentiment)
Point PlusVibe webhooks at your relay URL (
). Select
all event types and category labels in PlusVibe — including any custom categories in the user’s instance. Standard ones to verify:
- , ,
LEAD_MARKED_AS_INTERESTED
, LEAD_MARKED_AS_NOT_INTERESTED
, LEAD_MARKED_AS_OUT_OF_OFFICE
, LEAD_MARKED_AS_AUTOMATIC_REPLY
LEAD_MARKED_AS_MEETING_BOOKED
, LEAD_MARKED_AS_MEETING_COMPLETED
, LEAD_MARKED_AS_WRONG_PERSON
,
Do not enable (duplicates
) or
(subset of
). Leave “Skip out of office replies” and “Skip autoreplies”
unchecked.
Each webhook is stored as an event.
Interested / not interested / sentiment come from label webhooks, not from reply webhooks alone. OOO is classified as
auto-reply (metadata flag, query with
). Bounces set event sentiment
but
do not auto-move the lead to stage
(use
to find them).
EmailBison webhooks
Point EmailBison webhooks at your relay URL (
). In EmailBison, go to Settings → Webhook, paste the URL, and toggle the events below. Enable all seven:
- — sent from sender to lead
- — delivery failure (hard and soft bounces)
- — lead replied to an email
- — lead marked as interested (replies with positive intent)
- — lead unsubscribed from the campaign
- — a tag was added to the lead
- — a tag was removed from the lead
All event types are accepted and stored.
lead.interested sets stage to
locally.
lead.replied sets stage to
. Bounces are recorded in the bounce events table with EmailBison-specific bounce field paths (
,
,
). Tags are stored as
/
event types with their raw vendor type preserved.
Campaign id/name are extracted from
/
in the payload.
After
, filter the pipeline by
current status (latest status-bearing event per lead):
bash
python3 scripts/pipeline.py show --sentiment positive
python3 scripts/pipeline.py show --sentiment invalid
python3 scripts/pipeline.py show --auto-reply true
python3 scripts/pipeline.py show --lead-status interested --json
Filter by date (created or updated on/after a date):
bash
python3 scripts/pipeline.py show --since today
python3 scripts/pipeline.py show --since 2026-05-26 --json
python3 scripts/pipeline.py lead-table --workspace acme_corp --since today --json
Then open full timeline for any lead (all events, not just the status event):
bash
python3 scripts/pipeline.py history --id 1
Native copy-performance analysis (full message bodies + best template):
bash
python3 scripts/pipeline.py copy-insights --lead-status interested
python3 scripts/pipeline.py copy-insights --lead-status interested --json
and
return
{"leads": [...], "data": [...], ...}
with
,
, and
when available.
Export full profiles (CSV / JSON)
bash
python3 scripts/pipeline.py export --workspace acme_corp --tag nace --format csv
python3 scripts/pipeline.py export --workspace acme_corp --since today --format json
Writes to
under your workspace by default. CSV uses
,
personalized_company_name
, plus lead fields, tags, HQ, and
.
Not for Google Sheets —
writes local files only. For a hosted Google Sheet, use
or
(below).
Reset local database (schema upgrade)
Prefer the guarded refresh command (syncs first, backs up, then rebuilds):
bash
python3 scripts/pipeline.py refresh --yes
Preview tag fixes without writing:
bash
python3 scripts/pipeline.py tag repair --dry-run
python3 scripts/pipeline.py tag repair
Manual equivalent (no pre-sync backup):
bash
rm <skill_home>/databases/outreachmagic.db # see pipeline.py paths
python3 scripts/pipeline.py init
python3 scripts/pipeline.py pull --full
Tell your agent (rare): “Run
pipeline.py refresh --yes
to back up, sync local changes to the relay, wipe the local DB, and re-import from the cloud. Do not use
alone — it skips already-imported rows.”
only re-downloads relay pages; it does
not clear
. Use
when you need a true rebuild.
LinkedIn IDs (v1.17): Public profiles are stored in
as
(no
). Sales Nav (
) and member IDs (
) are stored as identity aliases and used for matching when the public slug arrives later.
Core Workflow
View a lead's full timeline
bash
python3 scripts/pipeline.py history --id 1
python3 scripts/pipeline.py history --email jane@acme.com
python3 scripts/pipeline.py history --name "Jane Doe"
python3 scripts/pipeline.py history --id 1 --json
Outputs lead info + numbered event timeline with direction arrows (← inbound, → outbound),
human-readable timestamps, and event details.
Add leads when researching prospects
bash
python3 scripts/pipeline.py add-lead \
--name "Jane Doe" --company "Acme Corp" --title "VP Marketing" \
--industry "Martech" --headcount "50-200" \
--email "jane@acme.com" \
--channel email --stage prospecting
To also associate the lead with a workspace at creation time:
bash
python3 scripts/pipeline.py add-lead \
--name "Jane Doe" --email "jane@acme.com" --company "Acme Corp" \
--workspace thesystemsmethod --stage contacted
is optional on
— creating a lead is an org-wide operation. Use it when you know which workspace the lead belongs to; omit it when just researching.
If lead exists by email, LinkedIn, or (when both are missing) case-insensitive
, returns
{"status": "exists", "id": N}
.
Bulk import / enrich (CSV, JSON, research dumps)
Use for spreadsheets, enriched exports, or batched research — not repeated
calls. Matching uses
tiered identities (strongest first):
→ email → LinkedIn → phone → name+domain → name+company →
(name-only rows). CSV columns
/
are accepted as aliases and stored as
. Fills empty fields only (same as relay/PlusVibe); use
to replace existing values.
bash
python3 scripts/pipeline.py import-profiles \
--file outreachmagic/imports/contacts_enriched.csv
python3 scripts/pipeline.py import-profiles \
--file leads.json
python3 scripts/pipeline.py import-profiles \
--json '[{"email":"j@acme.com","name":"Jane","job_title":"VP Marketing","industry":"Martech","headcount":"11-50","company":"Acme"}]'
python3 scripts/pipeline.py import-profiles \
--file contacts.csv --dry-run
# With workspace association, tags, and LinkedIn status tracking
python3 scripts/pipeline.py import-profiles \
--file contacts.csv --workspace default --sender-profile "https://linkedin.com/in/myprofile" \
--source csv_import --source-detail "Q2 Apollo list" --import-batch-id "nace-2026-05"
python3 scripts/pipeline.py import-profiles \
--file sales_nav_export.csv --source sales_navigator --source-detail "Q2 Sales Nav list"
# Rows with only name + company_domain + unified_lead_id (no email/LinkedIn)
python3 scripts/pipeline.py import-profiles \
--file nace.csv --workspace acme_corp --import-batch-id nace-2026-05
: Records LEV rows locally and sets
— run
to push verification fields to the relay (same as other lead mutations).
Email-finder batch save (known on every row): email-finder calls
— updates email, workspace tags, and provider verification in one pass. Requires
. Run
after batch-find when COMPLETE shows pending snapshots. Manual recovery:
bash
python3 scripts/pipeline.py apply-email-find-results \
--workspace your_workspace --source trykitt --source-detail "email-finder/batch" \
--file import.json
Use
when rows lack
/ need tiered matching or CSV-only fields (personalization columns,
, etc.).
Core profile fields (column aliases — first non-empty wins):
| Canonical field | Aliases | Required |
|---|
| , | No (see identity tiers below) |
| , , | No |
| , (or + ) | No |
| , | No |
| , , | No |
| — | No |
| , , | No |
| , | No |
| , , | No |
| , | No |
Headcount normalization: is stored as-is (text) plus a computed
(integer midpoint). Ranges like
become
,
becomes
, exact numbers pass through. Both leads and companies get the numeric column for sorting/filtering (
WHERE headcount_numeric BETWEEN 10 AND 100
).
Extra fields (auto-detected from CSV columns):
| Column | Effect |
|---|
| Stored in table, normalized (strips protocol/www/path) |
| / / | Company HQ location, stored on table |
| Auto-populated as in personalization table |
| Auto-populated as in personalization table |
| / | Attribution + namespace for when value has no |
| CRM/list ID in (namespaced if bare) |
| , | Import aliases → same as |
| (CLI flag) | Stable dedupe for name-only rows via within a batch |
| Requires ; normalized (lowercase, spaces) and set on workspace_leads |
| Requires ; normalized (lowercase) and set on workspace_leads |
| Requires ; semicolon or comma separated, normalized (lowercase), stored in |
| Requires ; integer priority stored as on workspace_leads |
| Requires + ; // sets connected status |
is_linkedin_request_pending
| Requires + ; // sets pending status |
Normalization rules:
- Tags: lowercased, whitespace collapsed — and are the same tag
- Status/sentiment: lowercased, underscores to spaces — becomes
- Headcount: range string preserved + numeric midpoint computed ( → 30)
- Location: stored as-is (city/state/country text)
Attribution is automatic: every import sets
(immutable first touch) and
(updates each time) on the lead, following the Salesforce/HubSpot model. Use
for the machine-readable channel (
,
,
,
, …). Use
or
/
columns for list/campaign labels. Per-row
overrides the CLI
default when present.
Personalization (mail-merge)
Lead fields (
, contact-specific lines): per lead.
Company fields (
,
): org-wide, one write per account.
| Raw field | Mail-merge field | Scope |
|---|
| | lead |
| / | | company |
bash
# Lead
python3 .../pipeline.py personalize-pending --fields first_name --json
python3 .../pipeline.py personalize-set --lead-id 5 --field first_name --value "Jane"
python3 .../pipeline.py personalize-set --lead-id 5 --field upcoming_event --value "SaaStr talk" --date 2026-09-10
# Company (org-wide)
python3 .../pipeline.py company-personalize-pending --fields company_name,company_icebreaker --json
python3 .../pipeline.py company-personalize-set --domain acme.com --field company_name --value "Acme"
python3 .../pipeline.py company-personalize-set --domain acme.com --field company_icebreaker --value "..."
# Read merged (export uses same shape: personalized_* columns)
python3 .../pipeline.py personalize-get --lead-id 5 --json
Import:
→ lead;
,
→ company. Sync pushes lead and company snapshots separately; merge is local.
Email verification tracking (org-wide)
Record verification results from tools like ZeroBounce, NeverBounce, etc. Results are org-wide (not workspace-scoped). Platform bounces from Smartlead, Instantly, etc. are auto-recorded during relay sync.
bash
# Record a verification result
python3 scripts/pipeline.py verify-email \
--lead-id 5 --status valid --source zerobounce
# Batch verify from JSON
python3 scripts/pipeline.py verify-email --batch \
--json '[{"lead_id":5,"status":"valid","source":"zerobounce"}]'
# Check verification status for a lead
python3 scripts/pipeline.py verify-status --lead-id 5
python3 scripts/pipeline.py verify-status --email j@acme.com
# List leads needing verification
python3 scripts/pipeline.py verify-pending --limit 50 --json
Verification status values: ,
,
,
,
,
,
,
,
,
Bounce handling: Platform bounces (from relay sync) are auto-recorded in
with
. Hard bounces override soft bounces. Tool verifications (ZeroBounce, etc.) take precedence over platform bounces — a tool "valid" result is only overridden by a hard bounce that came after the verification. The consolidated status is materialized on
leads.email_verification_status
for fast filtering.
Companies and unified lead identity
- table — canonical company name, domain, industry, headcount (text + numeric midpoint), HQ location (city, state, country). Leads link via (business email domain or company name on ingest).
- Match by email and/or LinkedIn — a lead can have email only, LinkedIn only, or both. Relay ingest resolves identity from webhook payload + envelope field.
- Merge duplicates when email and LinkedIn history were separate rows:
- Auto: ingest with both identifiers matching two leads merges them (keeps row with more events).
- Manual:
bash
python3 scripts/pipeline.py merge-leads --keep 12 --merge 34
python3 scripts/pipeline.py merge-leads \
--email j@acme.com --linkedin linkedin.com/in/janedoe
Dedup (batch duplicate find + merge)
bash
python3 scripts/pipeline.py dedup find --workspace popcam --tag campaign --output outreachmagic/exports/candidates.json
python3 scripts/pipeline.py dedup merge --candidates outreachmagic/exports/candidates.json # dry-run
python3 scripts/pipeline.py dedup merge --candidates outreachmagic/exports/candidates.json --commit # apply
Google Sheets export (lead review)
To export leads to an editable Google Sheet (two-way sync), use
or
— not
.
Requires
. Sheets are created on
(no local Google credentials).
bash
python3 scripts/pipeline.py sheets export --workspace popcam --title "NACE Leads"
# equivalent:
python3 scripts/pipeline.py review export --template lead-review --workspace popcam \
--tag nace --detail standard --title "NACE Leads"
See Lead review sheet below for sync-back workflow.
Dedup review (hosted Google Sheets)
Requires
. Sheets are created on
and shared to your org owner email (or
). Check
Merge? in the sheet, then sync.
bash
python3 scripts/pipeline.py review export --input outreachmagic/exports/candidates.json --title "Popcam Dedup"
python3 scripts/pipeline.py review sync --sheet-id SHEET_ID --dry-run
python3 scripts/pipeline.py review sync --sheet-id SHEET_ID --commit
Lead review sheet (export → edit → sync)
Requires login. Sheets are created on
with edit/lock header icons (no column colors). All usage notes live in the
header cell note; freeze row and dropdowns apply before the export URL is returned. Detail levels:
--detail basic|standard|full|custom
. Use
for the current column catalog (full adds
,
,
,
keys). Dropdowns:
,
. Export prints row progress and API timing to stderr.
bash
python3 scripts/pipeline.py review presets --template lead-review
python3 scripts/pipeline.py review export-payload --workspace popcam --tag nace --detail standard
python3 scripts/pipeline.py review export --template lead-review --workspace popcam \
--tag nace --detail standard --title "NACE Review"
python3 scripts/pipeline.py review sync --template lead-review --workspace popcam \
--sheet-id SHEET_ID --detail standard --dry-run
python3 scripts/pipeline.py review sync --template lead-review --workspace popcam \
--sheet-id SHEET_ID --detail standard --commit
Email-finder candidates (safe domain export)
Never use
COALESCE(domain, company)
— use this command to emit batch-find JSON with real
only:
bash
python3 scripts/pipeline.py email-finder-candidates --workspace popcam --tag nace \
--no-email --require-domain --never-contacted
# Scope to a CSV batch (returns scanned / skipped_has_email / candidates)
python3 scripts/pipeline.py email-finder-candidates --workspace popcam --file outreachmagic/batches/find-batch.json
also supports
,
, and
. Force large relay snapshot pages with
(or
for routine sizes).
bash
python3 scripts/pipeline.py history --linkedin linkedin.com/in/janedoe
After
, use
for per-campaign event and lead counts (unchanged).
Log every outreach send
bash
python3 scripts/pipeline.py log-event \
--lead-id 1 --type email_sent --direction outbound \
--subject "Quick intro" --workspace thesystemsmethod
is
required in multi-workspace mode. Outreach events are workspace-scoped — they belong to a specific pipeline. In single-workspace mode it falls back to the default workspace.
Update stage and log replies
bash
python3 scripts/pipeline.py update-stage \
--id 1 --stage replied --next-action "Send case study" --workspace thesystemsmethod
is
required in multi-workspace mode. Stage is per-workspace — a lead can be "contacted" in one workspace and "interested" in another.
Connect sequencers (paid)
If the user already has a key, skip the browser flow:
bash
python3 scripts/pipeline.py login
Generate webhook URLs for platforms directly from the CLI (requires agent key):
bash
python3 scripts/pipeline.py connect-platform --platform smartlead
python3 scripts/pipeline.py connect-platform --platform instantly
python3 scripts/pipeline.py connections
Update skill scripts
bash
python3 scripts/pipeline.py update
Lead Fields Reference
| Field | CLI flag | Notes |
|---|
| name | | Required |
| company | | |
| title | | Job title |
| industry | | e.g. Martech, Fintech, Healthcare |
| headcount | | Size band, e.g. 1-10, 50-200, 1000+ |
| email | | Dedup key — unique per lead |
| linkedin | | LinkedIn profile URL |
| channel | | email, linkedin, whatsapp (default: email) |
| stage | | Pipeline stage (default: prospecting) |
| notes | | Free-form |
| tags | | JSON array string like '["vip","enterprise"]' |
| workspace | | Optional on ; required on and in multi-workspace mode |
Privacy & Security
- Local-first data. Pipeline leads, events, and campaign stats live in local SQLite ( → ).
- Relay pass-through. Webhooks hit ; the CLI imports them locally via . We store tokens and usage on our side, not a searchable cloud copy of your outreach archive.
- Portal API. handles tokens, billing, and optional workspace routing sync when connected.
- Credentials. Store relay tokens in
config/outreachmagic_config.json
only. Never hardcode tokens in SKILL.md or commit them to git.
- Read before connect. See SECURITY.md for full data boundaries and vulnerability reporting.
Common Pitfalls
- Time-window analytics: use (no pull). Latest activity: pull before / .
- Forgetting add-lead before log-event
- Not updating stage after reply
- Setup/auth errors (including 401 Unauthorized) should run
python3 scripts/pipeline.py login
in terminal.
- Version: run — do not guess from SKILL.md frontmatter alone.
- Relay archive stays on api.outreachmagic.io; dedupes locally. Use for a true rebuild (sync + backup + wipe + ). alone only helps after deleting the DB manually.
- Tags: plain names (, ) — not JSON list strings like . Run for bracket-form tags.
- on an existing email does not enrich — use or rely on relay for fill-if-empty updates.
ModuleNotFoundError: data_freshness
— run .
- Large batches — chunked 200 rows; if save times out, re-run with on your export JSON/CSV.
Pull Troubleshooting Runbook
When relay flow appears stale, diagnose before using destructive reset commands:
bash
python3 scripts/pipeline.py pull --diagnose
python3 scripts/pipeline.py pull --full --diagnose
Diagnostic verdicts:
- — no events returned for the current cursor window.
relay has events but deduped
— relay returned events already recorded in local .
- — event cursor moved forward ( increased).
- — relay returned a full page but cursor did not advance; inspect relay pagination.
- Pull uses id cursors only: (webhooks) and per-table snapshot cursors (
last_snapshot_core_after_id
, last_snapshot_workspace_after_id
, last_snapshot_company_after_id
). No on relay pull.
If events were ingested but still seem missing, inspect a specific lead timeline:
bash
python3 scripts/pipeline.py history --email "<lead_email>" --json