| name | outbound |
| description | This skill should be used when the user asks to "send emails", "do outreach", "contact prospects", "run outbound sales", or wants to execute outbound sales. Automatically sends emails, fills in contact forms, and sends SNS DMs to prospects on the list. Count can be specified. |
| argument-hint | <project-id> [count] |
| allowed-tools | ["Bash","Read","mcp__claude_in_chrome__tabs_context_mcp","mcp__claude_in_chrome__tabs_create_mcp","mcp__claude_in_chrome__navigate","mcp__claude_in_chrome__read_page","mcp__claude_in_chrome__find","mcp__claude_in_chrome__get_page_text","mcp__claude_in_chrome__form_input","mcp__claude_in_chrome__computer","mcp__claude_in_chrome__javascript_tool","mcp__claude_in_chrome__read_network_requests","mcp__plugin_lead-ace_api__get_outbound_targets","mcp__plugin_lead-ace_api__send_email_and_record","mcp__plugin_lead-ace_api__record_outreach","mcp__plugin_lead-ace_api__record_outreach_with_inquiry","mcp__plugin_lead-ace_api__update_outreach_status","mcp__plugin_lead-ace_api__update_prospect_status","mcp__plugin_lead-ace_api__get_document","mcp__plugin_lead-ace_api__get_master_document","mcp__plugin_lead-ace_api__get_project_settings","mcp__plugin_lead-ace_api__pick_subject_variant","mcp__plugin_lead-ace_api__get_compliance_status"] |
Outbound - Outbound Sales Execution
A skill that sequentially reaches out to prospects on the sales list via email, contact forms, and SNS DMs.
For each prospect, sends a message via an available channel and records the result in the DB. After all processing, generates a summary report.
Steps
1. Setup
- Project ID:
$0 (required)
- Approach count:
$1 (default: 30)
Compliance pre-flight (run before anything else). Call
mcp__plugin_lead-ace_api__get_compliance_status. If ready: false,
abort immediately — do not load documents, do not call
get_outbound_targets. Report to the user which fields are missing
(from the missing array) and direct them to the URL in fix_url
(e.g. https://app.leadace.ai/tenant-settings). Tell them to re-run
this skill once the workspace fields are saved. Sending without these
will fail with HTTP 412 at the send call regardless, so bailing here
saves the token cost of draft generation.
Load documents via MCP:
Call mcp__plugin_lead-ace_api__get_document with projectId: "$0" and slug: "business".
Call mcp__plugin_lead-ace_api__get_document with projectId: "$0" and slug: "sales_strategy".
Pay particular attention to these sections in the sales_strategy document:
- Outreach mode:
precision (deep personalization) or volume (template-based semi-personalization). Default to precision if not set
- Sales channels: Channel priority and which channels not to use
- Messaging: Subject line patterns, body structure, and A/B test instructions if any -- follow them
- Sender information: Signature block (organization phone, name, title, etc.). Sender display name and email come from project settings, not from this document — the backend send path uses them automatically
- Email template: If a template is defined, use it as a base (especially important in volume mode)
- SNS messages: SNS DM messaging policy
Important: If SALES_STRATEGY.md has specific instructions on subject line variations, A/B tests, etc., always follow them. Never ignore instructions and revert to default behavior.
Note: Sending timing (day of week, time of day) is not controlled by this skill.
Retrieve the uncontacted prospect list and the project's send settings:
- Call
mcp__plugin_lead-ace_api__get_outbound_targets with projectId: "$0" and limit: $1 (default 30).
- Call
mcp__plugin_lead-ace_api__get_project_settings with projectId: "$0".
The targets response includes Outbound mode: send | draft. Capture this value — it determines whether each channel actually delivers (send) or only stores a draft for the user to review and send manually from https://app.leadace.ai/drafts (draft). The draft path applies to all channels (email, form, SNS).
From the settings response, surface for body composition:
inquiryLandingEnabled — drives the mail template branch in step 3.
inquiryChatBrief (when non-empty) — the system-prompt fragment the
recipient's inquiry chat will use. Skim it so the email body can promise
something concrete the chat can actually deliver (e.g., if the brief
carries pricing FAQ, the CTA can say "ask about pricing in chat"; if it
doesn't, do not promise pricing answers).
inquiryOneLiner (when non-empty) — the tagline the recipient sees on
the landing page. Avoid contradicting it in the email subject / opener.
outboundMode, senderEmailAlias, senderDisplayName — read but do
not surface in body; the backend uses them. The compliance block
(legal_name / physical_address / privacy_url) is also tenant-level and
is appended by the backend at send time — never inline these in the body.
Each prospect in the targets list also carries:
cycle: { n, kind, lastOutreach, lastResponse } — the prospect's
outreach history for this project. n is the count of confirmed sends.
kind ∈ first | no_response | rejection_followup. lastOutreach.subject
is the most recent subject used; lastResponse.responseType is the most
recent response (rejection / reply / etc.). Drives tone selection in step 3
and the re-approach branch in step 3b.
hasFreshSignal: boolean — true when org_signals_global.signals_updated_at
is within the last 14 days. Used by the prioritisation server-side; you
may surface it in the report.
If the tool returns a "Project not found" error, instruct the user to run /setup first and abort.
2. Approach Each Prospect
Pick one channel per prospect. SALES_STRATEGY's "Sales Channels"
section is authoritative when present — follow its order and inclusion
list verbatim. The ladder below is the default used when SALES_STRATEGY
omits "Sales Channels" or doesn't constrain ordering.
Default channel ladder (apply when SALES_STRATEGY does not override):
- Personal email —
email is set AND looks like a named address
(first.last@, flast@, f.last@, etc., not a department mailbox).
- LinkedIn DM —
snsAccounts.linkedin is set. Skip this rung if the
prospect is not a 1st-degree connection (the browser flow surfaces this).
- Department email —
email is set and starts with a department
prefix like sales@, bd@, partnerships@, recruiting@.
- Generic email —
email is set and starts with info@, contact@,
support@, hello@, pr@. Demoted but never excluded — for many
small companies it is the only reachable address. The mid-funnel reply
rate is lower; reflect that in priority, not in eligibility.
- Contact form —
contactFormUrl is set.
- X / Twitter DM —
snsAccounts.x is set. Lowest rung because reach
depends on the recipient's DM-settings.
If a step is the only one that produces a valid channel for the prospect,
use it. Skip channels SALES_STRATEGY explicitly disables (e.g. "we don't
do SNS DMs"). One channel per prospect — do not chain channels.
Country pre-flight. Each prospect carries country (and the
organization carries one as fallback). LeadAce currently only allows sends
to recipients in US, CA, and JP — anywhere else is blocked at send
time with HTTP 422. Skip non-allowed prospects up front so the report
stays clean:
- Treat
country ∈ {null, undefined, 'US', 'CA', 'JP'} as eligible. null
is warn-only at send time; we proceed but the operator should backfill.
- For any other country code, do NOT call the send tool. Instead call
mcp__plugin_lead-ace_api__record_outreach with status: "failed"
and errorMessage: "skipped: country not supported (<code>) — currently sends to US/CA/JP only". The server stamps next_outreach_after so
the prospect drops out of get_outbound_targets for the recycle
window; quota is not consumed.
Attempt limit per prospect: Limit sending attempts to a maximum of 2 per prospect (main channel + 1 fallback only when the main channel fails for a transient reason). If both fail for any reason, immediately skip and move to the next prospect. Do not waste context and tool calls lingering on a single prospect.
SNS DM caution: SNS DMs have a lower reach rate (depends on recipient's DM settings). Skip if SNS is disabled in SALES_STRATEGY.
Bad-timing skip (optional, on by default). If the prospect's overview
contains a ## Timing section that explicitly says now is a bad moment
(layoffs ongoing, just announced bankruptcy / wind-down, recent leadership
shake-up implying the buyer left, post-acquisition integration freeze),
skip outreach entirely:
- Do NOT send.
- Call
mcp__plugin_lead-ace_api__record_outreach with status: "failed"
and errorMessage: "skipped: bad timing — <one-line reason>". The
server stamps the prospect's next_outreach_after to
now + noResponseRecycleDays (default 90 days), so they drop out of
get_outbound_targets until that window elapses. No quota is consumed.
- Continue to the next prospect.
If the user passed an explicit override ("send anyway", "ignore timing"),
skip the skip — they own the trade-off.
If claude-in-chrome is not connected (send mode only): Contact-form submission and SNS DMs require it. Target only prospects with email addresses and skip those where the channel is unavailable. Report skipped count in results report as "Skipped due to browser not connected: N". In draft mode this restriction does not apply — the skill never opens the browser, so all channels work.
3. Email Sending
Retrieve email guidelines via mcp__plugin_lead-ace_api__get_master_document with slug: "tpl_email_guidelines" and follow them. Get the signature block from the "Sender Information" section of SALES_STRATEGY.md (append it to the body). Sender display name and From: address are applied automatically by send_email_and_record from project settings — do not pass them as arguments.
Subject line variation (round-robin). Subject patterns are stored
server-side in subject_variants and rotated by the server. Per send,
call mcp__plugin_lead-ace_api__pick_subject_variant with the project
id; the response is { variantId, subjectPattern, label } and the
server's cursor advances by one. Render the subject by substituting
{{org}} / {{name}} / {{signal}} placeholders the pattern uses,
then forward the variantId to send_email_and_record so
outreach_logs.variant_id is stamped (this is what /evaluate joins
to compare reply rates).
If pick_subject_variant returns NOT_FOUND ("No active subject
variants"), the project has no patterns registered yet — generate a
short one-off subject, send without variantId, and surface the gap in
the run-end report so the operator can add patterns via
upsert_subject_variant (or via /strategy's onboarding step). Do not
fabricate a SALES_STRATEGY.md "Subject Line Patterns" section; that
content is no longer the authoritative source.
Signal-aware opening (Phase 1.5 hook). If the prospect's overview
contains a ## Recent Signals section with at least one entry, the email's
first sentence must reference the most recent / most relevant signal
in concrete terms ("Saw the Series B announcement on TechCrunch last
week — congrats on…", "Noticed you're hiring senior platform engineers in
Seattle…"). One signal mention, then move to the actual ask. If
## Recent Signals is absent or empty, do not invent one; open per
SALES_STRATEGY's normal pattern.
Inquiry-aware CTA branch. When inquiryLandingEnabled === true
(captured in step 1):
- The body's primary CTA should invite the recipient to spend ~5 minutes
on the inquiry-landing page ("If a 5-minute AI conversation works
better than scheduling a call, you can ask anything here: "). Frame it as low-effort,
recipient-led, no calendar required.
- Backup CTAs (reply / scheduling link) stay but in second position.
When inquiryLandingEnabled === false:
- Use the existing reply / scheduling-link / meeting-request CTA pattern
per SALES_STRATEGY.md.
In both branches the inquiry / unsubscribe URLs and the compliance
footer (legal name, physical address, privacy URL) are appended by
the backend — do not insert any of them in the body, and do not
include a closing address block from SALES_STRATEGY.md's "Sender
Information" beyond the human signature (name + role + sign-off).
Duplicating the address yields a confused-looking footer. If
send_email_and_record returns 412 Tenant compliance settings incomplete, surface the message verbatim and tell the user to fill in
Workspace settings at https://app.leadace.ai/tenant-settings before
retrying.
Body personalization (vary depth by outreach mode):
- Precision mode: Refer to each prospect's
overview and matchReason, and write the entire body tailored to the recipient -- not just the opening. Reference specific numbers, achievements, and initiatives of the target company. Generic openers like "I visited your website" alone are insufficient
- Volume mode: Use the SALES_STRATEGY.md email template as a base, adjusting the opening (why you're reaching out) and the problem statement in 2 places based on
overview / matchReason. The solution through CTA can follow the template structure as-is
Always call mcp__plugin_lead-ace_api__send_email_and_record regardless of mode:
projectId: "$0"
prospectId: the prospect's id
to: array with the recipient address
subject: subject line (rendered from the variant's subjectPattern)
body: complete body including signature (no compliance footer — the
backend appends it)
variantId: from pick_subject_variant; omit when no variants exist
The server reads the project's outboundMode and decides what happens:
send mode → email is sent via the user's connected Gmail. Response: { mode: "sent", outreachId, messageId, threadId }.
draft mode → no send; the composed email is stored as a pending_review draft for the user to review and send from https://app.leadace.ai/drafts. Response: { mode: "drafted", outreachId }. Drafts do not count against the outreach quota.
Track the response mode per call for the step 8 report (sent vs. drafted counts).
On a 502 Send failed, the outreach is still logged with status: "failed" and the prospect's next_outreach_after is stamped to defer re-eligibility by noResponseRecycleDays (default 90 days) — do not retry record_outreach manually. On a 412 Gmail not connected / Gmail token revoked, abort all email sending for this run and surface the message; the user must reconnect Gmail in the web app's Settings.
Notes:
- The body must be the complete content including the signature
- The
From: address and display name are pulled from project settings (senderEmailAlias / senderDisplayName) by the backend. If senderEmailAlias is set to a Send-As alias not yet verified in the user's Gmail account, sending fails with a Gmail error — surface it in the report and tell the user to verify the alias at https://mail.google.com → Settings → Accounts → "Send mail as"
3b. Re-approach Branching (cycle.kind != 'first')
When cycle.kind === 'first', use the normal first-touch composition above.
When cycle.kind === 'no_response':
- This is a follow-up after silence. Acknowledge the silence lightly
("circling back on my note from "), then lead with
what's new — a fresh signal from
## Recent Signals, a new product
release, a different angle in matchReason. If you genuinely have no
new material to add, skip the prospect: log via record_outreach
with status: "failed" and errorMessage: "skipped: no fresh material for re-approach (cycle n=<n>)". The server stamps
next_outreach_after so the prospect drops out of future runs until
the recycle window elapses. Quietly re-spamming the same pitch hurts
the long-term reply rate.
- Subject must differ from
cycle.lastOutreach.subject. Pick a
different SALES_STRATEGY pattern. Never use a generic "Re:" or
"Following up" alone — those are inboxes' top-of-spam triggers.
When cycle.kind === 'rejection_followup':
- The recipient previously responded substantively (rejection / reply /
bounce / meeting_request). Auto-replies do NOT set this kind — they
fall under
no_response, so a rejection_followup always has a real
prior reason to reference.
- Inspect
cycle.lastResponse.responseType:
'rejection' (or 'reply' with negative sentiment, surfaced as
'reply' here): recipient previously rejected with a recontact
window now elapsed (wrong_timing / budget). Open by referencing
the prior reason without quoting it verbatim ("When I reached out
in Q1 you mentioned timing wasn't right — checking back now…"),
then highlight what's specifically changed since (Phase 1.5
signals, product updates, pricing changes).
'meeting_request' / 'reply' (positive): unusual to be back in
the candidate pool — only happens if the recycle window elapsed
without a follow-up booking. Reference the earlier interest neutrally.
- If no concrete change has happened, skip — same logic as
no_response
(the server stamps the recycle window so future runs drop the prospect
until the window elapses).
- Tone is collegial, not pitchy. They opted in to "later" or never
responded substantively; do not punish that with hard sell.
4. Contact Form Submission
Read ${CLAUDE_PLUGIN_ROOT}/skills/outbound/references/claude-in-chrome-guide.md and ${CLAUDE_PLUGIN_ROOT}/skills/outbound/references/form-filling.md for the underlying procedures.
Compose the message body per the form's fields and the email guidelines (adapted to be concise).
Allocate the row + inquiry URL. Before opening the browser or doing any form inspection, call:
mcp__plugin_lead-ace_api__record_outreach_with_inquiry
projectId: "$0"
prospectId: <id>
channel: "form"
body: <composed body>
The server reads the project's outboundMode and either allocates the row as status: "pre_send" (send mode, in-flight reservation) or status: "pending_review" (draft mode). When inquiryLandingEnabled=true the response's finalBody carries the inquiry-landing URL footer appended to the body — submit it verbatim. The response is { outreachLogId, status, finalBody, inquiryUrl }.
If status === 'pending_review' (draft mode): stop here. The row is already stored for the user to review at https://app.leadace.ai/drafts. Do not open the browser, do not inspect the form, do not check formType. The user submits whatever form themselves; pre-screening (CAPTCHA / iframe / sales refusal notices) is irrelevant in draft mode.
If status === 'pre_send' (send mode): open the form page and run pre-submit screening before any form-fill. Branch on formType:
| formType | Processing |
|---|
google_forms | Follow "Google Forms" section in references/form-filling.md. Extract entry IDs via javascript_tool, then submit via formResponse POST with curl |
native_html / wordpress_cf7 / null | Use claude-in-chrome MCP tools (navigate, read_page / find, form_input, computer, read_network_requests). Follow basic flow in references/form-filling.md |
iframe_embed | Skip. Call update_outreach_status with status: "failed", errorMessage: "iframe-embedded form -- skipped" |
with_captcha | Skip. Call update_outreach_status with status: "failed", errorMessage: "captcha present". Follow "reCAPTCHA / hCaptcha etc." section in references/form-filling.md for the prospect-status handling |
Other pre-submit skip cases — sales refusal notice ("no sales inquiries", "営業お断り" etc.) or no form on the page — likewise resolve the pre_send row with update_outreach_status, status: "failed", and a concise errorMessage. Then continue with the case-specific prospect status update per references/form-filling.md (e.g. sales refusal → set prospect inactive).
If formType is null (not yet determined), inspect the page with read_page / find first. However, if null, skip immediately on first attempt failure (to prevent wasted tool calls in case it's iframe_embed or with_captcha) — call update_outreach_status with status: "failed" and errorMessage: "form not parseable".
Filling the form. Once screening passes, fill the form fields using finalBody as the message text (the inquiry URL is already in finalBody — submit it verbatim, do not strip or re-embed it), then submit per references/form-filling.md.
After submission verification. Always call mcp__plugin_lead-ace_api__update_outreach_status:
- On success →
status: "sent". The server flips the prospect to contacted and confirms quota consumption.
- On failure (HTTP 4xx/5xx, no POST observed, no thank-you state, etc.) →
status: "failed" plus a concise errorMessage. The in-flight quota reservation is refunded and the server stamps next_outreach_after to defer re-eligibility by noResponseRecycleDays (default 90 days). Do not retry the form.
5. SNS DM
Message: Keep it short and concise for SNS. Refer to the "SNS Messages" section of SALES_STRATEGY.md.
Determine the channel from the prospect's snsAccounts field — use sns_twitter for X / Twitter and sns_linkedin for LinkedIn. Compose the DM body per SALES_STRATEGY.md guidance.
Allocate the row + inquiry URL. Before opening the browser (in send mode) or anything else, call:
mcp__plugin_lead-ace_api__record_outreach_with_inquiry
projectId: "$0"
prospectId: <id>
channel: "sns_twitter" | "sns_linkedin"
body: <composed DM body>
The server returns { outreachLogId, status, finalBody, inquiryUrl }. finalBody already includes the inquiry-landing URL footer when the project has it enabled — submit it verbatim.
If status === 'pending_review' (draft mode): stop here. The DM is stored as a draft (with finalBody) for the user to send manually from https://app.leadace.ai/drafts. Do not open the browser or pre-check DM settings — the user finds out at send time and can mark the prospect inactive then.
If status === 'pre_send' (send mode): use claude-in-chrome to deliver the DM. See references/claude-in-chrome-guide.md for tool reference.
Common steps:
- Navigate to the SNS profile page in the browser.
- Open the DM / messaging UI.
- Type
finalBody and send.
For X (Twitter):
- Click the DM (message) icon from the profile page.
- If recipient's DM settings are closed, sending is not possible — call
update_outreach_status with outreachLogId, status: "failed" and errorMessage: "X DM closed", then call update_prospect_status with status: "inactive".
For LinkedIn:
- Click the "Message" button from the profile page.
- DMs can only be sent to connected users. If not connected, call
update_outreach_status with status: "failed" and errorMessage: "LinkedIn not connected", then set the prospect to inactive.
- Do not use InMail (paid feature).
After sending: the row is pre_send until you resolve it. Always call update_outreach_status:
- On success →
status: "sent".
- On failure (UI error, network failure, etc.) →
status: "failed" plus a concise errorMessage.
6. Handle Inactive Prospects
For prospects where approach failed due to a structural reason making future approaches impossible, call mcp__plugin_lead-ace_api__update_prospect_status with status: "inactive".
Cases where inactive should be set:
- Email address was invalid and bounced (permanent error)
- SNS DMs are not open
- Form was not suitable for B2B inquiries
- No available contact method at all
Cases where inactive should NOT be set (keep as new):
- Temporary network error or timeout
- System-side issues such as Gmail token revocation or quota exhaustion
7. Additional Outreach When Target Not Met
After all prospects are processed, if successes fall short of the target count:
- Shortfall = target count - successes (in draft mode, count
pending_review records as success — drafts are the intended outcome)
- Retrieve additional prospects: call
mcp__plugin_lead-ace_api__get_outbound_targets with limit: <shortfall>
- Repeat steps 2-6 for retrieved prospects
- Retry one round only. Also end retry if total reachable is 0
- Include final target achievement in the report (e.g., "Target 5, achieved 3 (ended due to depleted list)")
8. Results Report
Report the following:
- Number of prospects approached
- Attempts and successes per channel, success rate (Email: X successes/Y attempts (XX%), Form: X successes/Y attempts (XX%), SNS: X successes/Y attempts (XX%))
- If
outboundMode was draft, report total drafts created across all channels (Drafts: N) and remind the user to review and send them at https://app.leadace.ai/drafts
- Number of failures and reasons
- Guide the user to run
/check-results as the next step (or, if drafts were created, after the user sends the reviewed drafts)
- Append a single low-key dashboard line at the end:
Dashboard: https://app.leadace.ai/outreach — purely informational, do not push the user to open it