The Problem
On Monday you tested the 3 prompts in ChatGPT. Cool! You saw how discovery → outreach → tracking works. But here's reality: you can't manage 50 active partnerships in a spreadsheet. You lose context. Forget follow-ups. Miss deal signals. Your VP asks 'where are we with Acme Corp?' and you're digging through emails.
See It Work
Watch the 3 prompts chain together automatically. This is what you'll build.
The Code
Three levels: start simple, add reliability, then scale to production. Pick where you are.
Level 1: Simple API Calls
Good for: 0-50 active deals | Setup time: 30 minutes
// Simple API Calls (0-50 active deals) import Anthropic from '@anthropic-ai/sdk'; import axios from 'axios'; interface PartnershipCriteria { industry: string; employeeRange: string; fundingStage: string[]; geography: string; technicalReqs: string[]; } interface DealFlow { discovery: any; outreach: any; tracking: any; } async function automatePartnershipPipeline( criteria: string ): Promise<DealFlow> { const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, }); // Step 1: Discover and score potential partners const discoveryPrompt = `Extract partnership search criteria and identify top 3 potential partners. Criteria: ${criteria} Output as JSON with: - search_criteria (industry, employee_range, funding_stage, etc) - top_matches array with: company, score (0-100), why_good_fit, contact_priority, estimated_deal_size Use real companies that match the criteria.`; const discoveryResponse = await anthropic.messages.create({ model: 'claude-3-5-sonnet-20241022', max_tokens: 2048, messages: [{ role: 'user', content: discoveryPrompt }], }); const discoveryContent = discoveryResponse.content[0]; if (discoveryContent.type !== 'text') throw new Error('Invalid response'); const discovery = JSON.parse(discoveryContent.text); // Step 2: Generate outreach sequences for top match const topPartner = discovery.top_matches[0]; const outreachPrompt = `Create a 3-touch outreach sequence for this partnership opportunity. Target: ${topPartner.company} Why good fit: ${topPartner.why_good_fit} Output as JSON with: - decision_makers array (name, title, linkedin, email_pattern, outreach_angle) - outreach_sequence array (day, channel, message, goal) - next_action - follow_up_date Make messages personalized and specific, not generic.`; const outreachResponse = await anthropic.messages.create({ model: 'claude-3-5-sonnet-20241022', max_tokens: 2048, messages: [{ role: 'user', content: outreachPrompt }], }); const outreachContent = outreachResponse.content[0]; if (outreachContent.type !== 'text') throw new Error('Invalid response'); const outreach = JSON.parse(outreachContent.text); // Step 3: Initialize deal tracking const trackingPrompt = `Create initial deal tracking structure for this partnership. Partner: ${topPartner.company} Outreach plan: ${JSON.stringify(outreach.outreach_sequence)} Output as JSON with: - deal_stage (Initial Contact, Technical Review, Negotiation, Closing) - health_score (0-100) - next_steps array (action, owner, due_date, priority) - risk_factors array (risk, mitigation, severity) - estimated_close_date - confidence (percentage)`; const trackingResponse = await anthropic.messages.create({ model: 'claude-3-5-sonnet-20241022', max_tokens: 2048, messages: [{ role: 'user', content: trackingPrompt }], }); const trackingContent = trackingResponse.content[0]; if (trackingContent.type !== 'text') throw new Error('Invalid response'); const tracking = JSON.parse(trackingContent.text); return { discovery, outreach, tracking }; } // Usage const criteria = `Looking for SaaS companies in HR tech space with 50-200 employees...`; const pipeline = await automatePartnershipPipeline(criteria); console.log(`Found ${pipeline.discovery.top_matches.length} potential partners`); console.log(`Top match: ${pipeline.discovery.top_matches[0].company}`); console.log(`Next action: ${pipeline.outreach.next_action}`);
Level 2: With CRM Integration & Enrichment
Good for: 50-200 active deals | Setup time: 2 hours
# With CRM Integration & Enrichment (50-200 active deals) import anthropic import requests import os from datetime import datetime, timedelta from typing import Dict, List class PartnershipPipeline: def __init__(self): self.anthropic = anthropic.Anthropic(api_key=os.getenv('ANTHROPIC_API_KEY')) self.clearbit_key = os.getenv('CLEARBIT_API_KEY') self.hubspot_key = os.getenv('HUBSPOT_API_KEY') def enrich_company_data(self, company_name: str) -> Dict: """Use Clearbit to get real company data""" try: response = requests.get( f'https://company.clearbit.com/v2/companies/find', params={'name': company_name}, headers={'Authorization': f'Bearer {self.clearbit_key}'}, timeout=10 ) response.raise_for_status() data = response.json() return { 'name': data.get('name'), 'domain': data.get('domain'), 'employees': data.get('metrics', {}).get('employees'), 'funding': data.get('metrics', {}).get('raised'), 'tech_stack': data.get('tech', []), 'linkedin': data.get('linkedin', {}).get('handle'), } except Exception as e: print(f'Enrichment failed for {company_name}: {e}') return {'name': company_name, 'error': str(e)} def create_hubspot_deal(self, partner_data: Dict, outreach_data: Dict) -> str: """Create deal in HubSpot CRM""" deal_payload = { 'properties': { 'dealname': f'Partnership - {partner_data["company"]}', 'dealstage': 'qualifiedtobuy', # Initial contact stage 'amount': partner_data.get('estimated_deal_size', '0'), 'closedate': (datetime.now() + timedelta(days=90)).isoformat(), 'pipeline': 'partnerships', 'partnership_score': partner_data.get('score', 0), 'next_action': outreach_data.get('next_action', ''), } } try: response = requests.post( 'https://api.hubapi.com/crm/v3/objects/deals', headers={ 'Authorization': f'Bearer {self.hubspot_key}', 'Content-Type': 'application/json' }, json=deal_payload, timeout=10 ) response.raise_for_status() deal_id = response.json()['id'] print(f'Created HubSpot deal: {deal_id}') return deal_id except Exception as e: print(f'Failed to create HubSpot deal: {e}') return '' def discover_partners(self, criteria: str) -> List[Dict]: """Step 1: Discover and score partners with enrichment""" prompt = f"""Extract partnership criteria and identify top 5 potential partners. Criteria: {criteria} Output as JSON with search_criteria and top_matches array. Include: company, score, why_good_fit, contact_priority, estimated_deal_size, integration_complexity.""" response = self.anthropic.messages.create( model='claude-3-5-sonnet-20241022', max_tokens=2048, messages=[{'role': 'user', 'content': prompt}] ) content = response.content[0] if content.type != 'text': raise ValueError('Invalid response type') discovery = anthropic.json.loads(content.text) # Enrich top 3 matches with real data for match in discovery['top_matches'][:3]: enriched = self.enrich_company_data(match['company']) match['enriched_data'] = enriched return discovery['top_matches'] def generate_outreach(self, partner: Dict) -> Dict: """Step 2: Generate personalized outreach sequence""" prompt = f"""Create a 3-touch outreach sequence for this partnership. Target: {partner['company']} Why good fit: {partner['why_good_fit']} Enriched data: {partner.get('enriched_data', {})} Output as JSON with: - decision_makers (find real people if possible) - outreach_sequence (3 touches: LinkedIn, Email, Email) - next_action - follow_up_date Make messages specific and personalized.""" response = self.anthropic.messages.create( model='claude-3-5-sonnet-20241022', max_tokens=2048, messages=[{'role': 'user', 'content': prompt}] ) content = response.content[0] if content.type != 'text': raise ValueError('Invalid response type') return anthropic.json.loads(content.text) def track_deal(self, partner: Dict, outreach: Dict) -> Dict: """Step 3: Initialize deal tracking with health scoring""" prompt = f"""Create deal tracking structure for this partnership. Partner: {partner['company']} Score: {partner['score']} Outreach plan: {outreach['outreach_sequence']} Output as JSON with: - deal_stage - health_score (0-100) - signals array (type, signal, impact) - next_steps array (action, owner, due_date, priority) - risk_factors array (risk, mitigation, severity) - estimated_close_date - confidence""" response = self.anthropic.messages.create( model='claude-3-5-sonnet-20241022', max_tokens=2048, messages=[{'role': 'user', 'content': prompt}] ) content = response.content[0] if content.type != 'text': raise ValueError('Invalid response type') return anthropic.json.loads(content.text) def run_pipeline(self, criteria: str) -> Dict: """Full pipeline: discover → enrich → outreach → track → CRM""" print('🔍 Discovering partners...') partners = self.discover_partners(criteria) print(f'✅ Found {len(partners)} potential partners') top_partner = partners[0] print(f'📧 Generating outreach for {top_partner["company"]}...') outreach = self.generate_outreach(top_partner) print('📊 Setting up deal tracking...') tracking = self.track_deal(top_partner, outreach) print('💼 Creating HubSpot deal...') deal_id = self.create_hubspot_deal(top_partner, outreach) return { 'partner': top_partner, 'outreach': outreach, 'tracking': tracking, 'crm_deal_id': deal_id } # Usage pipeline = PartnershipPipeline() criteria = """Looking for SaaS companies in HR tech space with 50-200 employees, raised Series A or B...""" result = pipeline.run_pipeline(criteria) print(f"\n🎯 Pipeline complete for {result['partner']['company']}") print(f"Next action: {result['outreach']['next_action']}") print(f"HubSpot deal: {result['crm_deal_id']}")
Level 3: Production Pattern with LangGraph
Good for: 200+ active deals | Setup time: 1 day
# Production Pattern with LangGraph (200+ active deals) from langgraph.graph import StateGraph, END from typing import TypedDict, List, Annotated import operator import anthropic import requests import os class PipelineState(TypedDict): criteria: str discovered_partners: Annotated[List[dict], operator.add] enriched_partners: Annotated[List[dict], operator.add] outreach_plans: dict active_deals: dict crm_synced: bool error_log: Annotated[List[str], operator.add] def discover_node(state: PipelineState) -> PipelineState: """Discover potential partners using AI""" client = anthropic.Anthropic(api_key=os.getenv('ANTHROPIC_API_KEY')) try: response = client.messages.create( model='claude-3-5-sonnet-20241022', max_tokens=2048, messages=[{ 'role': 'user', 'content': f'Find top 10 partners matching: {state["criteria"]}' }] ) content = response.content[0] if content.type != 'text': raise ValueError('Invalid response') partners = anthropic.json.loads(content.text)['top_matches'] state['discovered_partners'] = partners except Exception as e: state['error_log'].append(f'Discovery failed: {e}') return state def enrich_node(state: PipelineState) -> PipelineState: """Enrich with Clearbit data""" clearbit_key = os.getenv('CLEARBIT_API_KEY') enriched = [] for partner in state['discovered_partners'][:5]: # Top 5 only try: response = requests.get( 'https://company.clearbit.com/v2/companies/find', params={'name': partner['company']}, headers={'Authorization': f'Bearer {clearbit_key}'}, timeout=10 ) if response.status_code == 200: data = response.json() partner['enriched'] = { 'employees': data.get('metrics', {}).get('employees'), 'funding': data.get('metrics', {}).get('raised'), 'tech': data.get('tech', []), 'domain': data.get('domain') } enriched.append(partner) except Exception as e: state['error_log'].append(f'Enrichment failed for {partner["company"]}: {e}') state['enriched_partners'] = enriched return state def outreach_node(state: PipelineState) -> PipelineState: """Generate outreach sequences for enriched partners""" client = anthropic.Anthropic(api_key=os.getenv('ANTHROPIC_API_KEY')) outreach_plans = {} for partner in state['enriched_partners'][:3]: # Top 3 get full sequences try: response = client.messages.create( model='claude-3-5-sonnet-20241022', max_tokens=2048, messages=[{ 'role': 'user', 'content': f'Create outreach sequence for: {partner}' }] ) content = response.content[0] if content.type != 'text': raise ValueError('Invalid response') outreach_plans[partner['company']] = anthropic.json.loads(content.text) except Exception as e: state['error_log'].append(f'Outreach generation failed: {e}') state['outreach_plans'] = outreach_plans return state def crm_sync_node(state: PipelineState) -> PipelineState: """Sync to HubSpot CRM""" hubspot_key = os.getenv('HUBSPOT_API_KEY') active_deals = {} for company, outreach in state['outreach_plans'].items(): try: deal_payload = { 'properties': { 'dealname': f'Partnership - {company}', 'dealstage': 'qualifiedtobuy', 'pipeline': 'partnerships', 'next_action': outreach.get('next_action', '') } } response = requests.post( 'https://api.hubapi.com/crm/v3/objects/deals', headers={ 'Authorization': f'Bearer {hubspot_key}', 'Content-Type': 'application/json' }, json=deal_payload, timeout=10 ) if response.status_code == 201: deal_id = response.json()['id'] active_deals[company] = deal_id except Exception as e: state['error_log'].append(f'CRM sync failed for {company}: {e}') state['active_deals'] = active_deals state['crm_synced'] = len(active_deals) > 0 return state def should_enrich(state: PipelineState) -> str: """Route based on discovery success""" if len(state['discovered_partners']) > 0: return 'enrich' return 'end' def should_generate_outreach(state: PipelineState) -> str: """Route based on enrichment success""" if len(state['enriched_partners']) > 0: return 'outreach' return 'end' # Build the graph def build_partnership_graph(): workflow = StateGraph(PipelineState) # Add nodes workflow.add_node('discover', discover_node) workflow.add_node('enrich', enrich_node) workflow.add_node('outreach', outreach_node) workflow.add_node('crm_sync', crm_sync_node) # Add edges workflow.set_entry_point('discover') workflow.add_conditional_edges( 'discover', should_enrich, { 'enrich': 'enrich', 'end': END } ) workflow.add_conditional_edges( 'enrich', should_generate_outreach, { 'outreach': 'outreach', 'end': END } ) workflow.add_edge('outreach', 'crm_sync') workflow.add_edge('crm_sync', END) return workflow.compile() # Usage partnership_graph = build_partnership_graph() initial_state = PipelineState( criteria="SaaS companies in HR tech, 50-200 employees, Series A/B", discovered_partners=[], enriched_partners=[], outreach_plans={}, active_deals={}, crm_synced=False, error_log=[] ) result = partnership_graph.invoke(initial_state) print(f"✅ Discovered: {len(result['discovered_partners'])} partners") print(f"✅ Enriched: {len(result['enriched_partners'])} partners") print(f"✅ Outreach plans: {len(result['outreach_plans'])}") print(f"✅ CRM deals: {len(result['active_deals'])}") print(f"⚠️ Errors: {len(result['error_log'])}")
When to Level Up
Start: Simple API Calls
0-50 active deals
- Sequential API calls, no framework needed
- Manual CRM entry after generation
- Basic error logging with console.log
Scale: Add Integrations
50-200 active deals
- Clearbit enrichment for company data
- Automatic HubSpot deal creation
- Error handling with retries and logging
- Slack notifications for high-priority deals
Production: Framework & Orchestration
200-500 active deals
- LangGraph for complex workflows with conditional routing
- Parallel processing for discovery + enrichment
- State management across steps (no data loss)
- Human-in-the-loop for deal review before outreach
Enterprise: Multi-Agent System
500+ active deals
- Specialized agents (discovery, enrichment, outreach, negotiation)
- Real-time deal health monitoring dashboard
- Automated follow-up sequences with email/LinkedIn integration
- Multi-CRM sync (HubSpot, Salesforce, Pipedrive)
- Revenue forecasting and pipeline analytics
Strategy-Specific Gotchas
The code examples work. But partnership pipeline has unique challenges you need to handle.
Company Data Enrichment Rate Limits
Clearbit and similar APIs have strict rate limits (1 request/second for free tier). If you're enriching 100 companies, that's 100 seconds. Use batch processing and caching to avoid hitting limits.
import time import redis import json class EnrichmentCache: def __init__(self): self.redis = redis.Redis(host='localhost', port=6379, db=0) self.ttl = 86400 * 7 # Cache for 7 days def get_enriched_data(self, company_name: str) -> dict: # Check cache first cached = self.redis.get(f'company:{company_name}') if cached: return json.loads(cached) # Rate limit: 1 req/sec time.sleep(1) # Fetch from Clearbit response = requests.get( 'https://company.clearbit.com/v2/companies/find', params={'name': company_name}, headers={'Authorization': f'Bearer {os.getenv("CLEARBIT_KEY")}'}, timeout=10 ) if response.status_code == 200: data = response.json() # Cache the result self.redis.setex( f'company:{company_name}', self.ttl, json.dumps(data) ) return data return {} # Usage: Batch process with caching cache = EnrichmentCache() for company in discovered_partners: enriched = cache.get_enriched_data(company['name']) company['enriched'] = enriched
CRM Duplicate Detection
HubSpot and Salesforce will create duplicate deals if you don't check first. Always search by company domain before creating new deals. Use upsert pattern instead of create.
async function upsertDeal( companyName: string, dealData: any ): Promise<string> { const hubspotKey = process.env.HUBSPOT_API_KEY!; // Search for existing deal by company name const searchResponse = await fetch( 'https://api.hubapi.com/crm/v3/objects/deals/search', { method: 'POST', headers: { Authorization: `Bearer ${hubspotKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ filterGroups: [ { filters: [ { propertyName: 'dealname', operator: 'CONTAINS_TOKEN', value: companyName, }, ], }, ], }), } ); const searchData = await searchResponse.json(); if (searchData.results && searchData.results.length > 0) { // Update existing deal const dealId = searchData.results[0].id; await fetch(`https://api.hubapi.com/crm/v3/objects/deals/${dealId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${hubspotKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ properties: dealData }), }); return dealId; } else { // Create new deal const createResponse = await fetch( 'https://api.hubapi.com/crm/v3/objects/deals', { method: 'POST', headers: { Authorization: `Bearer ${hubspotKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ properties: dealData }), } ); const createData = await createResponse.json(); return createData.id; } }
Outreach Personalization at Scale
Generic outreach gets ignored. You need to personalize based on recent company news, mutual connections, or specific pain points. Use LinkedIn Sales Navigator API or news APIs to find personalization hooks.
import anthropic import requests def get_personalization_hooks(company_name: str, domain: str) -> dict: """Find recent news and signals for personalization""" # Get recent company news (using NewsAPI or similar) news_response = requests.get( 'https://newsapi.org/v2/everything', params={ 'q': company_name, 'sortBy': 'publishedAt', 'pageSize': 3, 'apiKey': os.getenv('NEWS_API_KEY') }, timeout=10 ) recent_news = [] if news_response.status_code == 200: articles = news_response.json().get('articles', []) recent_news = [a['title'] for a in articles[:3]] # Check for job postings (signals growth) jobs_response = requests.get( f'https://api.lever.co/v0/postings/{domain}', timeout=10 ) open_roles = [] if jobs_response.status_code == 200: jobs = jobs_response.json() open_roles = [j['text'] for j in jobs[:5]] return { 'recent_news': recent_news, 'open_roles': open_roles, 'hiring_signals': len(open_roles) > 0 } def generate_personalized_outreach(partner: dict) -> str: """Generate outreach with personalization hooks""" hooks = get_personalization_hooks( partner['company'], partner['enriched'].get('domain', '') ) client = anthropic.Anthropic(api_key=os.getenv('ANTHROPIC_API_KEY')) prompt = f"""Create a personalized LinkedIn message for this partnership opportunity. Target: {partner['company']} Why good fit: {partner['why_good_fit']} Recent news: {hooks['recent_news']} Open roles: {hooks['open_roles']} Make it: 1. Reference specific recent news or hiring 2. Show you understand their business 3. Clear value prop in 2 sentences 4. Soft ask for 15 min call Keep under 150 words.""" response = client.messages.create( model='claude-3-5-sonnet-20241022', max_tokens=512, messages=[{'role': 'user', 'content': prompt}] ) content = response.content[0] if content.type != 'text': raise ValueError('Invalid response') return content.text
Deal Health Scoring Accuracy
AI-generated health scores are guesses without real signals. Track actual engagement: email opens, link clicks, meeting attendance, response time. Feed these signals back to improve scoring.
interface EngagementSignal { type: 'email_open' | 'link_click' | 'meeting_booked' | 'response'; timestamp: Date; metadata?: any; } function calculateDealHealth( baseScore: number, signals: EngagementSignal[] ): number { let score = baseScore; // Recent signals matter more (decay over time) const now = new Date(); const daysSinceLastSignal = signals.length ? (now.getTime() - signals[signals.length - 1].timestamp.getTime()) / (1000 * 60 * 60 * 24) : 999; // Penalize stale deals if (daysSinceLastSignal > 14) { score -= 20; } else if (daysSinceLastSignal > 7) { score -= 10; } // Positive signals in last 7 days const recentSignals = signals.filter( (s) => (now.getTime() - s.timestamp.getTime()) / (1000 * 60 * 60 * 24) <= 7 ); recentSignals.forEach((signal) => { switch (signal.type) { case 'email_open': score += 5; break; case 'link_click': score += 10; break; case 'meeting_booked': score += 25; break; case 'response': score += 15; break; } }); // Cap between 0-100 return Math.max(0, Math.min(100, score)); } // Usage: Update health score based on real engagement const signals: EngagementSignal[] = [ { type: 'email_open', timestamp: new Date('2025-09-01') }, { type: 'link_click', timestamp: new Date('2025-09-02') }, { type: 'meeting_booked', timestamp: new Date('2025-09-03') }, ]; const healthScore = calculateDealHealth(75, signals); console.log(`Updated health score: ${healthScore}`);
Contract Automation with DocuSign
Once a deal is closing, you need to send contracts. Integrate DocuSign API to auto-generate partnership agreements from templates. Pre-fill company details, send for signature, track completion.
import requests import base64 import os def send_partnership_contract( partner_name: str, partner_email: str, deal_terms: dict ) -> str: """Send partnership agreement via DocuSign""" docusign_account_id = os.getenv('DOCUSIGN_ACCOUNT_ID') docusign_access_token = os.getenv('DOCUSIGN_ACCESS_TOKEN') template_id = os.getenv('DOCUSIGN_PARTNERSHIP_TEMPLATE_ID') # Create envelope from template envelope_definition = { 'templateId': template_id, 'templateRoles': [ { 'email': partner_email, 'name': partner_name, 'roleName': 'Partner', 'tabs': { 'textTabs': [ { 'tabLabel': 'PartnerName', 'value': partner_name }, { 'tabLabel': 'DealValue', 'value': deal_terms.get('value', '') }, { 'tabLabel': 'StartDate', 'value': deal_terms.get('start_date', '') } ] } } ], 'status': 'sent' } response = requests.post( f'https://demo.docusign.net/restapi/v2.1/accounts/{docusign_account_id}/envelopes', headers={ 'Authorization': f'Bearer {docusign_access_token}', 'Content-Type': 'application/json' }, json=envelope_definition, timeout=10 ) if response.status_code == 201: envelope_id = response.json()['envelopeId'] print(f'Contract sent! Envelope ID: {envelope_id}') return envelope_id else: raise Exception(f'DocuSign API error: {response.text}') # Usage: Auto-send contract when deal reaches "Closing" stage deal_terms = { 'value': '$250,000 ARR', 'start_date': '2025-10-01', 'duration': '12 months' } envelope_id = send_partnership_contract( 'Lattice', 'partnerships@lattice.com', deal_terms )
Cost Calculator
Manual Process
Limitations:
- • Can't track more than 20-30 active deals
- • 60% of follow-ups forgotten or delayed
- • No data-driven prioritization
- • Spreadsheet chaos across team
Automated Pipeline
Benefits:
- ✓ Track 200+ active deals simultaneously
- ✓ Automated follow-ups (0% missed)
- ✓ Data-driven deal prioritization
- ✓ Full team visibility in CRM
- ✓ Contract automation saves 5 hours/deal