Agent skill
outreachmagic
Your agent goes blind after send.
Filed under Outbound email.
From outreachmagic/outreachmagic · 1 skill entry · 11 · pushed 2026-09-17
What it does when it runs
Your agent goes blind after send. Sync sequencer webhooks, research leads via Serper, and find/verify emails — all in one local SQLite DB your agent queries directly.
Automated analysis of the skill and the 112 files bundled beside it. A skill’s own description is written to be selected by an agent, so it describes the job and not the dependencies.
- Keys and connectors you must supply
- API_KEY
- CAMPAIGN_FILTER_KEY
- CONFIG_AGENT_SECRETS_VERSION_KEY
- CONFIG_API_BASE_KEY
- CONFIG_ORGANIZATION_ID_KEY
- CONFIG_ROUTING_VERSION_KEY
- ENV_KEY
- FIRECRAWL_API_KEY
- ICYPEAS_API_KEY
- MILLIONVERIFIER_API_KEY
- MSG_NO_AGENT_KEY
- OUTREACHMAGIC_AGENT_KEY
- OUTREACHMAGIC_API_KEY
- SCRUBBY_API_KEY
- SERPER_API_KEY
- TRYKITT_API_KEY
- Hosts it reaches
- api.firecrawl.dev
- api.github.com
- api.hubapi.com
- api.millionverifier.com
- api.outreachmagic.io
- api.scrubby.io
- api.trykitt.ai
- app.icypeas.com
- app.outreachmagic.io
- bulkapi.millionverifier.com
- cdn.jsdelivr.net
- developers.hubspot.com
- docs.cursor.com
- google.serper.dev
- hermes-agent.nousresearch.com
- highlevel.stoplight.io
- linkedin.com
- outreachmagic.io
- rdap.org
- services.leadconnectorhq.com
- trykitt.ai
- www.linkedin.com
- www.python.org
- Tool permissions it declares
- No
allowed-toolsin the frontmatter. It does act, so it runs under whatever permissions your session already grants. - Actions present in the files
- shellwrites filesnetwork
Install it
View source on GitHub ↗git clone --depth 1 --filter=blob:none --sparse https://github.com/outreachmagic/outreachmagic.git /tmp/outreachmagic git -C /tmp/outreachmagic sparse-checkout set "skills/outreachmagic" mkdir -p ~/.claude/skills/outreachmagic cp -R "/tmp/outreachmagic/skills/outreachmagic/." ~/.claude/skills/outreachmagic/
Picked up without a restart. A project skill of the same name is shadowed by your personal one. For one repository only, swap ~/.claude/skills for .claude/skills. Claude Code docs ↗
The folder is the same in every client that implements the format — 46 of them — so if yours is not above, only the destination changes.
Before you install: this skill will not complete its job on a bare agent. It needs API_KEY, CAMPAIGN_FILTER_KEY, CONFIG_AGENT_SECRETS_VERSION_KEY, CONFIG_API_BASE_KEY, CONFIG_ORGANIZATION_ID_KEY, CONFIG_ROUTING_VERSION_KEY, ENV_KEY, FIRECRAWL_API_KEY, which you have to obtain separately.
The skill
Source on GitHub ↗Reproduced in full from outreachmagic/outreachmagic/blob/31363eeaaf8b164368199c70f82b9854e240a5ba/skills/outreachmagic/SKILL.md, which is licensed MIT (skill frontmatter). 3,684 words, 39 headings.
Outreach Magic
Sync Smartlead, Instantly, HeyReach, PlusVibe, EmailBison, Prosp, and Calendly into one local SQLite DB. Research leads via Serper, find and verify emails via trykitt/Icypeas/MillionVerifier/Scrubby — all from one skill, one install, one SKILL.md.
CLI convention
python3 scripts/pipeline.py <command> # run from skill root
python3 scripts/pipeline.py paths # resolve install paths anytime
Requires Python 3.10+. Stock macOS ships python3 at 3.9 — if commands fail with a
version error, use the Homebrew interpreter directly (e.g. python3.12 scripts/pipeline.py ...).
install.sh's _find_python() already detects and uses the right interpreter automatically.
Config keys: data_root (share DB across agents), api_base_url, dev_repo.
Platform install
OM_VERSION=v1.9.1
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}"
Agent-readable install guide: AGENTS-INSTALL.md. Use --platform cursor / --platform claude for other agents.
Hermes profiles: real files in ~/.hermes/skills/; profiles symlink. Re-run install for new profiles: bash install.sh --platform hermes --profile <name>.
First-Time Setup
Always check if already connected first:
python3 scripts/pipeline.py version
python3 scripts/pipeline.py pull # returns error if no key configured
If pull fails with "No agent key or token configured", run pipeline.py login (opens browser for sign-in). Tell the user: "Opening Outreach Magic sign-in — come back when you're done." Never paste secrets into chat.
If setup is already done (pull succeeds), skip to showing data:
python3 scripts/pipeline.py pull
python3 scripts/pipeline.py show
Setup portal: https://app.outreachmagic.io/onboarding. Account errors (account_revoked): direct to [email protected].
Common workflows
| User says | You do |
|---|---|
| "Show my pipeline" | pull to show |
| "Sync my sequencers" | pull --full to report new records |
| "Import my Sales Nav CSV" | import-profiles --file ... --workspace W --dry-run first |
| "Research Jane Doe at Acme Corp" | enrich.py check "Jane Doe" "Acme Corp" → if not_found, run Serper search pack |
| "Research my CSV of leads" | enrich.py batch-check --workspace W file.csv → then Serper for unmatched |
| "Find email for Bill at stripe.com" | email_finder.py find --name "Bill" --domain stripe.com (saves by default, use --no-save to skip or --dry-run to preview) |
| "Find emails for my CSV" | email_finder.py batch-find --dry-run → --yes |
| "Verify these emails" | email_finder.py verify-bulk --yes |
| "Deep verify catch-all emails" | email_finder.py verify-with-scrubby --workspace W --dry-run → --yes |
| "Export to Google Sheets" | whoami --json → share_email, then sheets export ... |
| "Connect Smartlead / Instantly" | connections create --platform ... and share webhook URL |
| "Push leads to our CRM" | crm-sync sync --workspace W --dry-run first, then without --dry-run (GHL / HubSpot) |
| "Open the dashboard" | dashboard — local web UI at http://127.0.0.1:8765 (deliverability, pipeline, campaigns; stage/enrich/log-event actions) |
whoami --json returns account email, org, and plan. init creates the local DB. Sync dashboard API keys: pipeline.py sync-secrets.
Network & privacy
- Default: All lead data stays in local SQLite.
- Inbound only:
pullimports webhook/agent events fromapi.outreachmagic.io. - Outbound upload: Only
pipeline.py sync(user- or agent-initiated). Import and local edits never auto-upload. - Update check: GitHub release tag lookup (read-only, no lead data, ≤1/hour).
Version & updates
python3 scripts/pipeline.py version # authoritative — not SKILL.md frontmatter
python3 scripts/pipeline.py update # user-triggered (never auto-downloads)
python3 scripts/pipeline.py update --check # check without installing
Updates are user-triggered only. The CLI may print a notice when a newer release exists (≤1/hour). Releases are pinned to GitHub tags, not the moving main branch.
When to Use
- About to send outreach (email, LinkedIn, WhatsApp)
- Researching a prospect and want to track them
- User asks "show my pipeline" or "how is outreach going"
- User says "track this" followed by outreach details
- User asks for campaign breakdowns, engagement analytics, or workspace inventory
- User wants to connect a sequencer platform
- User asks about connection status, webhook URLs, or platform health
Agent Behavior Rules
- Bulk enrichment: use
import-profiles, not repeatedadd-lead. - Reads:
pipeline.py querypresets first. See references/query-guide.md. - Don't guess table or column names.
pipeline.py schema [table] [--relations]prints them, and references/schema.md has the whole thing including how each entity is scoped to a workspace (leadshas noworkspace_id— it joins throughworkspace_leads).PRAGMAis blocked by the read-only guard;schemais the way. - Freshness goes to stderr, and is also in the JSON body (
freshness_message). Do not strip it out of stdout with a regex — pass--quiet(orOM_QUIET=1) instead. --limit 0means no cap. Ad-hoc SQL stops at 500 rows by default and says so on stderr; atruncatedflag comes back in the JSON.OM_WORKSPACEsupplies--workspacewhen you do not pass it.- Writes: only
pipeline.pymutation commands. NeverINSERT/UPDATE/DELETEvia ad-hoc SQL. - After any
pull: report exact number of new records imported. - Analytics format: (1) human table, (2) preset name or SQL used, (3) freshness note. Offer
pullif they need latest data. - Do not run
pullbefore local time-window analytics unless user asks for latest/refresh. - Run
pullfirst when showing live activity (show,historyfor "what just happened"). - Never run
syncunless the user asked. Never runarchive --purgewithout explicit confirm after--dry-run. (A user clicking the dashboard's "Push to relay" button counts as asking.) - Answer with
pipeline.py versionwhen user asks about version (authoritative). - Pipeline stages:
prospecting→contacted→replied→interested→scheduled→won|not_interested|lost.
Deliverability and bounces
Everything on the dashboard's Deliverability page is a command:
python3 scripts/pipeline.py query deliverability --workspace acme --since 7d --json
python3 scripts/pipeline.py deliverability domains --workspace acme --json # sends, provider, reseller, age, DNSBL
python3 scripts/pipeline.py deliverability mailboxes --workspace acme --flagged --json
python3 scripts/pipeline.py deliverability blacklist --domain acmemail.com --json # every list, verdict and check time
python3 scripts/pipeline.py deliverability bounces --workspace acme --group-by signature --json
python3 scripts/pipeline.py deliverability bounce-detail --id <id> --json # message + the sends before it
python3 scripts/pipeline.py deliverability trend --mailbox [email protected] --json
python3 scripts/pipeline.py sender-domains registration # domain age via RDAP (cached)
python3 scripts/pipeline.py campaign-replies --workspace acme --kind human --json
The Campaigns table is a command too — reach for it before writing reply SQL:
# "positive human replies in the last 30 days, by campaign"
python3 scripts/pipeline.py campaign-audit --workspace acme --since 30d --kind human --json
python3 scripts/pipeline.py campaign-audit --workspace acme --exclude-tag existing_customer
# per-day sends and replies, and this window against the one before it
python3 scripts/pipeline.py campaign-audit --workspace acme --since 14d --by-day --json
python3 scripts/pipeline.py campaign-audit --workspace acme --since 14d --compare-previous-window
campaign-audit returns one row per campaign plus a totals row. --kind
narrows the reply and sentiment columns only (sends stay whole, or the rates
describe a population that does not exist); --tag / --exclude-tag narrow
every column. Hand-writing the reply condition is how the date filter gets
swallowed by operator precedence and a 30-day question returns all-time totals —
see references/query-guide.md. --by-day covers the daily breakdown, so that
is not a reason to write SQL either; references/sql-pitfalls.md lists the rest.
"Which titles / industries actually reply" is a command too:
python3 scripts/pipeline.py attribute-audit --workspace acme --since 14d --field title --json
Same window and filters as campaign-audit, grouped by a lead attribute.
Positives are dated from first_positive_at, not current_sentiment_since —
that column is when the status LAST changed, so windowing on it drops every
lead whose status has not moved since.
A DNSBL result of check_failed means the list refused or timed out — it is not a
listing. Report it as "could not check", never as blacklisted.
campaign-replies splits leads into human / auto_reply / bounce / status_only.
A bounce and an out-of-office both set a sentiment, so "we got 4 replies" is wrong
unless you looked at the kinds.
Pull policy
python3 scripts/pipeline.py pull # full sync
python3 scripts/pipeline.py pull --if-stale 5m # skip if pulled within 5 min
python3 scripts/pipeline.py pull --skip-routing-sync # events only (fast)
python3 scripts/pipeline.py pull --probe # backlog only, no ingest
python3 scripts/pipeline.py pull --kind events # webhook events only
Analytics routing
| User intent | Command |
|---|---|
| Reply/engagement counts in time window | query replies / query engagement --since … --json |
| Lead rows / pipeline detail | show / lead-table (use --limit) |
| All-time totals | stats / campaigns --json |
| Tag / LinkedIn connection counts | workspace summary --workspace <slug> --json |
| Message bodies | history |
| Fresh webhook events | pull or pull --kind events |
| Dashboard / connection health | status / connections |
Relay sync progress legend and batch size details: references/command-reference.md.
Pricing
| Tier | Price | Webhook events | Features |
|---|---|---|---|
| Free | $0 | 1,000 / period | 1 sequencer, single workspace |
| Pro | $9/mo | 50,000 / mo | All sequencers, multi-workspace routing |
| Scale | $29/mo | 250,000 / mo | Unlimited workspaces, priority support |
Only webhook and sync traffic counts. Local tracking, queries, exports do not count. Over-quota events are buffered. Sign up: https://outreachmagic.io
Quick Reference
pipeline.py show # pipeline table
pipeline.py lead-table # canonical lead info
pipeline.py history --id 1 # lead timeline
pipeline.py history --email [email protected] # lookup by email
pipeline.py stats # pipeline stats
pipeline.py campaigns # per-campaign counts
pipeline.py query engagement --workspace W --since 48h --json
pipeline.py query replies --workspace W --since 7d --json
pipeline.py workspace summary --workspace W --json
pipeline.py phone list --lead-id 42 # numbers on a lead (or --company-id)
pipeline.py phone add --lead-id 42 --phone "612-555-0143" --label mobile --source apollo
pipeline.py status # dashboard overview
pipeline.py connections # webhook URLs + event counts
pipeline.py connect-platform --platform smartlead # generate webhook URL
pipeline.py db-health # local DB diagnostics
pipeline.py platform-map --json # vendor event type map
pipeline.py campaign-map list # show routing rules
pipeline.py campaign-map add --platform P --workspace W # add routing rule
pipeline.py campaign-map conflicts # list name_exact rows shadowing a broader rule
pipeline.py campaign-map deactivate --id MAP_ID # soft-deactivate one stale routing row
pipeline.py campaign-map reconcile --dry-run # preview re-routing already-ingested leads/events
pipeline.py agent-changes # cross-platform sync (JSON)
pipeline.py crm-sync sync --workspace W --dry-run # preview CRM push (GHL/HubSpot)
pipeline.py crm-sync sync --workspace W # push leads to CRM
pipeline.py crm-sync sync --workspace W --max-age 30d # only leads active in last 30 days
pipeline.py sync # push to relay
pipeline.py dashboard # local web dashboard (http://127.0.0.1:8765)
pipeline.py refresh --yes # backup + rebuild DB
Core Workflow
# Add a lead
pipeline.py add-lead --name "Jane" --email "[email protected]" --company "Acme" \
--title "VP Marketing" --channel email --stage prospecting --workspace W
# Log an outreach event
pipeline.py log-event --lead-id 1 --type email_sent --direction outbound \
--subject "Quick intro" --workspace W
# Update stage
pipeline.py update-stage --id 1 --stage replied --sentiment positive \
--next-action "Send case study" --workspace W
# A lead's status looks stale or wrong? Rebuild it from its latest status event
pipeline.py repair-lead-status --workspace W --since 90d # dry run
pipeline.py repair-lead-status --workspace W --since 90d --apply
# Bulk import CSV/JSON (preferred over repeated add-lead)
pipeline.py import-profiles --file leads.csv --workspace W --dry-run
pipeline.py import-profiles --file leads.csv --workspace W
# Bulk enrich from research (Serper, Apollo, etc.)
pipeline.py import-profiles --file enriched.csv --workspace W \
--source sales_navigator --source-detail "Q2 list"
add-lead returns {"status": "exists", "id": N} on duplicates (matched by email, LinkedIn, or name+company). import-profiles uses tiered identity matching: external_id → email → LinkedIn → phone → name+domain → name+company.
Read the dry run before the real run. Each row reports would_fill — the
columns that row would actually populate — so "did they have a LinkedIn before?"
is answered without querying the database. Real runs report enriched_by_field
({"linkedin_url": 296, "email": 38}) rather than one opaque total.
Same person, two profile URLs. A list that includes someone twice under two
LinkedIn URLs and no email looks like two people to the identity tiers.
--on-identity-conflict decides what happens: review (default — create, but
report both records under identity_review), merge, or create. Every run
also reports duplicate_candidates, the same-name/same-company groups it
touched, with ids ready for merge-leads. Nothing merges silently: two people
can legitimately share a name, a title and an employer.
Formats detected automatically: Sales Navigator and AI Ark. A raw AI Ark
export imports with no preprocessing — AI Ark People ID maps to external_id,
so a later pull of the same source matches on identity tier 1 instead of
duplicating.
Full import field reference, personalization workflow, email verification, dedup, Google Sheets export, quarantine management, and troubleshooting: references/command-reference.md.
Company-only lists (Google Maps / directory scrapes)
A scraped business list has no people in it. Import it with an explicit record type so the rows are never mistaken for contacts:
python3 scripts/pipeline.py import-profiles --file dealers.csv \
--record-type company_placeholder --source google_maps
company_placeholder rows are real records — taggable, personalizable, linked
to a company — but they are excluded from the contacts list, email-finder
targeting and CRM sync, because there is no person to contact yet. Once
research turns up real contacts at those companies:
python3 scripts/pipeline.py record-type --resolve # dry run
python3 scripts/pipeline.py record-type --resolve --yes # execute
Stubs that were never sent to are deleted; stubs with outreach history are kept
and stamped superseded_at, so the record of what was sent survives.
Without --record-type, import auto-detects only when the name carries a clear
business signal (Inc, LLC, Motors, a marque, an &, a digit). That is
deliberately conservative: a sole trader whose company is their own name looks
identical to a scraped business, and misclassifying a real person hides them
from outreach. Pass the flag when you know the list is companies.
That whitelist misses plenty of real dealerships — K L M of Riverton,
Corwin Vance, Brennt of Ashgrove carry no business token at all, and 30 rows
of one 773-row manufacturer import landed as fake people because of it. Two
things now cover that:
- Known directory sources skip the whitelist. When
--sourceis a manufacturer locator or a Maps scrape (mercedes-benz-official, anything ending-official,google_maps— seeCOMPANY_DIRECTORY_SOURCES), a row whose name IS its company with no email/title/LinkedIn is a company, whatever the name looks like. - Email finding refuses them anyway. A
contactrow whose name equals its company and that has no email, title or LinkedIn is excluded from finder targeting, so a missed classification costs no credits.
Personalization scope: contact or company?
A personalization field belongs to exactly one scope. Lead-scoped values are per person; company-scoped values are shared by every contact at that company. Writing to the wrong one is silent, so say which you mean:
| You want | Column name / command |
|---|---|
| Per-contact value | personalized_<field> · personalize-set |
| Per-company value | company_personalized_<field> · company-personalize-set |
Before reusing a field name, look at what it already holds:
python3 scripts/pipeline.py personalization-fields --values
# lead icp_segment 2857 entities 9 values
# mercedes franchise 971
# independent dealer 673
# office_owners 275 ← another campaign's values
The first write to a scope claims the name; a later write to the other scope is rejected rather than quietly landing in a second table. Fields that predate the registry were backfilled from what was on disk.
import-profiles reports where every column went, in
summary.personalization_routing. Anything listed under
personalization_routing_guessed was routed by the old name-shape heuristic
because nothing had claimed it — worth a look before you trust it.
Company personalization is one value per company, but a sheet is per lead. If the same company arrives with several values for one field, the import aborts and names them rather than letting the last row win:
pipeline.py import-profiles --file segments.csv --company-conflict last-wins
Updating personalization on existing leads
A sheet of lead_id + personalized columns is a personalization update, not an
import. Say so, or the merge value becomes the person's name — this is how
"Brian Williams" once became "Brian":
python3 scripts/pipeline.py import-profiles --file segments.csv \
--mode personalization-only --workspace W
Rows must match an existing lead. Nothing is created, no profile column is
touched, and unmatched rows come back as a list instead of becoming new leads.
Validation runs first on every import (not just --dry-run) and reports rows
that won't match, values that would be overwritten, and company conflicts.
Bulk input: always --file
Every command that takes a batch accepts --file (and --json - for stdin).
Passing a path to --json used to fail as JSONDecodeError: Expecting value,
which reads like bad data rather than the wrong flag:
pipeline.py personalize-set --batch --file segments.json
pipeline.py company-personalize-set --batch --file companies.json
pipeline.py tag bulk --workspace W --tags campaign-x --file lead_ids.txt
Test data
Mark synthetic rows at the door so they can never reach a campaign:
pipeline.py import-profiles --file fixtures.csv --test --workspace W
pipeline.py test-leads suggest # real-looking leads that may be test data
pipeline.py test-leads set --lead-ids 1,2,3
Test leads are excluded from lists, counts, exports and bulk actions by default
(--test all to include, --test only to see just them) — the same discipline
as suppression. Note that test-leads suggest is a report: a real contact
imported under a test-named source is a naming problem, not a test lead, and
flagging it would hide them from every export.
Contact sourcing (find-contacts)
Turns companies with no people into companies with people: the staff page is fetched to markdown, a regex pass pulls name+title pairs, and only the pages it could not crack come to you.
python3 scripts/pipeline.py icp set --workspace acme --name decision-makers \
--whitelist "general manager,service manager,owner" \
--blocklist "assistant general manager"
# Always dry-run first: reports targets and worst-case credits, spends nothing.
python3 scripts/pipeline.py find-contacts --workspace acme --icp decision-makers --dry-run
python3 scripts/pipeline.py find-contacts --workspace acme --icp decision-makers --max-fetches 50
# The tail the regex pass could not crack:
python3 scripts/pipeline.py contact-extract-pending --workspace acme --limit 20 --json
python3 scripts/pipeline.py contact-apply --batch --workspace acme --json '[...]'
# Or hand a page to a person: every candidate, with the ICP's verdict on each.
python3 scripts/pipeline.py contact-review --workspace acme --icp decision-makers
python3 scripts/pipeline.py contact-apply --company-id 83544 --contact-ids 3,7 --workspace acme
python3 scripts/pipeline.py contact-review --company-id 83544 --none-of-these --workspace acme
Always --dry-run before a real run, and always pass --max-fetches. Firecrawl bills per page and the credits do not roll over. The dry run counts cache misses, so its estimate stays honest on a second pass, and --reparse re-extracts cached pages for zero credits when the ICP changes.
Spawn a subagent per batch. This is not a style preference.
contact-extract-pending returns whole page bodies — roughly 8–10k tokens each,
so a batch of 20 is ~200k. Read into the main conversation that is a 10M-token
run and a context you cannot recover; read into a subagent it dies with the
batch. For anything over ~5 companies:
One subagent per batch of 20. The subagent calls
contact-extract-pending, extracts, callscontact-apply --batch, and reports one line:"batch 3/34: 31 contacts, 4 no-data". The main thread never sees markdown.
676 companies is ~34 such batches and ~34 short lines in the main thread.
Applying is idempotent. A contact is keyed on name + company domain, never
on the title (titles get re-scraped differently), so re-running a batch matches
the same leads instead of duplicating them. --dry-run reports what would be
attached and writes nothing. The ICP blocklist is enforced on apply even though
you were handed it — a "never contact this person" rule should not depend on
having been honoured.
contact-review is the human surface, and you should not pre-empt it. It
prints every person on the page — the ones the ICP kept and the ones it refused,
with the reason — and nothing is marked. Don't summarise it down to a
recommendation: the rejects are why someone is looking, and the whole point is
to find out where the ICP is wrong. Candidate ids are positions in the cached
page, so they survive an ICP edit but not a re-fetch; pass the payload's
content_hash to contact-apply --content-hash when the two are not seconds
apart. --none-of-these is a decision that gets recorded, not a skip.
Lead Fields Reference
| Field | CLI flag | Notes |
|---|---|---|
| name | --name | Required |
| record_type | --record-type | contact (default) or company_placeholder |
| company | --company | |
| title | --title | Job title |
| industry | --industry | e.g. Martech, Fintech |
| headcount | --headcount | Size band, e.g. 11-50, 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: '["vip","enterprise"]' |
| workspace | --workspace | Required on log-event and update-stage in multi-workspace mode |
Privacy & Security
- Local-first. Pipeline data in local SQLite (
pipeline.py paths→database). - Relay pass-through. Webhooks hit
api.outreachmagic.io; imported locally viapull. - Portal API.
app.outreachmagic.iofor tokens, billing, routing config. - Credentials. Store in
config/outreachmagic_config.jsononly. Never hardcode in SKILL.md or git. - Full disclosure: SECURITY.md.
Common Pitfalls
- Time-window analytics: use
query engagement(no pull). Latest activity: pull beforeshow/history. - Forgetting
add-leadbeforelog-event. - Not updating stage after a reply.
- Auth errors (401): run
pipeline.py loginin terminal. - Version: run
pipeline.py version— not SKILL.md frontmatter. - Fresh DB rebuild:
refresh --yes.pull --fullalone skips already-ingested rows. - Tags: plain names (
nace,vip), not JSON['nace']. Runtag repairfor bracket-form tags. add-leadon existing email does not enrich — useimport-profilesor relaypullfor fill-if-empty.ModuleNotFoundError: data_freshness— runpipeline.py update.- Large imports: chunked 200 rows. Re-run with
--fileon export if timeout.
Files bundled with it
These load only when the skill asks for them, so they cost nothing until it runs.
- README.md
- SECURITY.md
- lead-review-presets.json
- install.sh
- outreachmagic.mdc
- agent-triage.md
- command-reference.md
- dashboard-panes.md
- email-finder.md
- email-finding-research.md
- query-guide.md
- schema.md
- sql-pitfalls.md
- VERSION
- activity_sync.py
- agent_secrets_cloud.py
- api_key_pool.py
- batch_runner.py
- batch_sync_to_relay.py
- blacklist_monitor.py
- bounces.py
- campaign_stats.py
- connections_cloud.py
- constants.py
- contact_discovery.py
- contact_extract.py
- contact_icp.py
- contact_review.py
- credits.py
- __init__.py
- base.py
- ghl.py
- hubspot.py
- crm_sync.py
- dashboard.html
- dashboard_actions.py
- dashboard_server.py
- data_freshness.py
- data_hygiene.py
- db_conn.py
- db_health.py
- deliverability_cli.py
- detect_platform.py
- device_login.py
- domain_discovery.py
- domain_registration.py
- email_finder.py
- enrich.py
- event_classification.py
- event_dedup.py
- formatters.py
- generate_schema_reference.py
- health.py
- icypeas.py
- import_formats.py
- junk_cleanup.py
- lead_actions.py
- lead_emails.py
- lead_export.py
- lead_links.py
- lead_status_repair.py
- lead_sync.py
- linkedin_connections.py
- millionverifier.py
- normalize.py
- om_paths.py
- outbox.py
- page_cache.py
- phone_numbers.py
- pipeline.py
- pipeline_batch_jobs.py
- pipeline_cli.py
- pipeline_dedup.py
- pipeline_lead_review.py
- pipeline_migration.py
- pipeline_personalize.py
- pipeline_provider_attempts.py
- pipeline_sender_accounts.py
- pipeline_sync.py
- pipeline_tags.py
- pipeline_update.py
- pipeline_utils.py
- pipeline_workspace.py
- platform_registry.py
- progress.py
- provider_observations.py
- public_emails.py
- quarantine_resolutions.py
- query_cli.py
- read_queries.py
- relay_extractors.py
- relay_ingest.py
- reporting_queries.py
- reprocess.py
- review_cloud.py
- routing_cloud.py
- schema.py
- schema_views.py
- scrubby.py
- serper_candidates.py
- serper_review.py
- shared.py
- suppression.py
- sync_audit.py
- sync_contract.py
- sync_debug.py
- trykitt.py
- user_messages.py
- waterfall.py
- workspace_archive.py
- workspace_routing.py
- update-manifest.json
Need help setting it up?
This page tells you what outreachmagic does and what it needs. Cheetah builds the agent setup it runs inside: data, CRM, sequencing and the guardrails.
Book a call →The directory stays free. There is nothing gated behind this.