Enterprise Prospecting & Outreach Orchestrator
The capstone integrates the Claude API, the Claude Agent SDK, and Claude Code into a single production system. You will build a coordinator agent that decomposes prospecting requests into parallel research subagents spawned with the Task tool, connects to CRM data through MCP, ships outreach at scale through Message Batches, and stays reliable under long-running sessions with deterministic hooks, case facts, and an independent CI review pipeline.
Project Overview
Self-driven lab Module12_Self_Driven_Lab.ipynbYou are building the Enterprise Prospecting & Outreach Orchestrator: a system that identifies high-value prospects, performs deep research using a multi-agent hub-and-spoke model, generates high-volume outreach campaigns, and maintains strict reliability through deterministic hooks, re-injected case facts, and crash-recoverable state. Every concept from Modules 1 through 11 lands in this single project.
Phase 1: Coordinator & Agent Definitions
Define the coordinator with the Claude Agent SDK so the runtime owns history, tool dispatch, and termination. The coordinator delegates through the Task tool; each research specialist is an AgentDefinition with its own description, prompt, tool allowlist, and model.
- Coordinator Tools: the coordinator's
allowed_toolsmust include"Task"; without it, the coordinator cannot spawn subagents at all. - AgentDefinition Fields: each subagent declares a
description(when the coordinator should pick it), aprompt(its system prompt),tools(its allowlist), and amodel; use lighter models for narrow research tasks. - Research Capability: give research subagents a client-side
web_lookupcustom tool (your application executes the search and returns results to the agent) or a tool exposed by an MCP server. The application, not the model provider, owns search execution and rate limits. - Interview Pattern: before planning, the coordinator must ask 2-3 clarifying questions about outreach goals, such as tone constraints, competitor sensitivity, required technical depth, and any claims that require human approval.
from claude_agent_sdk import AgentDefinition, ClaudeAgentOptions
options = ClaudeAgentOptions(
system_prompt=(
"You are the lead orchestrator. Use the Task tool to delegate to "
"specialist subagents. Always maintain a 'case facts' block with "
"non-negotiable transactional data. Before generating the research "
"plan, ask 2-3 concise clarifying questions about outreach goals, "
"including tone, competitor sensitivity, and technical depth."
),
# The coordinator MUST have "Task" to spawn subagents.
allowed_tools=["Task", "Read", "Grep", "mcp__crm__crm_query"],
agents={
"web-research": AgentDefinition(
description=(
"Researches a prospect's public footprint. Use for market "
"signals, news, and technology-adoption research."
),
prompt=(
"Research the assigned prospect with the web_lookup tool. "
"Return findings with sources; never speculate."
),
tools=["mcp__research__web_lookup"],
model="sonnet",
),
"financial-analysis": AgentDefinition(
description="Analyzes prospect financials from CRM data.",
prompt=(
"Analyze revenue, budget, and spend signals from CRM records. "
"Return a structured summary with nullable fields for unknowns."
),
tools=["mcp__crm__crm_query"],
model="sonnet",
),
"competitor-tracking": AgentDefinition(
description="Tracks competitor positioning in a prospect's market.",
prompt=(
"Identify competitors and positioning gaps for the assigned "
"market segment. Return findings with sources."
),
tools=["mcp__research__web_lookup"],
model="haiku",
),
},
)
Phase 2: Knowledge Mapping with MCP & Resources
Connect the CRM to the agent through an MCP server declared in the project's .mcp.json. Expose Resources as a navigable map of the data so Claude can plan its tool sequence before executing.
- Project Config:
.mcp.jsonwith environment variable substitution (${CRM_TOKEN}) keeps secrets out of version control. - CRM Resource Hierarchy: expose
crm://docs,crm://schemas, andcrm://workflowsresources so Claude can inspect the CRM's documentation hierarchy before calling query or write tools. - Selection Reliability: tool descriptions must explain why CRM MCP tools are preferred over built-in text tools such as
Grepfor structured data lookups. - Discovery Tax Reduction: Resources act as catalogs. The agent should inspect
crm://schemasandcrm://workflowsbefore exploratory CRM queries instead of discovering tables through repeated tool calls.
{
"mcpServers": {
"crm": {
"command": "node",
"args": ["./mcp-servers/crm-server.js"],
"env": {
"CRM_TOKEN": "${CRM_TOKEN}",
"CRM_BASE_URL": "${CRM_BASE_URL}"
}
}
}
}
{
"resources": [
{
"uri": "crm://docs/prospecting",
"name": "CRM prospecting documentation",
"description": "Use before lead searches. Explains lead stages, required fields, and routing rules.",
"mimeType": "text/markdown"
},
{
"uri": "crm://schemas/leads",
"name": "Leads schema",
"description": "Use before CRM lead lookups. Lists column names, allowed enum values, and join keys so the agent chooses crm_query_leads instead of built-in Grep for structured account data.",
"mimeType": "application/json"
},
{
"uri": "crm://workflows/outreach-approval",
"name": "Outreach approval workflow",
"description": "Approval gates required before process_outreach can run.",
"mimeType": "text/markdown"
}
]
}
Phase 3: Multi-Agent Research Workflow
Hub-and-Spoke architecture: the coordinator decomposes a prospecting request and spawns specialist subagents in parallel inside a single turn.
- Parallel Execution: the coordinator emits multiple
Tasktool calls in one response (one per subagent) so Web Research, Financial Analysis, and Competitor Tracking run concurrently. - Isolated Context: subagents do not inherit the coordinator's history. Everything a subagent needs must be passed explicitly in its Task prompt, and findings return to the coordinator only through the Task result.
- Structured Errors: subagents return
errorCategoryandisRetryableso the coordinator can recover intelligently rather than failing the workflow.
import anyio
from claude_agent_sdk import ClaudeSDKClient
async def run_research():
# `options` is the ClaudeAgentOptions from Phase 1 (Task + AgentDefinitions)
async with ClaudeSDKClient(options=options) as coordinator:
await coordinator.query(
"Research AI adoption in Fintech and Healthcare simultaneously. "
"Delegate to the web-research, financial-analysis, and "
"competitor-tracking subagents — issue one Task call per subagent, "
"all in the same response, so they run in parallel. Each subagent "
"starts with an empty context: include the prospect summary and "
"the specific question in each Task prompt."
)
async for message in coordinator.receive_response():
print(message)
anyio.run(run_research)
Exam tip: Task subagents run in isolated context windows. Nothing is shared implicitly: the coordinator forwards inputs in the Task prompt, and the subagent's findings come back as the Task tool result, which the coordinator then synthesizes.
def subagent_tool_handler(tool_input):
try:
return {"data": run_research(tool_input)}
except TimeoutError:
return {
"errorCategory": "TRANSIENT",
"failure_type": "timeout",
"isRetryable": True,
"message": "Research API timed out. Retry with a narrower query."
}
except PermissionError:
return {
"errorCategory": "PERMISSION",
"isRetryable": False,
"message": "Token lacks access. Coordinator should pivot, not retry."
}
Phase 4: Synthesis & Scaled Outreach
Synthesize research findings, then ship outreach at volume.
- Message Batches: 50% discount on bulk outreach. Each request needs a unique
custom_id; results return out of order. - Single-Shot Only: batch requests do not support multi-turn tool calling; there is nobody to execute a tool and send the result back mid-batch. Do all research first with the interactive agent, then submit plain, self-contained generation requests.
- Structured Extraction: JSON-schema outputs with nullable fields for
annual_revenue,budget_range, etc., to prevent hallucination when data is missing. - Position-Aware Input Ordering: the synthesis prompt must put
key_findingsand decision-critical summaries first, then detailed evidence under explicit prospect and source-type section headers. - Extensible Categories: prospect schemas must use an enum with
otherplus a required detail field for extensible fields such asindustryorreason_for_outreach.
aggregated_research = {
"key_findings": [
"Fintech CTOs show high interest in agentic workflow governance.",
"Healthcare prospects require stricter compliance language and human review."
],
"sections": {
"prospect_startup_segment": startup_evidence,
"prospect_fortune_500_segment": enterprise_evidence,
"source_crm_notes": crm_notes,
"source_web_research": cited_web_findings,
},
}
# Plain single-shot generation requests: research is already done, so each
# request carries everything the model needs. No tools inside the batch.
message_batch = client.messages.batches.create(
requests=[
{
"custom_id": f"outreach-{prospect['crm_id']}", # unique per prospect
"params": {
"model": "claude-sonnet-4-6",
"max_tokens": 2048,
"messages": [{
"role": "user",
"content": (
"Draft a personalized outreach email.\n"
f"Prospect brief: {prospect['brief']}\n"
f"Key findings: {prospect['key_findings']}\n"
"Follow the standard outreach template rules."
),
}],
},
}
for prospect in qualified_prospects
]
)
# Later: correlate by custom_id — results arrive in any order.
results = {}
for result in client.messages.batches.results(message_batch.id):
results[result.custom_id] = result.result
# Resubmit ONLY the failed/expired requests, never the whole batch.
retry_ids = [cid for cid, r in results.items() if r.type in ("errored", "expired")]
Exam tip: Four batch rules the exam loves: (1) no multi-turn tool calling inside a batch: every request must be a self-contained single shot; (2) batches have a 24-hour processing window, and requests still unfinished at the deadline expire; (3) results are unordered, so always correlate by custom_id, never by position; (4) on partial failure, resubmit only the failed or expired custom_ids.
For CRM-ready extraction, structured outputs work inside batch requests too; the schema below uses nullable fields and enum + other + detail-field pairs:
output_config = {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"company_name": {"type": "string"},
"contact_email": {"type": "string"},
"annual_revenue": {"type": ["integer", "null"]},
"budget_range": {"type": ["string", "null"]},
"industry": {
"type": "string",
"enum": ["fintech", "healthcare", "manufacturing", "retail", "other"]
},
"industry_detail": {"type": ["string", "null"]},
"reason_for_outreach": {
"type": "string",
"enum": ["ai_governance", "workflow_automation", "security_review", "other"]
},
"reason_detail": {"type": ["string", "null"]}
},
"required": [
"company_name", "contact_email", "annual_revenue", "budget_range",
"industry", "industry_detail", "reason_for_outreach", "reason_detail"
],
"additionalProperties": False
}
}
}
Phase 5: Production Reliability & Context Hygiene
Long-running research sessions need active context management and deterministic safety rails.
- Deterministic Hooks: a
PreToolUsehook blocks anyprocess_outreachcall if the prospect'srisk_scorehas not been verified. A hook can only deny with a reason; it cannot redirect the call to a different tool. The denial reason is returned to the model, and the model then callsescalate_to_humanitself. - Case Facts Pattern: non-negotiable transactional data ("Max Budget: $50k") lives in a case-facts block that the orchestration code re-injects verbatim on every turn, so drift and summarization can never paraphrase it away.
- State Manifest: each agent exports a scratchpad/state manifest (completed subtasks, pending prospects, verified facts) to disk after every phase, so a crashed run can be resumed without re-doing finished research.
- Fresh Session with Summary: when a long session's context degrades (repetition, forgotten constraints, contradictions), stop patching it. Start a fresh session seeded with the case-facts block plus a concise summary of verified findings from the state manifest.
- Human-in-the-Loop: if a prospect requests "no AI contact" or a policy gap is detected, the agent must call
escalate_to_humanimmediately.
CASE_FACTS = (
"<case_facts>\n"
"Max Budget: $50,000\n"
"Contract term: 12 months\n"
"Prospect opt-outs: no phone contact\n"
"</case_facts>"
)
def build_turn(user_text: str) -> dict:
# Re-inject case facts verbatim on EVERY turn so they can never
# be paraphrased away as the conversation grows.
return {
"role": "user",
"content": f"{CASE_FACTS}\n\n{user_text}",
}
def export_state_manifest(path: str, state: dict) -> None:
"""Each agent writes its progress after every phase for crash recovery."""
import json
with open(path, "w") as f:
json.dump({
"completed_subtasks": state["done"],
"pending_prospects": state["pending"],
"verified_facts": state["facts"],
}, f, indent=2)
{
"hooks": {
"PreToolUse": [
{
"matcher": {"tool_name": "process_outreach"},
"hooks": [
{
"type": "command",
"command": "python verify_risk_score.py"
}
]
}
]
}
}
Phase 6: Claude Code & Skills
Manage the project's development standards through Claude Code.
- Path-Scoped Rules: a rule file in
.claude/rules/with apathsfrontmatter key enforces the standard outreach template only when Claude is editing files underoutreach/. - Agent Skills: a
generate-auditskill withcontext: forkperforms deep compliance checks on generated emails without polluting the main development context. - Three-Level Progressive Disclosure: the skill must keep trigger logic in frontmatter, actionable workflow steps in
SKILL.md, and detailed compliance material inreferences/so the agent only loads extra context when needed. - CI Review Output: use
claude -pwith--output-format jsonand--json-schemaso automated review findings can be posted as inline PR comments. - CI Dedupe: when re-running the review after new commits land on the same PR, include the prior findings in the prompt and instruct Claude to report only new or unaddressed issues; otherwise every push re-posts the same comments.
---
paths: ["outreach/**/*.md", "outreach/**/*.html"]
---
# Standard Outreach Template Rules
- Open with the prospect's confirmed pain point from CRM, never a generic hook.
- Include one quantified case study from the same industry vertical.
- Close with a single, specific call to action (no menu of options).
- Never reference compliance regimes the prospect has not opted into.
---
name: generate-audit
description: Audit generated outreach emails when asked to review campaign copy, regulated claims, unsupported personalization, or compliance-sensitive prospect messaging.
context: fork
allowed-tools: [Read, Grep, Glob]
---
# Audit Skill
1. Read the email under review.
2. Cross-check claims against the prospect's stated regulated regimes.
3. Open `references/compliance-claims.md` only when the email mentions HIPAA, SOC 2, ISO 27001, privacy, safety, or procurement claims.
4. Flag unsupported claims, weak personalization, policy conflicts, and missing escalation triggers.
5. Return a structured JSON report with findings, severity, evidence, and recommended next action.
# First run on a PR
claude -p "Review this capstone PR and return inline findings." \
--output-format json \
--json-schema .claude/schemas/pr-review-findings.schema.json
# Re-run after new commits: pass prior findings and dedupe
claude -p "Review this PR again. Prior findings: $(cat prior-findings.json). \
Report ONLY issues that are new or still unaddressed — do not repeat \
findings that were already posted or have been fixed." \
--output-format json \
--json-schema .claude/schemas/pr-review-findings.schema.json
Phase 7: Evaluation & Success Metrics
Evaluate the orchestrator as a product, not only as a demo.
- Quantitative: skill trigger reliability reaches at least 90% of relevant queries, the workflow uses fewer tool calls than a baseline, the coordinator salvages
partial_resultsrather than re-running failed searches, and 10 high-confidence extractions are manually sampled across prospect segments and source types to catch segment-specific failures. - Qualitative: a new user can complete the task on the first try with minimal guidance, escalation summaries are actionable, PR review findings are specific enough to become inline comments, and a completely independent Claude instance in a fresh session catches unsupported claims or weak outreach that the generator missed.
- Independent Review: start a fresh Claude session with no prior reasoning context. Provide only the final outreach, source evidence, and review rubric. The reviewer must catch unsupported claims, policy issues, weak personalization, and missing escalations that the generator may have rationalized.
- Stratified Random Sampling: sample extractions across prospect segments and source types rather than relying on aggregate accuracy. Include at least one startup, one mid-market account, one Fortune 500 account, one CRM-sourced record, and one web-researched record.
Final Audit Checklist
- Tool descriptions: every tool description states its inputs and its boundaries: what the tool does, what it must never be used for, and when to prefer it over neighboring tools.
- Subagent failures: every subagent failure returns structured context (
errorCategory,isRetryable, a human-readable message) with partial results, so the coordinator can salvage completed work instead of re-running the whole task. - Batch discipline: batch jobs are single-shot generation requests with unique
custom_ids: no multi-turn tool calling, results correlated bycustom_id, only failures resubmitted. - Escalation triggers: escalation covers all three cases: an explicit human request, a detected policy gap, and no measurable progress after repeated attempts.
- Handoff payload: every
escalate_to_humanhandoff stands alone: a human can act on the summary, case facts, and pending decision without reading the transcript. - Review pipeline: the review runs per-file passes plus a cross-file integration pass, executed by an independent Claude instance in a fresh session.
- Independent review: final outreach must be audited by a fresh Claude session, not the generation session, so the reviewer is isolated from the generator's assumptions.
- Subagents: isolated context by default. The coordinator must explicitly forward only the data each subagent needs.