Module 12

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.

Answer key Module12_Complete.ipynb

Project Overview

Self-driven lab Module12_Self_Driven_Lab.ipynb

You 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_tools must include "Task"; without it, the coordinator cannot spawn subagents at all.
  • AgentDefinition Fields: each subagent declares a description (when the coordinator should pick it), a prompt (its system prompt), tools (its allowlist), and a model; use lighter models for narrow research tasks.
  • Research Capability: give research subagents a client-side web_lookup custom 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.
Python
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.json with environment variable substitution (${CRM_TOKEN}) keeps secrets out of version control.
  • CRM Resource Hierarchy: expose crm://docs, crm://schemas, and crm://workflows resources 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 Grep for structured data lookups.
  • Discovery Tax Reduction: Resources act as catalogs. The agent should inspect crm://schemas and crm://workflows before exploratory CRM queries instead of discovering tables through repeated tool calls.
JSON (.mcp.json)
{
  "mcpServers": {
    "crm": {
      "command": "node",
      "args": ["./mcp-servers/crm-server.js"],
      "env": {
        "CRM_TOKEN": "${CRM_TOKEN}",
        "CRM_BASE_URL": "${CRM_BASE_URL}"
      }
    }
  }
}
JSON (CRM resources/list response)
{
  "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 Task tool 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 errorCategory and isRetryable so the coordinator can recover intelligently rather than failing the workflow.
Python
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.

Python (subagent error handler)
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_findings and decision-critical summaries first, then detailed evidence under explicit prospect and source-type section headers.
  • Extensible Categories: prospect schemas must use an enum with other plus a required detail field for extensible fields such as industry or reason_for_outreach.
Python
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,
    },
}
Python
# 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:

Python (structured extraction schema)
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 PreToolUse hook blocks any process_outreach call if the prospect's risk_score has 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 calls escalate_to_human itself.
  • 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_human immediately.
Python (case facts re-injection)
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)
JSON (PreToolUse hook)
{
  "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 a paths frontmatter key enforces the standard outreach template only when Claude is editing files under outreach/.
  • Agent Skills: a generate-audit skill with context: fork performs 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 in references/ so the agent only loads extra context when needed.
  • CI Review Output: use claude -p with --output-format json and --json-schema so 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.
Markdown (.claude/rules/outreach-template.md)
---
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.
YAML (.claude/skills/generate-audit/SKILL.md)
---
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.
CI review command
# 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_results rather 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

Architect Tip for the Exam
  • 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 by custom_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_human handoff 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.