npx skills add ...
npx skills add nikiandr/goose-skills --skill cold-email-outreach
End-to-end cold email outreach orchestration. Handles goal alignment, lead selection from Supabase, sequence design, email generation (via email-drafting), campaign setup in the user's chosen outreach tool, and logging. Tool-agnostic — supports Smartlead (default), Instantly, Lemlist, Apollo, or manual CSV export.
npx skills add nikiandr/goose-skills --skill cold-email-outreach
The final mile of the outbound pipeline. Takes qualified leads from Supabase, builds email sequences using the email-drafting skill, loads campaigns into the user's chosen outreach tool, and logs everything back to Supabase.
Tool-agnostic: Asks the user which outreach platform they use. Defaults to Smartlead if they have MCP tools configured. Falls back to CSV export for any other tool or manual workflow.
Load this skill when:
lead-qualification and wants to act on the resultsThis skill does NOT assume a specific tool. It asks first, then adapts.
| Tool | Integration | How It Works |
|---|---|---|
| Smartlead (default) | MCP tools (mcp__smartlead__*) | Full automation: create campaign, add sequences, import leads, configure schedule, launch |
| Instantly | CSV import | Generate CSV matching Instantly's import format, user uploads manually |
| Lemlist | CSV import | Generate CSV with Lemlist-compatible columns |
| Apollo | CSV import | Generate CSV matching Apollo sequence import format |
| Manual / Other | CSV + instructions | Export leads + emails as generic CSV, provide setup instructions |
Tool selection logic:
email, first_name, last_name, company, title, subject, body per touch) and ask user for their tool's import requirementsPeople must be stored in Supabase with the schema from tools/supabase/schema.sql. The people and outreach_log tables must exist. Run python3 tools/supabase/setup_database.py if setting up fresh.
Environment variables in .env:
mcp__smartlead__*. No additional setup.Ask all questions at once. Organize by category. Skip any already answered by an upstream skill.
client_nameicp_segmentqualification_score above a thresholdsource (crustdata, apollo, linkedin, etc.)Use the shared Supabase client:
Map user criteria to PostgREST query parameters on the people table:
| User Says | PostgREST Filter |
|---|---|
| "VP Operations" | title=ilike.*VP Operations* |
| Client "happy-robot" | client_name=eq.happy-robot |
| Score > 7 | qualification_score=gte.7 |
| Has verified email | email_verified=eq.true |
| Industry "logistics" | industry=ilike.*logistics* |
| Location "San Francisco" | location=ilike.*San Francisco* |
| Source "crustdata" | source=eq.crustdata |
| Not contacted in 84 days | or=(last_contacted.is.null,last_contacted.lt.{84_days_ago}) |
Always exclude people contacted within 84 days (12 weeks). This is not optional.
Use the shared client's check_cooldown() method:
Or query directly:
outreach_log for person_ids with sent_date in the last 84 days:
person_ids into an exclusion setid=not.in.({excluded_ids}) to the people queryShow a sample table (10-15 leads) with:
Tell user: total eligible leads, how many excluded by cooldown, how many have verified emails.
Ask user to confirm or adjust filters before proceeding.
Present the sequence plan as a table before writing any copy:
| Touch | Day | Email Type | Framework | CTA |
|---|---|---|---|---|
| 1 | 1 | Cold intro | Signal-Proof-Ask | 15-min call |
| 2 | 5 | New angle / asset | PAS | Resource offer |
| 3 | 12 | Social proof | BAB | Open to chat? |
Get user approval on the structure before generating copy in Phase 3.
Load the email-drafting skill and pass it:
Tier 1 (Generic): Generate one template per touch with merge fields ({first_name}, {company}, {title}). Same template for all leads.
Tier 2 (Segment): Generate one template per segment per touch. Segments are defined by role, industry, or signal type. Swap pain points and proof points between segments.
Tier 3 (Deep): Generate unique email per lead per touch. Cap at 50 leads — recommend Tier 2 above that volume.
Full automation via MCP tools. Execute in this order:
Step 1: List email accounts
Present available accounts. User selects which to use.
Step 2: Create campaign
Save the returned campaign_id.
Step 3: Add sequence steps
Merge variable mapping: Convert {first_name} → {{first_name}}, {company} → {{company}} (Smartlead uses double-brace syntax).
Step 4: Import leads (batch 100)
Batch in groups of 100 if more than 100 leads.
Step 5: Assign sending accounts
Step 6: Set schedule
Step 7: Configure settings
Step 1: Generate CSV
Columns depend on personalization tier:
Tier 1 (same template for all):
email, first_name, last_name, company, title, custom_field_1 (signal/hook)Tier 2/3 (per-segment or per-lead emails):
email, first_name, last_name, company, title, touch_1_subject, touch_1_body, touch_2_subject, touch_2_body, touch_3_subject, touch_3_bodyStep 2: Save file
Step 3: Provide tool-specific import instructions
Instantly:
Lemlist:
Apollo:
Other / Manual:
Present campaign summary:
Do NOT activate the campaign without explicit user confirmation. Present the summary, then ask: "Ready to launch? Type 'yes' to activate."
mcp__smartlead__update_campaign_status → set to activeAll database writes in this phase require the user's prior approval from the launch gate in Phase 5. The Phase 5 approval ("Ready to launch?") covers both the campaign activation AND the subsequent logging. However, if the campaign was exported as CSV (not launched via Smartlead), confirm with the user before logging — they may not have actually imported/sent yet.
After launch (or export), insert records into outreach_log:
Or use the shared client:
For CSV-based tools: Log with status = "exported". It changes to "sent" when user confirms they launched the campaign in their tool.
Update last_contacted on the people table for all people in this campaign:
Reference section for cooldown logic used throughout this skill.
| Rule | Detail |
|---|---|
| Default cooldown | 84 days (12 weeks) from sent_date |
| Bounced leads | Exempt from cooldown — email never reached them. Filter: status=neq.bounced when checking cooldown |
| Active campaign leads | Always ineligible — if a lead is in an active campaign (status = "sent", no reply/bounce), they cannot be added to another campaign |
| User override | User can explicitly override cooldown for specific leads — ask for confirmation before allowing |
| Null last_contacted | Leads never contacted are always eligible |
Campaign exports are saved to:
Create this directory if it doesn't exist. Files are named {campaign-name}-{YYYY-MM-DD}.csv.
in_cooldown = client.check_cooldown(client_name="happy-robot", cooldown_days=84)
# Returns set of person_id strings still in cooldown