| name | outreach-composer |
| description | Writes calibrated outreach messages โ forwardable emails in the champion's voice, LinkedIn DMs with proof-of-work hooks, time-appropriate follow-ups, and thank-you notes with specific conversation callbacks. Respects contact channel preferences. Use this skill whenever the user wants to contact, message, email, DM, text, thank, reconnect with, or ask a favor of any named person โ even if they never say the word "outreach" (e.g. "can you ping Sarah about the role", "I should reply to Mike", "ask Priya for an intro"). Say "write outreach for [Contact]" or "follow up with [Contact]".
|
| triggers | ["write outreach for","oc","draft message to","follow up with","thank you note for","reach out to","message for","draft email to"] |
Outreach Composer โ Career OS Skill
Task Substrate (v0.25.0+)
$CAREER_GITHUB_REPO is derived from: git -C $CAREER_HOME remote get-url origin | sed 's/.*github.com[:/]//;s/.git$//'
Tasks live in $CAREER_GITHUB_REPO GitHub Issues (canonical source of truth โ single inbox for all Cyborg work). Repo of work indicated by repo:* label, NOT by issue location. Cadence indicated by cadence:* label (operational for high-frequency churn; strategic for sprint-scale; meta for trackers). Tier indicated by tier:* label (p1/p2/p3/backlog).
Tasks.md is DEPRECATED as of v0.25.0. See $CAREER_HOME/workspace.manifest.yaml task_routing: section for the full architecture.
This skill reads/writes via:
gh CLI (universal, all agents)
github-mcp MCP server (post-restart, when MCP boots โ at npx @modelcontextprotocol/server-github)
Outreach follow-up nudges open as kind:follow-up issues:
gh issue create --repo $CAREER_GITHUB_REPO \
--title "Follow up with {Contact} re: {Company} โ by {due_date}" \
--label "tier:p3,cadence:operational,repo:career-os-data,kind:follow-up" \
--body "<context summary + suggested approach + last_contact date>"
Outreach drafts are written via brain.write("career-intelligence/tasks/outreach-{contact}-{date}.md", ...) (drafts are working artifacts in tasks/**, which is in career-intelligence owned_paths). Only the scheduled follow-up nudge is a task issue.
Purpose
Generates personalized outreach messages calibrated to the relationship, channel, and context. Applies networking psychology to maximize response rates without being pushy:
- Invisible board principle โ people quietly evaluate whether helping you makes them look good to THEIR network. Position the ask so saying yes is low-risk and reflects well on them.
- Serotonin from helping โ asking for advice triggers the advisor's reward circuit; asking for a favor triggers cost-accounting. Frame asks as advice whenever the relationship allows.
- Proof-of-work hooks โ a concrete artifact (article, repo, demo) converts "stranger asking for time" into "builder worth meeting." Every cold-ish message carries one.
Two non-negotiable protocols (apply to EVERY behavior below)
- Draft โ Show โ Wait. Every message is presented to the user and explicitly approved before any send. A blanket "yes, send messages to X and Y" approves ZERO sends โ each message needs its own approval in its own turn. Showing a draft is NOT sending; never record
message_sent/last_contact from a draft. Only a confirmed send (user says sent, or user approves an automated send) updates the people file.
- Plain text for copy-paste output. Anything the user will paste into WhatsApp, Gmail, LinkedIn, or iMessage must be plain prose: no markdown tables, no pipe characters, no
**bold**, no backticks, no code fences. Pipes and asterisks paste as literal junk characters and make the sender look careless. If a structured reference view helps, put the plain-text message FIRST and alone, then a clearly-labeled "for reference" section.
Output Format
Always start with:
โโโ Career OS: Outreach Composer โโโ
How to Invoke
write outreach for [Contact] to [Company] โ forwardable referral email
linkedin message to [Contact] โ DM with proof-of-work hook
follow up with [Contact] โ time-calibrated follow-up
thank you note for [Contact] โ post-interaction note with conversation callback
DATA ARCHITECTURE
Inputs
| Source | Path | What It Provides |
|---|
| Target contact | User request (name or company) | Who to write to |
| Knowledge context | python3 $CAREER_HOME/brain/assembler.py "<contact>" | WHY-relative context: goal alignment, typed edges, ranked related nodes |
| Contact profiles | network/people/*.md | Relationship warmth, shared history, channel preference |
| Pipeline | career-intelligence/projects/job-search/job-pipeline.json | Company context, role, hiring manager, stage |
| Stories | brain/stories/*.md | Shared history with contact |
| Outreach patterns | brain/projects/outreach-patterns.md (if exists) | Proven templates (Pattern D, etc.) |
| Key assets | CLAUDE.md Key Assets section | LinkedIn article, GitHub, demo URLs |
| Glossary | brain/identity/glossary.md | Contact quick-reference |
Brain API (brain-kernel >= 1.0.0)
All writes go through brain.write(). Contact profiles at network/people/** are
xOS primitives โ permitted via writes_to_primitives declaration.
Outputs
| Output | brain.write() path | When Created |
|---|
| Message draft | career-intelligence/projects/outreach-{contact}-{date}.md | Every outreach |
| New contact profile | network/people/{slug}.md | When new contact ingested |
Local Artifact Event
After any successful message-draft write, emit the local-only beta-funnel
artifact signal. The helper no-ops unless XOS_98_TELEMETRY is enabled:
bun "$CLAUDE_PLUGIN_ROOT/src/telemetry/beta-funnel.ts" artifact-created \
'{"artifact_type":"outreach"}'
STEP 0: Dedup Pre-Flight (MANDATORY โ fires before every trigger, no exceptions)
Before doing anything else โ before channel selection, before drafting, before asking for missing info โ run the warm-contact-outreach-dedup check:
python3 "$(ls -v ~/.claude/plugins/cache/xos/career-intelligence/*/rules/warm-contact-outreach-dedup/HOW.py 2>/dev/null | tail -1)" \
'{"contact_name": "<CONTACT_NAME>"}'
Parse the result:
verdict: PASS โ proceed to Channel Selection
verdict: BLOCK โ STOP. Do NOT draft. Surface the block message verbatim and halt:
โ DEDUP BLOCK: {reason}
Last contact: {last_contact} ({days_since_outreach} days ago)
Check for a reply instead of re-contacting.
Skip-rule: NONE. Every trigger (write outreach for, follow up with, linkedin message to, thank you note for, reach out to, message for, draft email to) must invoke this check. The question "has outreach already gone out?" must be answered by data, not memory.
Origin (2026-05-04): Two back-to-back failures โ drafted WhatsApp for a contact (Amanesh) who had already been reached via LinkedIn + phone + email; drafted follow-up for a contact (Ravi) whose people file showed same-day outreach. Neither would have happened if this gate had run. User: "i thought you codified this in the plugin and shipped."
STEP 0.5: Knowledge Layer Context (runs after dedup PASS)
Load WHY-relative context before drafting. This surfaces the contact's typed graph edges, active goal alignment, and related context โ so the message angle is calibrated to why this contact matters right now, not just who they are.
python3 "$CAREER_HOME/brain/assembler.py" "<CONTACT_NAME>"
Read the output bundle. Key signals to extract:
- Which active goals this contact maps to (from WHY.md)
- Their typed edges โ
works_at, involved_in, championing โ tells you the right proof-of-work angle
- Related projects and companies surfaced by edge traversal
Calibration rules:
- Contact with
championing โ projects/interview-prep โ lead with that project progress, not a generic ask
- Contact with
works_at โ companies/ai-fund โ frame against xOS mission, not job search
- Contact with
involved_in on a blocked project โ acknowledge the blocker, offer to help
If brain/assembler.py not found: skip this step and proceed. The assembler is context enrichment, not a blocker โ outreach-composer works without it.
CHANNEL SELECTION (ALWAYS CHECK FIRST)
Before composing any message, read the contact profile's channel: field.
| Channel | Tone | Constraints |
|---|
| WhatsApp | Casual, short, personal | Brief paragraphs, emoji OK |
| LinkedIn | Professional, proof-of-work hook | Connection request: <300 chars. DM: <500 chars |
| Email | Structured, forwardable | Subject line that works as-is |
| Text | Very brief, friends-only | 1-2 sentences max |
If no channel specified in profile: ask the user before composing. Never guess.
BEHAVIOR: Forwardable Email (write outreach for [Contact] to [Company])
This is the highest-value outreach pattern. The goal: your champion can forward your message to their contact at the target company in 10 seconds flat.
Context Pre-Flight (MANDATORY FIRST CHECK)
Before doing anything else, verify required context files exist:
EXPERIENCE_HISTORY="$CAREER_HOME/brain/identity/experience-history.md"
if [ ! -f "$EXPERIENCE_HISTORY" ]; then
echo "โ experience-history.md not found. Run 'onboard me to career intelligence' first โ this file is required to verify biographical claims before any outreach is sent."
exit 1
fi
If experience-history.md is missing: STOP and print the error above. Do not draft. Outreach without canonical biography grounding is the failure mode that created fabricated credentials in past drafts.
Pre-Flight Check (Outreach Friction Rule)
Before composing, verify you have:
- Exact role name โ from pipeline or user
- Hiring manager name โ from pipeline or user
- Contact's relationship context โ from people profile
If ANY are missing, flag it:
Before I compose, I need:
โ ๏ธ Missing hiring manager name for {Company}
โ ๏ธ No profile for {Contact} โ how do you know them?
Fill these in and I'll draft the email.
Canonical-Claim Verification (MANDATORY โ do not skip)
This skill drafts T4 outreach: messages that land in real humans' inboxes. Biographical hallucinations here destroy warm paths and credibility.
Before WRITING the draft to disk (the final step), run the biographical-claim pre-check rule against your draft:
GATE=$(ls -v ~/.claude/plugins/cache/xos/career-intelligence/*/rules/biographical-claim-precheck/HOW.py 2>/dev/null | tail -1)
if [ -z "$GATE" ]; then
echo '{"verdict":"BLOCK","reason":"biographical-claim-precheck script not found","remediation":"Run: claude plugin update career-intelligence@xos --scope user"}'
exit 1
fi
python3 "$GATE" "$(jq -nc \
--arg draft "/path/to/in-progress-draft.md" \
--arg canonical "$CAREER_HOME/brain/identity/experience-history.md" \
'{draft_path:$draft, canonical_sources:[$canonical], stakes:"T4"}')"
The script returns JSON:
verdict: PASS โ ship the draft
verdict: BLOCK โ at least one biographical claim (tenure, role, report count, scale-figure, date range) is NOT anchored in canonical. Read each entry in claims_unanchored[], fix or remove the claim, re-run.
Origin (2026-04-26): [Recipient] email and [Connection] LinkedIn DM both shipped past prose verification with multiple unanchored biographical claims (employer-tenure, report-counts, product-attribution, platform-project-attribution). User caught both pre-send. The pre-write hook is the mechanical gate that prose alone failed to enforce.
Skip-rule: NONE. Even casual peer-to-peer DMs with biographical claims must run the check. The [Connection] DM was casual and still produced credibility-killing hallucination.
Compose
- Read contact profile โ relationship warmth, shared history, communication style
- Read pipeline entry for target company โ role, HM name, stage
- Generate email in the FORWARDER's voice (not the user's):
- Subject line the forwarder would naturally write
- Body the forwarder can paste into email/Slack as-is
- References verified shared context only โ never fabricated
- Positions user as someone worth meeting (proof-of-work, not begging)
- Includes forwardable blurb: who the user is, why they're relevant, 2-3 proof points
โโโ Forwardable Email Draft โโโ
To: {Contact} โ forwards to {HM Name} at {Company}
Channel: Email
Subject: {subject line in forwarder's voice}
---
{Email body โ written as if {Contact} is sending it}
---
Forwardable blurb (copy-paste for Slack/email):
"{User} is {1-line positioning}. {Proof point 1}. {Proof point 2}.
They're interested in the {Role} role โ worth a conversation."
Includes: role name โ
| HM name โ
| forwardable blurb โ
- Run Canonical-Claim Verification (see section above) against the in-progress draft. BLOCK on any unanchored claim. Do NOT proceed to step 5 until verdict is PASS.
- Write draft via
brain.write("career-intelligence/projects/outreach-{contact}-{date}.md", ...).
- In the agent's response to the user, include a one-line evidence note:
Canonical-claim verification: PASS (N claims, all anchored).
BEHAVIOR: LinkedIn DM (linkedin message to [Contact])
- Read contact profile
- Generate message with proof-of-work hook:
- Reference something specific: their post, shared connection, company news, mutual project
- Ask for advice, not a favor (invisible board principle: people want to feel like advisors, not gatekeepers)
- Include proof-of-work: LinkedIn article link, GitHub, specific project reference
- Enforce character limits:
- Connection request: < 300 characters
- InMail/DM: < 500 characters
โโโ LinkedIn DM Draft โโโ
To: {Contact}
Type: {Connection request / DM}
Characters: {N}/{limit}
---
{Message text}
---
Proof-of-work: LinkedIn article โ
- Run Canonical-Claim Verification (see "Canonical-Claim Verification" section under "Forwardable Email" above) against the in-progress draft. BLOCK on any unanchored claim. The [Connection] LinkedIn DM (2026-04-26) was the originating incident โ casual peer-to-peer DM that still produced credibility-killing hallucination. No skip-rule for casual register.
- Write draft via
brain.write("career-intelligence/projects/outreach-{contact}-{date}.md", ...).
- Include in agent response:
Canonical-claim verification: PASS (N claims).
BEHAVIOR: Follow-Up (follow up with [Contact])
- Read outreach history from contact profile and git log
- Calculate time since last contact
- Generate time-calibrated follow-up:
| Time Since Last | Approach | Example |
|---|
| < 1 week | Too soon โ advise waiting | "Last message was 3 days ago. Give it at least a week โ following up sooner signals desperation." |
| 1-2 weeks | Gentle nudge with new info | Share a relevant article, mention a new development, or add context |
| 2-4 weeks | Re-engage with different angle | New value-add, different shared context, or reference a recent event |
| > 4 weeks | Fresh approach โ reset | Different channel, different topic, or acknowledge the gap |
โโโ Follow-Up Draft โโโ
To: {Contact}
Last contact: {date} ({N} days ago)
Strategy: {approach from table above}
Channel: {from contact profile}
---
{Follow-up message}
---
- Run Canonical-Claim Verification if the follow-up references any new biographical claim about the user not present in the prior outreach. (Pure-recall follow-ups that only reference previous conversation can skip โ but ANY new claim must verify.)
- Write draft via
brain.write("career-intelligence/projects/outreach-{contact}-{date}.md", ...).
BEHAVIOR: Thank-You Note (thank you note for [Contact])
- Read recent interaction context from:
- Interview debrief (
brain/interview-prep/)
- Ledger entries mentioning this contact
- Contact profile notes
- Generate note with:
- Specific callback to something discussed โ never generic "thanks for your time"
- Subtle forward momentum โ next step, shared resource, follow-up question
- Match channel and tone to relationship
โโโ Thank-You Draft โโโ
To: {Contact}
Context: {what interaction this follows}
Channel: {from profile}
---
{Thank-you message with specific callback}
---
- Write draft via
brain.write("career-intelligence/projects/outreach-{contact}-{date}.md", ...)
BEHAVIOR: No Contact Profile
If no network/people/{name}.md exists:
โโโ Career OS: Outreach Composer โโโ
I don't have a profile for {Name} yet. To write effective outreach, I need:
1. How do you know them? (worked together, social, mutual friend)
2. What company are they at?
3. Preferred contact channel? (LinkedIn, email, WhatsApp, text)
Tell me about them โ I'll save their profile and compose the message.
After user describes the contact:
- Extract structured data โ write via
brain.write("network/people/{slug}.md", ...) (primitive write, engine_id: "career-intelligence")
- Proceed with the original outreach request
SSOT Write Rules (ADR-001)
- After composing outreach, write status ONLY to
people/<contact>.json (or .md if not yet migrated)
- Update: last_contact date, outreach status, channel used, message summary
- Do NOT write outreach status to job-pipeline.json, task issues, or handoff docs (the
kind:follow-up issue tracks the SCHEDULED NUDGE only โ not the outreach status itself)
- Other skills (Mission Control, pipeline-sync) read from people/*.json
UNIT-OF-WORK COMMIT (MANDATORY โ same execution turn as confirmed send)
A draft shown is NOT a message sent (see protocol 1 above). This section fires ONLY on confirmed send. When the user confirms a message was sent (or you send it via browser automation after explicit per-message approval), immediately call:
python3 $(ls -v ~/.claude/plugins/cache/xos/career-intelligence/*/rules/outreach-people-file-commit/HOW.py 2>/dev/null | tail -1) "$(jq -nc \
--arg people_file "$CAREER_HOME/network/people/{slug}.json" \
--arg career_home "$CAREER_HOME" \
--arg last_contact "$(date +%Y-%m-%d)" \
--arg follow_up "{follow_up_date}" \
--arg summary "{one-line message summary}" \
--arg commit_msg "outreach: {slug} โ {company} {role} [{action}] [unit-of-work]" \
'{people_file:$people_file,career_home:$career_home,updates:{last_contact:$last_contact,follow_up:$follow_up,conversation_history:{last_message_sent:$last_contact,last_message_summary:$summary}},commit_message:$commit_msg}')"
Rules:
- Call this in the SAME turn as the send confirmation โ NEVER defer to session end
- If the user kills the session after send but before this script runs, state is lost
- Exit 0 = committed (show SHA). Exit 1 = BLOCK (surface error, do not proceed). Exit 2 = WARN (committed with issues)
- Slug = the people file basename without extension (e.g.,
sandeep-reddy)
- follow_up_date = today + 3 days for LinkedIn DM, today + 5 days for email (unless user specifies)
Dependencies
organize โ contact profiles in network/people/ (required for warm outreach)
- Pipeline entry โ helpful for company context (recommended)
network-intelligence โ identifies paths before outreach is composed (optional)
- Key assets in CLAUDE.md โ LinkedIn article, GitHub links for proof-of-work