[Case Study] How We Built an AI Sales Risk Pipeline That Surfaces Real Problems, Not Just Sentiment
Designing an AI-Driven Sales Risk Pipeline for the Enterprise
Last month, our sales team came to the AI Center of Excellence with a familiar refrain: “Can AI help us stop drowning in data?” While they had access to the kind of enterprise CRM you’d expect at a company our size, their operational reality was different: a curated Notion database tailored to their specific workflows, tracking the metrics and relationships that actually mattered for their portfolio. Their vision? AI that could surface risks before they became surprises, draft contextual outreach, and turn static records into living intelligence.
I could have pushed them back toward the “official” system. But KISS kicked in: Keep It Simple, Stupid. They’re already using Notion as their source of truth. Why fight that battle when we could augment what they have?
Three weeks later, we have a working system: an AI agent named Prospector that lives in their chat workflow, answers natural language questions about portfolio risk, drafts contextual outreach, and generates executive presentations on demand, all backed by real-time web intelligence that refreshes automatically.
Here’s how we built it.
The Intelligence Gap
The sales team was managing a substantial enterprise software portfolio, the data was there, but it was static. Nobody had time to research whether Company A was laying off their IT team or if Company B had budget freezes coming.
Sales reps were making decisions in information vacuums. A client might be three months from churn due to acquisition rumors, but the rep wouldn’t know until the “we’re not renewing” email landed in their inbox.
The scale problem:
This wasn’t a data quality issue. The sales team was managing a $100M+ portfolio spanning over 600 client relationships, and frankly, their Notion database was impressive. Account details, renewal dates, relationship history, contact info: all well-maintained.
But here’s the thing: keeping 600 records accurate is a full-time job. Keeping 600 records enriched with current market intelligence is impossible. No human team can Google every client weekly, read the earnings reports, catch the layoff announcements, notice the acquisition rumors, while also doing their actual job of selling.
The data was solid. The intelligence wasn’t there because it couldn’t be. Not manually. Not at this scale.
Architecture Decision: Augment, Don’t Replace
Before writing code, I mapped the options:
Option 3 won. The sales team already lives in Notion. Adding an intelligence layer on top means they get smarter data without changing their workflow, and without waiting six months for IT to prioritize CRM customizations.
The architecture that emerged:
Key design principles:
Notion stays source of truth. No data migration, no sync conflicts
Intelligence is additive. New fields enrich existing records, never overwrite user data
API-first backend. Everything accessible via REST for agent integration
Human-in-the-loop by default. AI recommends, humans approve
The Intelligence Pipeline: Firecrawl + Evidence-Based Scoring
The enrichment pipeline runs on a simple premise: companies broadcast signals about their health constantly. News articles, press releases, job postings, earnings reports. We just need to collect and interpret them.
Firecrawl Integration
Firecrawl handles the web intelligence extraction. The key is using Firecrawl’s agent mode with Pydantic schemas for structured extraction. No separate LLM call needed:
from firecrawl import Firecrawl
from pydantic import BaseModel, Field
from typing import Optional, List
# Pydantic schemas for structured extraction
class CompanyInfo(BaseModel):
"""Schema for company information extraction."""
employee_count: Optional[str] = Field(description="Employee count or range (e.g., '1000-5000')")
industry: Optional[str] = Field(description="Primary business category")
company_size: Optional[str] = Field(description="Size category: startup/SMB/enterprise")
headquarters: Optional[str] = Field(description="City, State/Country")
class CompanyNews(BaseModel):
"""Schema for company news extraction."""
has_layoffs: bool = Field(default=False, description="Recent layoff announcements")
has_ma: bool = Field(default=False, description="M&A activity (acquiring or being acquired)")
is_expanding: bool = Field(default=False, description="Growth/expansion signals")
sentiment: Optional[str] = Field(description="Overall news sentiment: positive/neutral/negative")
key_events: List[str] = Field(default_factory=list, description="Recent significant events")
sources: List[str] = Field(default_factory=list, description="URLs of source articles")
def enrich_company(firecrawl: Firecrawl, company_name: str) -> dict:
"""
Enrich a single company using Firecrawl agent.
Returns structured data via Pydantic schema extraction.
"""
# Get company info
info_result = firecrawl.agent(
prompt=f"Find current information about {company_name}: employee count, industry, company size, headquarters location.",
schema=CompanyInfo,
max_pages=3
)
# Get recent news and events
news_result = firecrawl.agent(
prompt=f"Find recent news about {company_name} from the last 6 months. Look for: layoffs, acquisitions, mergers, expansion, funding, leadership changes.",
schema=CompanyNews,
max_pages=5
)
return {
"company_name": company_name,
"employee_count": info_result.employee_count,
"industry": info_result.industry,
"company_size": info_result.company_size,
"headquarters": info_result.headquarters,
"has_layoffs": news_result.has_layoffs,
"has_ma": news_result.has_ma,
"is_expanding": news_result.is_expanding,
"sentiment": news_result.sentiment,
"key_events": news_result.key_events,
"sources": news_result.sources,
}The beauty of this approach: Firecrawl’s agent handles the search, scraping, AND extraction in one call. The Pydantic schema enforces structure, so downstream code never deals with parsing failures.
Evidence-Based Risk Scoring
Here’s the critical insight: sentiment analysis alone is useless for churn prediction. A company can have negative press coverage and still be a loyal customer. What matters is evidence of specific business events that correlate with contract risk.
I built an evidence-based scoring system that only flags real churn indicators:
class RiskScorer:
"""
Evidence-based risk scoring for customer churn prediction.
Each signal must be backed by a specific event, not just sentiment.
"""
# Critical signals - immediate action required
CRITICAL_SIGNALS = {
"budget_cuts": 3, # Explicit cost reduction programs
"spending_freeze": 3, # Procurement freezes announced
"acquired_by_competitor": 3, # Being bought by someone who has competing product
"bankruptcy_filing": 3, # Chapter 11 or equivalent
}
# High-risk signals - schedule outreach this week
HIGH_RISK_SIGNALS = {
"major_layoffs": 2, # >10% workforce reduction
"it_department_cuts": 2, # Specifically IT/tech layoffs
"cto_cio_departure": 2, # Key technical stakeholder leaving
"negative_earnings": 2, # Missed earnings + negative guidance
"contract_consolidation": 2, # Announced vendor consolidation initiative
}
# Watch signals - monitor closely
WATCH_SIGNALS = {
"minor_layoffs": 1, # <10% workforce reduction
"leadership_transition": 1, # C-suite changes (not tech-specific)
"restructuring": 1, # Org changes without clear direction
"mixed_earnings": 1, # Beat/miss with uncertain outlook
}
# Positive signals - reduce risk score
POSITIVE_SIGNALS = {
"acquisition_mode": -2, # Company acquiring others (growth)
"expansion_announced": -2, # New markets, new offices
"funding_round": -2, # Fresh capital injection
"record_revenue": -1, # Strong financial performance
"hiring_spree": -1, # Significant hiring (especially IT)
}
def calculate_risk(self, events: list[CompanyEvent]) -> RiskAssessment:
"""
Calculate risk score from list of detected events.
Returns risk level + supporting evidence.
"""
score = 0
evidence = []
for event in events:
signal_weight = self._get_signal_weight(event.event_type)
if signal_weight != 0:
score += signal_weight
evidence.append({
"signal": event.event_type,
"weight": signal_weight,
"date": event.event_date,
"summary": event.event_summary,
"source": event.source_url
})
risk_level = self._score_to_level(score)
return RiskAssessment(
level=risk_level,
score=score,
evidence=evidence,
recommended_action=self._get_recommended_action(risk_level)
)
def _score_to_level(self, score: int) -> str:
if score >= 5:
return "CRITICAL"
elif score >= 3:
return "HIGH"
elif score >= 1:
return "WATCH"
elif score <= -2:
return "OPPORTUNITY"
else:
return "STABLE"Why evidence-based scoring matters:
The system won’t flag a company as high-risk just because there’s negative sentiment in a news article. It needs to detect a specific event type (layoffs, budget cuts, acquisition) with a source URL. This dramatically reduces false positives and gives sales reps something concrete to reference in conversations.
# Bad: Sentiment-only approach
if sentiment == "negative":
risk = "HIGH" # Too many false positives
# Good: Evidence-based approach
if any(e.event_type == "major_layoffs" for e in events):
risk = "HIGH" # Actionable, verifiable signalRisk Level to Action Mapping
Each risk level maps to a specific recommended action:
The Automation Stack: AWS Infrastructure
One of the first decisions was where to run this thing. The system has two distinct workloads with different requirements:
Trying to run both on the same infrastructure would mean either paying for idle compute (running enrichment on an always-on server) or dealing with cold starts (running the API on Lambda). The answer: use both.
The Architecture
Why Lambda for Enrichment
The enrichment pipeline is a perfect Lambda use case:
Burst pattern: Enriching 600 companies once a day means ~600 function invocations, then nothing until tomorrow. Paying for a server to sit idle 23 hours a day makes no sense.
Natural parallelization: Each company enrichment is independent. The orchestrator fans out to 10 concurrent workers, processes a batch, then fans out again. Total enrichment time for the full portfolio: ~15 minutes instead of ~10 hours sequential.
Built-in resilience: Lambda automatically retries failed invocations. If Firecrawl times out on one company, that worker retries without blocking the others.
# orchestrator.py - Fan-out pattern
def handler(event: Dict, context: Any) -> Dict:
"""
Orchestrator Lambda - reads Notion, dispatches workers in parallel.
Triggered daily by EventBridge at 12:00 UTC (7am EST).
"""
secrets = get_secrets() # From AWS Secrets Manager
reader = NotionReader(
api_key=secrets['NOTION_API_KEY'],
database_id=secrets['NOTION_DATABASE_ID']
)
# Find companies needing refresh (not enriched in 7+ days)
companies = reader.get_companies_to_enrich(
max_companies=event.get('max_companies', 50),
stale_days=event.get('stale_days', 7)
)
logger.info(f"Found {len(companies)} companies to enrich")
# Fan out to workers in batches of 10
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = [
executor.submit(invoke_worker, company, lambda_client)
for company in companies
]
for future in concurrent.futures.as_completed(futures):
results.append(future.result())
successful = sum(1 for r in results if r.get('success'))
return {
'statusCode': 200,
'body': {
'total': len(companies),
'successful': successful,
'failed': len(companies) - successful
}
}Why EC2 for the API
The FastAPI server runs on a t3.small EC2 instance. Why not Lambda?
Always-on requirement: The BrainTrust agent needs sub-second responses. Lambda cold starts (especially with the Notion SDK loaded) add 2-3 seconds of latency. Unacceptable for conversational UX.
Connection pooling: The API maintains persistent connections to Notion. Lambda’s stateless model would mean reconnecting on every request.
Cost math: A t3.small runs ~$15/month. The equivalent Lambda invocations (assuming 1000 queries/day × 500ms average × 30 days) would cost roughly the same, but with worse latency.
Deployment simplicity: SCP the files, restart the service. No container builds, no deployment packages, no git on the server.
# deploy-api.sh - Dead simple deployment
#!/bin/bash
set -e
EC2_HOST="ec2-user@your-api-domain.com"
REMOTE_PATH="/home/ec2-user/sales-pipeline"
echo "Deploying API server..."
# Copy source files directly (faster iteration than git)
scp -r api_server.py notion_actions.py requirements.txt $EC2_HOST:$REMOTE_PATH/
# Restart the service
ssh $EC2_HOST << 'EOF'
cd /home/ec2-user/sales-pipeline
pip install -r requirements.txt --quiet
sudo systemctl restart sales-pipeline-api
EOF
echo "Deployment complete. Health check:"
curl -s https://your-api-domain/health | jqEventBridge Scheduling
The enrichment runs daily at 7am EST (12:00 UTC) via EventBridge:
{
"Name": "portfolio-enrichment-daily",
"ScheduleExpression": "cron(0 12 * * ? *)",
"Target": {
"Arn": "arn:aws:lambda:us-east-1:ACCOUNT:function:portfolio-orchestrator",
"Input": "{\"max_companies\": 100, \"stale_days\": 7}"
}
}Why 7am? Sales team arrives, opens chat, asks “what needs attention today?” The data is already fresh from overnight enrichment.
Secrets Management
All credentials live in AWS Secrets Manager, not environment variables or config files:
# Shared secret retrieval pattern
def get_secrets() -> Dict[str, str]:
"""
Retrieve secrets from AWS Secrets Manager.
Falls back to .env for local development.
"""
try:
client = boto3.client('secretsmanager', region_name='us-east-1')
response = client.get_secret_value(
SecretId='Sales-Pipeline/secrets'
)
return json.loads(response['SecretString'])
except Exception:
# Local dev fallback
from dotenv import load_dotenv
load_dotenv()
return {
'NOTION_API_KEY': os.getenv('NOTION_API_KEY'),
'NOTION_DATABASE_ID': os.getenv('NOTION_DATABASE_ID'),
'FIRECRAWL_API_KEY': os.getenv('FIRECRAWL_API_KEY'),
'API_KEY': os.getenv('API_KEY'),
}This pattern means:
No secrets in git (ever)
Same code runs locally and in production
Credential rotation doesn’t require redeployment
IAM controls who can access what
Worker Function
Each worker Lambda enriches a single company:
# worker.py - Single company enrichment
def handler(event: Dict, context: Any) -> Dict:
"""
Worker Lambda - enriches one company via Firecrawl, syncs to Notion.
Invoked by orchestrator with company details.
"""
page_ids = event.get('page_ids', []) # May have multiple Notion pages
company_name = event['company_name']
secrets = get_secrets()
firecrawl = Firecrawl(api_key=secrets['FIRECRAWL_API_KEY'])
try:
# Gather and structure web intelligence
enrichment = enrich_company(firecrawl, company_name)
# Calculate risk score
risk = analyze_risk(enrichment)
# Sync to all Notion pages for this company
synced = 0
for page_id in page_ids:
if sync_to_notion(page_id, enrichment, risk, secrets['NOTION_API_KEY']):
synced += 1
# Create action item if high risk
action = None
if risk['risk_level'] in ['CRITICAL', 'HIGH']:
action = build_action(risk['risk_level'], risk['evidence'])
return {
'success': True,
'company': company_name,
'risk_level': risk['risk_level'],
'pages_synced': synced,
'action_created': action
}
except Exception as e:
logger.error(f"Failed to enrich {company_name}: {e}")
return {
'success': False,
'company': company_name,
'error': str(e)
}Cost Breakdown
Here’s what this actually costs to run:
Compare that to enterprise sales intelligence tools that charge $500+/seat/month. This system costs less than one user license and covers the entire team.
Notion Field Updates
The worker updates AI-managed fields in Notion (all prefixed with [Auto] to distinguish from user data):
def sync_to_notion(page_id: str, enrichment: Dict, risk: Dict, api_key: str) -> bool:
"""
Update Notion page with enrichment results.
Only updates AI-managed fields, never overwrites user data.
"""
notion = Client(auth=api_key)
properties = {
# Company intel
"[Auto] Employee Count": {"rich_text": [{"text": {"content": enrichment.get('employee_count', '')}}]},
"[Auto] Industry": {"rich_text": [{"text": {"content": enrichment.get('industry', '')}}]},
"[Auto] Company Size": {"rich_text": [{"text": {"content": enrichment.get('company_size', '')}}]},
# Risk assessment
"[Auto] Risk Level": {"select": {"name": risk['risk_level']} if risk['risk_level'] else None},
"[Auto] News Sentiment": {"select": {"name": enrichment.get('sentiment')} if enrichment.get('sentiment') else None},
"[Auto] Enrichment Notes": {"rich_text": [{"text": {"content": risk.get('reasoning', '')[:2000]}}]},
# Signals (boolean flags)
"[Auto] Has Layoffs": {"checkbox": enrichment.get('has_layoffs', False)},
"[Auto] Has M&A": {"checkbox": enrichment.get('has_ma', False)},
"[Auto] Is Expanding": {"checkbox": enrichment.get('is_expanding', False)},
# Metadata
"[Auto] Last Enriched": {"date": {"start": datetime.now().isoformat()[:10]}},
}
# Create action if needed
if risk['risk_level'] in ['CRITICAL', 'HIGH']:
action = ACTION_MAP.get(risk['risk_level'], 'Review Account')
properties["[Auto] Recommendation Action"] = {"rich_text": [{"text": {"content": action}}]}
properties["[Auto] Recommendation Status"] = {"select": {"name": "Pending"}}
properties["[Auto] Recommendation Reasoning"] = {"rich_text": [{"text": {"content": risk['reasoning'][:2000]}}]}
notion.pages.update(page_id=page_id, properties=properties)
return TrueThe FastAPI Backend: REST Interface for Agent Integration
The backend exposes the enrichment data and action management through clean REST endpoints. This became the foundation for the agent integration.
# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(
title="Sales Intelligence API",
description="Portfolio intelligence and action management",
version="1.0.0"
)
# ============ Actions Endpoints ============
@app.get("/actions/pending", response_model=list[ActionItem])
async def get_pending_actions(
risk_level: str | None = None,
limit: int = 20
):
"""
Get pending action items, optionally filtered by risk level.
Returns actions sorted by priority (CRITICAL first).
"""
actions = await notion_service.get_actions(
status="pending",
risk_level=risk_level,
limit=limit
)
return sorted(actions, key=lambda a: RISK_PRIORITY[a.risk_level])
@app.post("/actions/{action_id}/complete")
async def complete_action(
action_id: str,
outcome: ActionOutcome
):
"""
Mark an action as complete with outcome tracking.
"""
await notion_service.update_action(
action_id=action_id,
status="completed",
outcome=outcome.result,
notes=outcome.notes,
completed_at=datetime.now()
)
return {"status": "completed", "action_id": action_id}
# ============ Insights Endpoints ============
@app.get("/insights/portfolio-summary", response_model=PortfolioSummary)
async def get_portfolio_summary():
"""
Executive overview of portfolio health.
"""
companies = await notion_service.get_all_companies()
return PortfolioSummary(
total_companies=len(companies),
total_arr=sum(c.arr for c in companies if c.arr),
risk_distribution={
level: len([c for c in companies if c.risk_level == level])
for level in ['CRITICAL', 'HIGH', 'WATCH', 'STABLE', 'OPPORTUNITY']
},
enrichment_coverage=len([c for c in companies if c.last_enriched]) / len(companies),
pending_actions=await notion_service.count_pending_actions()
)
@app.get("/insights/churn-risk", response_model=list[ChurnRiskAccount])
async def get_churn_risk_accounts(
quarter: str | None = None,
min_arr: float | None = None
):
"""
Get accounts with elevated churn risk, filtered by renewal quarter and ARR.
"""
companies = await notion_service.get_companies(
risk_levels=['CRITICAL', 'HIGH'],
renewal_quarter=quarter,
min_arr=min_arr
)
return [
ChurnRiskAccount(
company_name=c.name,
arr=c.arr,
risk_level=c.risk_level,
risk_evidence=c.risk_evidence,
renewal_date=c.renewal_date,
days_to_renewal=(c.renewal_date - date.today()).days if c.renewal_date else None,
recommended_action=get_recommended_action(c.risk_level)
)
for c in companies
]
@app.get("/insights/expansion-opportunities", response_model=list[ExpansionOpportunity])
async def get_expansion_opportunities(
min_employees: int | None = None,
industry: str | None = None
):
"""
Get accounts showing growth signals suitable for upsell.
"""
companies = await notion_service.get_companies(
risk_levels=['OPPORTUNITY'],
min_employees=min_employees,
industry=industry
)
return [
ExpansionOpportunity(
company_name=c.name,
current_arr=c.arr,
employee_count=c.employee_count,
growth_signals=[e for e in c.events if e.event_type in GROWTH_EVENTS],
expansion_potential=estimate_expansion_potential(c)
)
for c in companies
]
# ============ Company Endpoints ============
@app.get("/companies/{company_id}", response_model=CompanyDetail)
async def get_company_detail(company_id: str):
"""
Get full detail for a single company including enrichment data.
"""
company = await notion_service.get_company(company_id)
if not company:
raise HTTPException(status_code=404, detail="Company not found")
return company
@app.post("/companies/{company_id}/enrich")
async def trigger_enrichment(company_id: str):
"""
Manually trigger enrichment for a specific company.
"""
company = await notion_service.get_company(company_id)
if not company:
raise HTTPException(status_code=404, detail="Company not found")
# Invoke worker Lambda directly
await trigger_worker(company_id, company.name, company.domain)
return {"status": "enrichment_triggered", "company": company.name}The OpenAPI spec generated from these endpoints becomes the foundation for the agent integration.
Meet Prospector: Where Users Actually Work
Here’s the thing about enterprise tools: nobody wants another application to check. Account managers live in Google Chat. They’re not going to context-switch to a separate dashboard, no matter how beautiful it is.
We run an internal platform called BrainTrust, an AI agent orchestration system that meets users where they are. Instead of building a chatbot from scratch, I created a specialized agent called “Prospector” that plugs into BrainTrust and gives the sales team conversational access to the pipeline intelligence.
How BrainTrust Works
BrainTrust uses a concept called Hive: you can upload an OpenAPI spec (automatically converted to an MCP server) or deploy a custom MCP server directly. I went the OpenAPI route since I already had the spec. Either way, the agent can call your endpoints as tools with full type safety and parameter validation.
The key insight: I didn’t build an agent platform. I built an API and leveraged BrainTrust to make it conversational.
OpenAPI → MCP: Zero Agent Code
Here’s the OpenAPI spec that defines what Prospector can do:
# openapi.yaml (uploaded to BrainTrust Hive)
openapi: 3.0.3
info:
title: Sales Pipeline Actions API
description: |
API for managing AI-driven actions in the Sales Pipeline.
Portfolio intelligence and action management capabilities:
- Portfolio enrichment with external intelligence (Firecrawl AI)
- AI-driven risk analysis and action recommendations
- Human-in-the-loop action approval workflow
- Portfolio insights and analytics
**Production URL:** https://your-api-domain.com
version: 1.0.0
paths:
/actions/pending:
get:
summary: Get all pending actions
description: |
Get all pending actions that need attention.
Returns entries with AI Actions that are not yet Completed or Denied.
operationId: getPendingActions
parameters:
- name: limit
in: query
schema:
type: integer
default: 100
/actions/{page_id}/approve:
post:
summary: Approve a pending action
description: |
Approve a pending action and mark it as completed.
Optionally provide execution details to track what was done.
operationId: approveAction
parameters:
- name: page_id
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
execution_type:
type: string
description: What happened (e.g., "Email Sent", "Call Scheduled")
execution_link:
type: string
description: Link to artifact (e.g., Google Doc URL)
/insights/churn-risk:
get:
summary: Get churn risk analysis
description: |
Get companies at risk of churning in specified timeframe.
Sorted by risk severity (CRITICAL first), then by ARR descending.
operationId: getChurnRisk
parameters:
- name: quarter
in: query
description: Quarter like Q1-2025
schema:
type: string
pattern: '^Q[1-4]-\d{4}$'
/enrich:
post:
summary: Trigger portfolio enrichment
description: |
Enriches companies with Firecrawl AI (news, sentiment, risk signals)
and automatically creates AI Actions in Notion for high-risk accounts.
operationId: enrichPortfolio
requestBody:
content:
application/json:
schema:
type: object
properties:
company_name:
type: string
description: Single company to enrichUpload this to BrainTrust Hive, and you get an MCP server. The agent automatically knows:
What tools are available (
getPendingActions,approveAction,getChurnRisk, etc.)What parameters each tool accepts
What the response schemas look like
No LangChain. No custom tool definitions. No agent code at all.
How It Actually Works
When a user asks a question in Google Chat, here’s the flow:
The agent has access to multiple MCP tools. Not just the sales pipeline, but also Gmail, Google Docs, and Calendar. This means it can execute the full workflow: identify the risk → draft an email → schedule a call → mark the action complete.
The Compound Effect of MCP
This is where the architecture pays off. Because everything is exposed as MCP tools, the agent can compose actions:
User: “What needs my attention today?”
Prospector:
Calls
getPendingActions→ 3 itemsCalls
getChurnRisk(quarter="Q1-2025")→ 2 additional accountsSynthesizes: “You have 5 accounts needing attention. The highest priority is Acme Corp (CRITICAL risk, $150K ARR, renewal in 45 days). They announced budget cuts last week. Want me to draft a cost-optimization pitch?”
User: “Yes, draft something”
Prospector:
Calls
getActionsForCompany("Acme Corp")→ Gets full contextUses Gmail MCP to draft email with cost-optimization messaging
“Here’s a draft email. I’ve highlighted the efficiency gains from consolidation. Send it?”
User: “Send it and mark the action done”
Prospector:
Sends via Gmail MCP
Calls
approveActionwithexecution_type="Email Sent",execution_link="<gmail link>"“Done. Action marked complete. Next up is TechCorp...”
The sales team isn’t learning a new tool. They’re having a conversation that happens to orchestrate five different systems.
Real Interaction Examples
Query: “What needs my attention today?”
Prospector: You have 4 pending actions across 3 priority levels:
🔴 CRITICAL (1):
• Acme Manufacturing: Budget cuts announced last week
- ARR: $245K | Renewal: March 15
- Evidence: CFO quoted in trade press about "20% reduction in SaaS spend"
- Recommended: Executive call to discuss cost optimization options
🟠 HIGH (2):
• TechCorp Industries: IT department layoffs
- ARR: $180K | Renewal: April 1
- Evidence: LinkedIn shows 30% of IT team marked as "open to work"
- Recommended: Check in with technical champion, assess stakeholder changes
• GlobalServices Inc: Acquisition by competitor announced
- ARR: $320K | Renewal: February 28
- Evidence: Press release Dec 15, closing expected Q1
- Recommended: Urgent outreach to understand transition timeline
🟡 WATCH (1):
• StartupCo: Leadership transition
- ARR: $95K | Renewal: May 1
- Evidence: New CEO announced, previous champion departed
- Recommended: Introduction meeting with new leadership
Want me to draft talking points for any of these calls?Query: “Draft an email to the Acme contact about their renewal”
Prospector: Based on Acme's profile, I recommend a cost-optimization angle given
their announced budget cuts. Here's a draft:
---
Subject: Supporting Acme's efficiency initiatives
Hi [Contact Name],
I wanted to reach out as your renewal approaches in March. I understand Acme is
focused on optimizing operational costs. We've been helping similar customers
achieve 15-20% efficiency gains through [specific feature].
I'd welcome 20 minutes to share what's working for other manufacturing companies
and explore whether there's an opportunity to deliver more value within your
current investment.
Would Thursday or Friday work for a brief call?
Best,
[Rep Name]
---
Notes:
- Acknowledges their situation without being presumptuous
- Leads with value, not renewal ask
- Specific to their industry (manufacturing)
- Suggests efficiency rather than expansion
Should I adjust the tone or add anything specific?Query: “Create a summary of Q1 renewals for the leadership meeting”
Prospector: I'll pull the Q1 renewal data and generate a summary.
[Querying API: get_churn_risk_accounts(quarter="Q1-2025")]
[Querying API: get_portfolio_summary()]
## Q1 2025 Renewal Summary
**Portfolio at Risk: $1.2M ARR across 8 accounts**
| Risk Level | Count | ARR at Risk | % of Q1 Renewals |
|------------|-------|-------------|------------------|
| CRITICAL | 2 | $565K | 18% |
| HIGH | 3 | $420K | 14% |
| WATCH | 3 | $215K | 7% |
**Key Concerns:**
1. Two largest renewals (Acme, GlobalServices) both showing critical signals
2. Acquisition activity affecting 2 accounts. Need clarity on post-merger plans
3. IT layoffs at 3 accounts may indicate reduced technical advocacy
**Recommended Leadership Actions:**
- Executive sponsor calls for CRITICAL accounts this week
- Develop retention offer framework for budget-constrained accounts
- Accelerate renewal conversations for accounts with M&A uncertainty
Want me to generate a slide deck from this data?Multi-Agent Collaboration
The power of BrainTrust is that Prospector doesn’t work alone. The HostAgent can orchestrate multiple specialists:
The conversation server manages this orchestration transparently. The user just sees a helpful response with a link to their deck.
Where Users Actually Interact
BrainTrust integrates directly with Google Chat via webhook. Users can:
Start conversations with
@BrainTrustin any chatTag specific agents:
@BrainTrust ask Prospector what accounts need attentionContinue context across messages in a thread
The platform handles authentication, conversation state, and response streaming. Prospector feels like a team member who happens to have instant access to all portfolio data.
Where We Are Now
Full disclosure: I’m writing this article the week we started demonstrating the system. I don’t have six months of production metrics to share. What I have is a working pipeline, a functional agent, and a sales team that’s genuinely excited to use it.
What’s Built:
Enrichment pipeline running daily against 600+ accounts
Risk scoring generating evidence-based flags
FastAPI backend exposing all intelligence via REST
Prospector agent answering natural language queries in Google Chat
Action management workflow for human-in-the-loop approvals
What’s Happening This Week:
Tomorrow I’m meeting with the sales team to walk through the system end-to-end. We’ll look at real enrichment data, review the risk flags it’s generating, and discuss what’s working vs. what needs adjustment.
The skeleton is there. But sales intelligence isn’t something you build in isolation. The risk scoring weights? Those need validation from people who actually know which signals predict churn. The action recommendations? Those need input from reps who understand what “reach out” actually means in context. The enrichment fields? Those might need additions I haven’t thought of.
Early Signals:
The team’s reaction so far has been encouraging. When I demoed the agent answering questions about portfolio risk, the response wasn’t “that’s cool” but “when can I start using this?” That’s the reaction you want.
The real test comes when it’s in daily use. Does the risk scoring surface actual problems? Do the action recommendations feel useful or noisy? Does the agent save time or create friction? Those answers come from usage, not demos.
What I’m Watching:
Which risk signals correlate with actual renewal outcomes
How often AI-suggested actions get approved vs. modified vs. rejected
What questions the team asks that the system can’t answer yet
Where the enrichment data is wrong or incomplete
I’ll follow up with real results once we have them. For now, this is a story about building the thing, not measuring its impact.
The Human-AI Handoff Model
The key design decision throughout this project: AI recommends, humans decide.
Prospector doesn’t send emails without approval. It doesn’t mark actions complete without confirmation. It doesn’t update customer records with speculative information. Every output is a draft, a suggestion, a data point for human judgment.
The workflow:
AI identifies opportunity or risk (based on evidence)
AI researches context and drafts response
Human reviews, adjusts, approves
AI executes and tracks completion
Both learn from outcome
This creates a feedback loop where the AI gets better at context (we track which suggestions get approved vs. modified) and the human stays focused on relationship work.
What AI handles well:
Data gathering at scale
Pattern detection across portfolio
Draft generation with consistent structure
Task tracking and reminders
Presentation assembly
What humans handle better:
Reading between the lines of a negotiation
Navigating procurement politics
Deciding when to push vs. accommodate
Building genuine relationships
Handling exceptions and edge cases
The 80/20 rule applies: AI handles 80% of the information processing so humans can focus 80% of their time on the 20% that actually requires human judgment.
What’s Next
Immediate (Next 30 Days):
Incorporate feedback from sales team on risk signal relevance
Tune scoring weights based on their domain knowledge
Add any enrichment fields they’re missing
Track which action recommendations get approved vs. ignored
Near-Term (Q1):
Churn probability scoring using renewal countdown + risk trends
Expansion likelihood based on company growth signals
Calendar integration for automated meeting prep
Support ticket sentiment as a leading indicator
Future Exploration:
LinkedIn hiring signals for stakeholder changes
Closed-loop learning: track prediction accuracy and tune weights
CRM sync for teams using Salesforce alongside Notion
The Bottom Line
AI can’t close deals for you, but it can make sure you’re having the right conversations with the right people at the right time.
The technical stack (Firecrawl + FastAPI + MCP + BrainTrust) is less important than the design principles:
Augment existing workflows. Don’t ask people to change how they work
Evidence over sentiment. Actionable signals require verifiable events
Human-in-the-loop by default. AI recommends, humans approve
API-first architecture. Everything should be accessible to agents
The magic happens when AI amplifies human expertise instead of trying to replace it. Prospector doesn’t make sales calls, but it can ensure every sales call is informed by the best available intelligence.
That’s the difference between automation and intelligence augmentation.
This is a building-in-public story. I’ll follow up with real adoption data and lessons learned once we’ve had the system in production for a few months. Subscribe if you want to see how it actually plays out.
Questions about the architecture? Running into similar challenges with sales intelligence? Drop a comment. I’m curious how others are approaching this problem.











