Sync Smartlead, Instantly, HeyReach, PlusVibe, EmailBison, Prosp, MasterInbox, and Calendly into a local SQLite DB your AI agent can query. Pipeline visibility for Claude Code, Cursor, and Hermes.
SaferSkills independently audited outreachmagic (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
All commands below use the pipeline CLI in this skill's scripts/ directory (run from the skill root, or use absolute paths from pipeline.py paths):
python3 scripts/pipeline.py <command>Resolve install paths anytime:
python3 scripts/pipeline.py pathsOptional config keys: data_root (share one DB across platforms), api_base_url, dev_repo for local development.
Install from outreachmagic/outreachmagic — pin a release tag; download and verify SHA256 before running locally:
OM_VERSION=v1.2.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.
Use --platform cursor or --platform claude for other agents. Setup: https://app.outreachmagic.io/onboarding
~/.hermes/skills/ — profiles symlink only (../../../skills/<skill>)pipeline.py pathsbash install.sh --platform hermes --profile <name>pipeline.py update writes the global install; all profiles pick it up via symlinkInstall to ~/.cursor/skills/outreachmagic/. Invoke with /outreachmagic or ask about your pipeline in plain English.
Install to ~/.claude/skills/outreachmagic/. SKILL.md is the source of truth.
Environment variable: OUTREACHMAGIC_AGENT_KEY — overrides the config file agent_key. Set via .env, shell profile, or CI/CD.
On startup, always check if the agent is already connected by running:
python3 scripts/pipeline.py versionThen check whether an agent key exists in the config:
python3 scripts/pipeline.py pullIf pull returns an error like "No agent key or token configured", the user needs to set up.
When setup is needed, run pipeline.py login — 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.
If the skill is not installed yet, point them to https://app.outreachmagic.io/onboarding or https://app.outreachmagic.io/agent, then connect.
Account access error: If login fails with an account error (account_revoked in CLI/API), tell the user there was a sign-in problem and direct them to [email protected] for help.
| User says | You do |
|---|---|
| "Show my pipeline" | pull → show |
| "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 → email-finder-candidates → batch-find --workspace W --yes |
| "Export to Google Sheets" | whoami --json → share_email, 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.
init creates the database under <skill_home>/databases/. Dashboard API keys sync to <skill_home>/config/agent_secrets.env (next to outreachmagic_config.json). CSVs and exports live under `outreachmagic/imports/` and `outreachmagic/exports/` relative to your workspace directory (where the agent runs commands). Set "project_root" in config to pin a fixed folder instead of cwd. Run pipeline.py paths for resolved paths (batches/, sheets/, archive/, logs/ too).
If pull 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:
python3 scripts/pipeline.py pull
python3 scripts/pipeline.py showpull imports webhook/agent events from api.outreachmagic.io (user- or cron-initiated).One version for the whole skill. To see what is installed, always run:
python3 scripts/pipeline.py versionThe version: line in this file is synced from scripts/VERSION 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:
python3 scripts/pipeline.py update
# or
hermes skills updateCheck without installing: pipeline.py update --check. Install a specific release: pipeline.py update --tag v1.2.0.
Install commands for each platform are in Platform install above. After install, run python3 scripts/pipeline.py login.
status, connections)connect-platform, disconnect-platform)add-lead.workspace_lead_events when the question is relay volume (use `events`).pipeline.py mutation commands (add-lead, import-profiles, log-event, sync, personalize, tags, etc.). Never INSERT/UPDATE/DELETE via ad-hoc scripts.pull, explicitly report the exact number of new records imported.history on the specific lead.copy-insights.campaigns or stats. For last N hours/days by campaign, use `query engagement` or `query replies` — not campaigns.export or custom Python to aggregate.workspace_lead_tags) and LinkedIn connection state (workspace_lead_linkedin_status, is_connected per sender) are different.status or connections.connect-platform --platform <id>.--dry-run.pipeline.py platform-map --json.pull if they need latest relay data).| User intent | Use |
|---|---|
| Replies / engagement / campaign breakdowns (time window) | `query engagement` or `events` + `campaigns` |
| Lead list with stage/sentiment | show / lead-table |
| Tag counts, LinkedIn connected-by-sender | workspace summary --json |
| Message bodies / copy winners | history, copy-insights |
| Workspace ingest audit (rare) | workspace_lead_events — not for volume analytics |
workspace_lead_events is a subset; full relay timelines are in `events`.
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 --jsonAdvanced: pipeline.py query --sql 'SELECT …' --params '["popcam |%"]' --json (read-only).
Read commands print data freshness on stderr (and last_pull / stale_minutes in --json). If data is a few minutes old, tell the user before running pull unless they asked for up-to-the-minute.
| User intent | Command |
|---|---|
| Reply/engagement counts in a time window | query replies / query engagement --since … --json |
| Lead rows / pipeline detail | show (use --limit; avoid --json unless needed) |
| All-time totals | stats / campaigns --json |
| Fresh webhook events | pull or pull --kind events |
Do not run `pull` before local time-window analytics (last 48h, since today, engagement by campaign) unless the user asks for latest/refresh or you need relay catch-up.
Run `pull` first when showing live pipeline activity the user expects to be current (show, history for “what just happened”), or when they say sync/refresh/latest.
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:
python3 scripts/pipeline.py pull --skip-routing-syncThis fetches the latest events from the relay. Skip for offline/local analytics; use for fresh activity timelines.
Relay sync progress (stdout): When interpreting pull / sync output or outreachmagic/logs/batch_sync.log, use the log legend in docs/relay-sync-progress.md. Short version:
~ on counts/page estimate once per stream start, e.g. [02:10] ↓ Event : ~12,400 pending (~13p @ 1000/p) ...[02:11] ↓ Event : p13/62 — 1,000 this page, 13,000/62,400 (20%) ... (pull, no ok)limit=1/stream, no ingest) — use before a large catch-up pull--skip-snapshots when you only need the event cursor advanced[03:56] ↑ Event : p2/13 — ok 7.9s, 5,000 this page (10,000/62,093 (16%)) then done — N in Mp (Xs)batch 1/46 = lead-id walk; inner pN/M = HTTP pages inside one sync. Do not confuse them.Relay sync limits: Same endpoints always — POST /push and GET /pull. No separate bulk URLs.
cloud_pending snapshots ≥ 2500, uses 5000 entries per `/push`; otherwise routine batch size (default 200, max 500 per request).p2/118 and % remaining after a one-time pending probe per snapshot kind. Use pull --kind events if snapshots are already synced.show --since, workspace queries) — the relay does not filter by date or workspace.`workspace summary` reads local SQLite only (fast, works offline). Use when the user asks for counts by tag or LinkedIn sender connection state. Optional pull first if they need freshly synced tags/connection imports.
python3 scripts/pipeline.py workspace summary --workspace <slug> --json
python3 scripts/pipeline.py workspace summary --workspace <slug> --json --tags-onlyExample JSON keys: lead_count, last_pull, tags (tag, lead_count), linkedin_senders (sender_slug, connected, pending), linkedin_connected_leads. With --tags-only, LinkedIn keys are empty arrays/zero.
Companion attempt tags: `serper_attempted` (lead-enrich), `trykitt_attempted` / `icypeas_attempted` (email-finder), `mv_attempted` (MillionVerifier bulk — verification status still in email_verification_status). Found email is represented by leads.email, latest_source, and email_verification_status — not a separate tag.
Tag-only (same tag data as summary): pipeline.py tag list --workspace <slug>.
Only webhook and sync traffic: sequencer webhooks and pipeline.py sync uploads to the cloud (one event per sync batch, not per lead). Local tracking (log-event, add-lead), enrichment dedup, email finding, queries, and exports do not count.
Sequencer webhooks are always accepted. Over-quota events are buffered and delivered on your next pull or when the quota resets. Events are only rejected if the buffer cap is reached. Check status with pipeline.py status.
Sign up at https://outreachmagic.io
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 [email protected]
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.csvDashboard-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.
# 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 --yesShow locally-created leads and events (not from relay) as JSON or CSV. Useful for transferring data between platforms (Cursor, Hermes, Claude Code).
# 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 --allPush to relay for cross-platform sync:
python3 scripts/pipeline.py syncsync pushes pending lead snapshots (profile, external_id, company_domain, HQ/location, tags, mailmerge, workspace status, LinkedIn connection flags) plus local events, and quarantine resolutions (skip / assign) to the relay. Large backlogs use 5000 entries per `/push` 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 sync --no-health-report. Other machines run pull --full after a DB reset to restore everything that was synced.
Unmapped webhook events land in unmapped_campaign_queue. Resolve locally, then sync so other machines and pull --full stay consistent.
No campaign id/name: New webhook events are quarantined automatically. Legacy rows in events with campaign_id IS NULL are backfilled on init into quarantine and auto-skipped (no workspace mapping possible). Use history to inspect event details if needed. Manual: quarantine backfill-no-campaign.
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 syncpull, not immediately).campaign-map rules (no relay resolution).Relay stores resolutions in D1 (queue_resolutions). The first event page of each pull requests them (include_queue_resolutions=1); later pages reuse the in-memory map.
`sync --status` counters: recommended_mode is bulk when cloud_pending_leads ≥ 2500 (else push). relay_untracked_leads = imported/local leads with no relay pull history (normal after CSV; data is still in the shared DB). cloud_pending_leads = rows waiting to push — run sync. local_agent_events = agent-originated events not yet on relay.
python3 scripts/pipeline.py db-health
python3 scripts/pipeline.py db-health --json
python3 scripts/pipeline.py db-health --fullRead healthStatus, rulesTriggered (each has a hint), rowCounts, and tableBreakdown. Cloud copy: GET /api/agent/status → localDb after user has run sync.
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 --purgeFresh DB + full CSV round-trip:
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 --fullFile-based transfer (no server):
# Machine A: export
pipeline.py agent-changes --file changes.csv
# Machine B: import
pipeline.py import-profiles --file changes.csv --overwriteCounts by tag and LinkedIn connection accepted/pending per sender. Local DB only — no relay call; last_pull in output shows data freshness.
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>Relay imports auto-populate campaign names from webhook payloads (Smartlead, PlusVibe, etc.).
python3 scripts/pipeline.py campaigns
python3 scripts/pipeline.py campaigns --jsonstats also includes a campaign section. Use campaigns when the user only wants counts by campaign name.
Point PlusVibe webhooks at your relay URL (…/plusvibe/{token}). Select all event types and category labels in PlusVibe — including any custom categories in the user’s instance. Standard ones to verify:
EMAIL_SENT, ALL_EMAIL_REPLIES, BOUNCED_EMAILLEAD_MARKED_AS_INTERESTED, LEAD_MARKED_AS_NOT_INTERESTED, LEAD_MARKED_AS_OUT_OF_OFFICE, LEAD_MARKED_AS_AUTOMATIC_REPLYLEAD_MARKED_AS_MEETING_BOOKED, LEAD_MARKED_AS_MEETING_COMPLETED, LEAD_MARKED_AS_WRONG_PERSON, LEAD_MARKED_AS_CLOSEDDo not enable ALL_POSITIVE_REPLIES (duplicates ALL_EMAIL_REPLIES) or FIRST_EMAIL_REPLIES (subset of ALL_EMAIL_REPLIES). 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 --auto-reply true). Bounces set event sentiment invalid but do not auto-move the lead to stage lost (use --sentiment invalid to find them).
Point EmailBison webhooks at your relay URL (…/emailbison/{token}). In EmailBison, go to Settings → Webhook, paste the URL, and toggle the events below. Enable all seven:
email.sent — sent from sender to leademail.bounced — delivery failure (hard and soft bounces)lead.replied — lead replied to an emaillead.interested — lead marked as interested (replies with positive intent)lead.unsubscribed — lead unsubscribed from the campaigntag.attached — a tag was added to the leadtag.removed — a tag was removed from the leadAll event types are accepted and stored. lead.interested sets stage to qualified locally. lead.replied sets stage to engaged. Bounces are recorded in the bounce events table with EmailBison-specific bounce field paths (data.bounce.type, data.bounce.reason, data.lead.mx_provider). Tags are stored as tag_attached / tag_removed event types with their raw vendor type preserved.
Campaign id/name are extracted from data.campaign.id / data.campaign.name in the payload.
After pull, filter the pipeline by current status (latest status-bearing event per lead):
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 --jsonFilter by date (created or updated on/after a date):
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 --jsonThen open full timeline for any lead (all events, not just the status event):
python3 scripts/pipeline.py history --id 1Native copy-performance analysis (full message bodies + best template):
python3 scripts/pipeline.py copy-insights --lead-status interested
python3 scripts/pipeline.py copy-insights --lead-status interested --jsonshow --json and lead-table --json return {"leads": [...], "data": [...], ...} with personalization, tags, and latest_sender when available.
python3 scripts/pipeline.py export --workspace acme_corp --tag nace --format csv
python3 scripts/pipeline.py export --workspace acme_corp --since today --format jsonWrites to outreachmagic/exports/ under your workspace by default. CSV uses personalized_first_name, personalized_company_name, plus lead fields, tags, HQ, and latest_sender.
Not for Google Sheets — export writes local files only. For a hosted Google Sheet, use sheets export or review export (below).
Prefer the guarded refresh command (syncs first, backs up, then rebuilds):
python3 scripts/pipeline.py refresh --yesPreview tag fixes without writing:
python3 scripts/pipeline.py tag repair --dry-run
python3 scripts/pipeline.py tag repairManual equivalent (no pre-sync backup):
rm <skill_home>/databases/outreachmagic.db # see pipeline.py paths
python3 scripts/pipeline.py init
python3 scripts/pipeline.py pull --fullTell 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 pull --full alone — it skips already-imported rows.”
pull --full only re-downloads relay pages; it does not clear relay_ingested. Use refresh when you need a true rebuild.
LinkedIn IDs (v1.17): Public profiles are stored in linkedin_url as linkedin.com/in/handle (no https://). Sales Nav (ACwAA…) and member IDs (urn:li:member:…) are stored as identity aliases and used for matching when the public slug arrives later.
python3 scripts/pipeline.py history --id 1
python3 scripts/pipeline.py history --email [email protected]
python3 scripts/pipeline.py history --name "Jane Doe"
python3 scripts/pipeline.py history --id 1 --jsonOutputs lead info + numbered event timeline with direction arrows (← inbound, → outbound), human-readable timestamps, and event details.
python3 scripts/pipeline.py add-lead \
--name "Jane Doe" --company "Acme Corp" --title "VP Marketing" \
--industry "Martech" --headcount "50-200" \
--email "[email protected]" \
--channel email --stage prospectingTo also associate the lead with a workspace at creation time:
python3 scripts/pipeline.py add-lead \
--name "Jane Doe" --email "[email protected]" --company "Acme Corp" \
--workspace thesystemsmethod --stage contacted--workspace is optional on add-lead — 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 name+company, returns {"status": "exists", "id": N}.
Use `import-profiles` for spreadsheets, enriched exports, or batched research — not repeated add-lead calls. Matching uses tiered identities (strongest first): external_id → email → LinkedIn → phone → name+domain → name+company → import_key (name-only rows). CSV columns unified_lead_id / source_id are accepted as aliases and stored as external_id. Fills empty fields only (same as relay/PlusVibe); use --overwrite to replace existing values.
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":"[email protected]","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`verify-email`: Records LEV rows locally and sets cloud_pending — run sync to push verification fields to the relay (same as other lead mutations).
Email-finder batch save (known `lead_id` on every row): email-finder calls apply-email-find-results — updates email, workspace tags, and provider verification in one pass. Requires --workspace. Run sync after batch-find when COMPLETE shows pending snapshots. Manual recovery:
python3 scripts/pipeline.py apply-email-find-results \
--workspace your_workspace --source trykitt --source-detail "email-finder/batch" \
--file import.jsonUse import-profiles when rows lack lead_id / need tiered matching or CSV-only fields (personalization columns, import_batch_id, etc.).
Core profile fields (column aliases — first non-empty wins):
| Canonical field | Aliases | Required |
|---|---|---|
email | lead_email, work_email | No (see identity tiers below) |
linkedin | linkedin_url, lead_linkedin_url, profile_url | No |
name | full_name, display_name (or first_name + last_name) | No |
title | job_title, role | No |
company | company_name, organization, org | No |
industry | — | No |
headcount | company_size, employees, employee_count | No |
location_city | city, lead_city | No |
location_state | state, region, lead_state | No |
location_country | country, lead_country | No |
Headcount normalization: headcount is stored as-is (text) plus a computed headcount_numeric (integer midpoint). Ranges like "11-50" become 30, "500+" becomes 500, 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 |
|---|---|
company_domain | Stored in companies table, normalized (strips protocol/www/path) |
hq_city / hq_state / hq_country | Company HQ location, stored on companies table |
mailmerge_first_name | Auto-populated as first_name in personalization table |
mailmerge_company_name | Auto-populated as company_name in personalization table |
import_name / list_source | Attribution + namespace for external_id when value has no : |
external_id | CRM/list ID in lead_identities (namespaced list_source:id if bare) |
unified_lead_id, source_id | Import aliases → same as external_id |
import_batch_id (CLI flag) | Stable dedupe for name-only rows via import_key within a batch |
lead_status | Requires --workspace; normalized (lowercase, spaces) and set on workspace_leads |
lead_sentiment | Requires --workspace; normalized (lowercase) and set on workspace_leads |
tags | Requires --workspace; semicolon or comma separated, normalized (lowercase), stored in workspace_lead_tags |
contact_order | Requires --workspace; integer priority stored as contact_priority on workspace_leads |
is_connected_linkedin | Requires --workspace + --sender-profile; true/1/yes sets connected status |
is_linkedin_request_pending | Requires --workspace + --sender-profile; true/1/yes sets pending status |
Normalization rules:
"VIP" and "vip" are the same tag"Not_Interested" becomes "not interested""11-50" → 30)Attribution is automatic: every import sets original_source (immutable first touch) and latest_source (updates each time) on the lead, following the Salesforce/HubSpot model. Use `--source` for the machine-readable channel (sales_navigator, trykitt, lead_enrich, csv_import, …). Use `--source-detail` or import_name/list_source columns for list/campaign labels. Per-row list_source overrides the CLI --source default when present.
Lead fields (first_name, contact-specific lines): per lead. Company fields (company_name, company_*): org-wide, one write per account.
| Raw field | Mail-merge field | Scope |
|---|---|---|
name | first_name | lead |
company / companies.name | company_name | company |
# 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 --jsonImport: mailmerge_first_name → lead; mailmerge_company_name, mailmerge_company_* → company. Sync pushes lead and company snapshots separately; merge is local.
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.
# 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 [email protected]
# List leads needing verification
python3 scripts/pipeline.py verify-pending --limit 50 --jsonVerification status values: valid, invalid, catch-all, unknown, spamtrap, abuse, do_not_mail, risky, bounced, soft_bounce
Bounce handling: Platform bounces (from relay sync) are auto-recorded in lead_email_verification with source="platform_bounce". 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.
company_id (business email domain or company name on ingest).lead field.python3 scripts/pipeline.py merge-leads --keep 12 --merge 34
python3 scripts/pipeline.py merge-leads \
--email [email protected] --linkedin linkedin.com/in/janedoepython3 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 # applyTo export leads to an editable Google Sheet (two-way sync), use `sheets export` or `review export` — not export --format csv.
Requires pipeline.py login. Sheets are created on app.outreachmagic.io (no local Google credentials).
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.
Requires pipeline.py login. Sheets are created on app.outreachmagic.io and shared to your org owner email (or --share-email). Check Merge? in the sheet, then sync.
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 --commitRequires login. Sheets are created on app.outreachmagic.io with edit/lock header icons (no column colors). All usage notes live in the lead_id header cell note; freeze row and dropdowns apply before the export URL is returned. Detail levels: --detail basic|standard|full|custom. Use review presets for the current column catalog (full adds lev_*, be_*, latest_source*, linkedin_sender_<handle> keys). Dropdowns: workspace_stage, lead_sentiment. Export prints row progress and API timing to stderr.
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 --commitNever use COALESCE(domain, company) — use this command to emit batch-find JSON with real companies.domain only:
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.jsonexport also supports --never-contacted, --no-email, and --require-domain. Force large relay snapshot pages with sync --bulk (or sync --no-bulk for routine sizes).
python3 scripts/pipeline.py history --linkedin linkedin.com/in/janedoeAfter pull, use `campaigns` for per-campaign event and lead counts (unchanged).
python3 scripts/pipeline.py log-event \
--lead-id 1 --type email_sent --direction outbound \
--subject "Quick intro" --workspace thesystemsmethod--workspace 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.
python3 scripts/pipeline.py update-stage \
--id 1 --stage engaged --next-action "Send case study" --workspace thesystemsmethod--workspace is required in multi-workspace mode. Stage is per-workspace — a lead can be "contacted" in one workspace and "engaged" in another.
Stages: prospecting -> contacted -> engaged -> qualified -> proposal -> won | lost
If the user already has a key, skip the browser flow:
python3 scripts/pipeline.py loginGenerate webhook URLs for platforms directly from the CLI (requires agent key):
python3 scripts/pipeline.py connect-platform --platform smartlead
python3 scripts/pipeline.py connect-platform --platform instantly
python3 scripts/pipeline.py connectionspython3 scripts/pipeline.py update| Field | CLI flag | Notes |
|---|---|---|
| name | --name | Required |
| company | --company | |
| title | --title | Job title |
| industry | --industry | e.g. Martech, Fintech, Healthcare |
| headcount | --headcount | Size band, e.g. 1-10, 50-200, 1000+ |
--email | Dedup key — unique per lead | |
--linkedin | LinkedIn profile URL | |
| channel | --channel | email, linkedin, whatsapp (default: email) |
| stage | --stage | Pipeline stage (default: prospecting) |
| notes | --notes | Free-form |
| tags | --tags | JSON array string like '["vip","enterprise"]' |
| workspace | --workspace | Optional on add-lead; required on log-event and update-stage in multi-workspace mode |
pipeline.py paths → database).api.outreachmagic.io; the CLI imports them locally via pull. We store tokens and usage on our side, not a searchable cloud copy of your outreach archive.app.outreachmagic.io handles tokens, billing, and optional workspace routing sync when connected.config/outreachmagic_config.json only. Never hardcode tokens in SKILL.md or commit them to git.query engagement (no pull). Latest activity: pull before show / history.python3 scripts/pipeline.py login in terminal.pipeline.py version — do not guess from SKILL.md frontmatter alone.pull dedupes locally. Use refresh --yes for a true rebuild (sync + backup + wipe + pull --full). pull --full alone only helps after deleting the DB manually.nace, vip) — not JSON list strings like ['nace']. Run tag repair for bracket-form tags.import-profiles or rely on relay pull for fill-if-empty updates.pipeline.py update.--file on your export JSON/CSV.When relay flow appears stale, diagnose before using destructive reset commands:
python3 scripts/pipeline.py pull --diagnose
python3 scripts/pipeline.py pull --full --diagnoseDiagnostic verdicts:
relay empty — no events returned for the current cursor window.relay has events but deduped — relay returned events already recorded in local relay_ingested.cursor advanced — event cursor moved forward (last_max_id increased).cursor stalled — relay returned a full page but cursor did not advance; inspect relay pagination.last_max_id (webhooks) and per-table snapshot cursors (last_snapshot_core_after_id, last_snapshot_workspace_after_id, last_snapshot_company_after_id). No since on relay pull.If events were ingested but still seem missing, inspect a specific lead timeline:
python3 scripts/pipeline.py history --email "<lead_email>" --json~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.