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 gooseworks install --claude # Then in your agent: /gooseworks <prompt> --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:
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ...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:
import sys, os
sys.path.insert(0, os.path.join("tools", "supabase"))
from supabase_client import SupabaseClient
client = SupabaseClient(os.environ["SUPABASE_URL"], os.environ["SUPABASE_SERVICE_ROLE_KEY"])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:
in_cooldown = client.check_cooldown(client_name="happy-robot", cooldown_days=84)
# Returns set of person_id strings still in cooldownOr query directly:
outreach_log for person_ids with sent_date in the last 84 days:
GET /rest/v1/outreach_log?select=person_id&sent_date=gte.{84_days_ago}&status=neq.bounced&client_name=eq.{client}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
mcp__smartlead__get_email_accountsPresent available accounts. User selects which to use.
Step 2: Create campaign
mcp__smartlead__create_campaign
name: {campaign_name}Save the returned campaign_id.
Step 3: Add sequence steps
mcp__smartlead__save_campaign_sequences
campaign_id: {campaign_id}
sequences: [
{ seq_number: 1, subject: "...", email_body: "...", seq_delay_details: { delay_in_days: 0 } },
{ seq_number: 2, subject: "...", email_body: "...", seq_delay_details: { delay_in_days: 4 } },
{ seq_number: 3, subject: "...", email_body: "...", seq_delay_details: { delay_in_days: 7 } }
]Merge variable mapping: Convert {first_name} → {{first_name}}, {company} → {{company}} (Smartlead uses double-brace syntax).
Step 4: Import leads (batch 100)
mcp__smartlead__add_leads_to_campaign
campaign_id: {campaign_id}
lead_list: [{ email: "...", first_name: "...", last_name: "...", company_name: "...", ... }]Batch in groups of 100 if more than 100 leads.
Step 5: Assign sending accounts
mcp__smartlead__add_email_accounts_to_campaign
campaign_id: {campaign_id}
email_account_ids: [...]Step 6: Set schedule
mcp__smartlead__update_campaign_schedule
campaign_id: {campaign_id}
schedule: { ... }Step 7: Configure settings
mcp__smartlead__update_campaign_settings
campaign_id: {campaign_id}
settings: { track_opens: true, track_clicks: false, stop_on_reply: true }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
skills/cold-email-outreach/output/{campaign-name}-{YYYY-MM-DD}.csvStep 3: Provide tool-specific import instructions
Instantly:
Lemlist:
Apollo:
Other / Manual:
Present campaign summary:
Campaign: {name}
Leads: {count}
Sequence: {touches} touches over {days} days
Sending: {accounts} accounts × {daily_limit}/day = {daily_volume} emails/day
Estimated completion: {date}
Tool: {smartlead/instantly/etc.}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:
POST /rest/v1/outreach_log
Prefer: return=minimal
[
{
"person_id": "{person_uuid}",
"campaign_name": "{campaign_name}",
"external_campaign_id": "{smartlead_campaign_id or null}",
"channel": "email",
"tool": "{smartlead/instantly/lemlist/apollo/manual}",
"sent_date": "{ISO timestamp}",
"status": "sent",
"client_name": "{client_name}"
},
...
]Or use the shared client:
client.log_outreach(entries)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:
PATCH /rest/v1/people?id=in.({person_ids})
{ "last_contacted": "{ISO timestamp}" }{count} people logged to outreach_log
last_contacted updated for {count} people
Campaign ID: {id}
Cooldown active until: {date + 84 days}
Next eligible re-contact: {date}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:
skills/cold-email-outreach/output/Create this directory if it doesn't exist. Files are named {campaign-name}-{YYYY-MM-DD}.csv.
lead-qualification and wants to act on the resultsmcp__smartlead__*. No additional setup.Check and improve your brand's visibility across AI search engines (ChatGPT, Perplexity, Gemini, Grok, Claude, DeepSeek). Set up tracking, run visibility analyses, audit your website for AI readability, and get actionable recommendations. Uses the npx goose-aeo@latest CLI.
Extract competitor and customer intelligence from any company's landing page HTML. Discovers tech stack, analytics tools, ad pixels, customer logos, SEO metadata, CTAs, hidden elements, and more. No API keys required.
Discover all customers of a given company by scanning websites, case studies, review sites, press, social media, job postings, and more. Use when you need competitive intelligence on who a company sells to.