Building an MCP-Powered Pipeline That Turns Static CRM Data Into Actionable Intelligence
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 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.
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.
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.

┌─────────────────────────────────────────────────────────────────┐
│ User Interface │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Notion │ │ BrainTrust │ │ FastAPI Swagger │ │
│ │ Database │ │ Chat Agent │ │ (Debug) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
└─────────┼──────────────────┼─────────────────────┼─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ FastAPI Backend │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ /actions │ │ /insights │ │ /enrichment │ │
│ │ endpoints │ │ endpoints │ │ endpoints │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Notion API │ │ Firecrawl │ │ Claude API │
│ (Data Layer) │ │ (Web Intel) │ │ (Reasoning) │
└─────────────────┘ └─────────────────┘ └─────────────────┘No data migration, no sync conflicts
New fields enrich existing records, never overwrite user data
Everything accessible via REST for agent integration
AI recommends, humans approve
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 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,
}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"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.
if sentiment == "negative":
risk = "HIGH"
# Too many false positivesif any(e.event_type == "major_layoffs" for e in events):
risk = "HIGH"
# Actionable, verifiable signalEach risk level maps to a specific recommended action:
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.

┌─────────────────────────────────────────────────────────────────────────────┐
│ AWS INFRASTRUCTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────────────────────────────────────┐ │
│ │ EventBridge │ │ EC2 (t3.small) │ │
│ │ (Daily 7am) │ │ sales-pipeline.inceptapi.com │ │
│ └──────┬───────┘ │ │ │
│ │ │ ┌────────────────────────────────────────┐ │ │
│ ▼ │ │ FastAPI Server (uvicorn) │ │ │
│ ┌──────────────┐ │ │ • /actions/* endpoints │ │ │
│ │ Orchestrator │ │ │ • /insights/* endpoints │ │ │
│ │ Lambda │ │ │ • /enrich endpoint (triggers Lambda) │ │ │
│ │ │ │ │ • OpenAPI spec for MCP │ │ │
│ │ Reads Notion │ │ └────────────────────────────────────────┘ │ │
│ │ Finds stale │ │ │ │
│ │ companies │ └──────────────────────────────────────────────┘ │
│ └──────┬───────┘ ▲ │
│ │ │ │
│ │ Invokes (batches of 10) Agent queries │
│ ▼ │ │
│ ┌──────────────┐ ┌──────┴───────┐ │
│ │ Worker │ │ BrainTrust │ │
│ │ Lambda │ ×N parallel │ Hive Agent │ │
│ │ │ └──────────────┘ │
│ │ • Firecrawl │ │
│ │ • Risk score │ │
│ │ • Notion sync│ │
│ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Secrets Manager: jay-portfolio-enrichment/secrets │ │
│ │ • NOTION_API_KEY, NOTION_DATABASE_ID, FIRECRAWL_API_KEY, API_KEY │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘The enrichment pipeline is a perfect Lambda use case:
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.
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.
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
}
}The FastAPI server runs on a t3.small EC2 instance. Why not Lambda?
The BrainTrust agent needs sub-second responses. Lambda cold starts add 2-3 seconds of latency.
The API maintains persistent connections to Notion. Lambda's stateless model would mean reconnecting on every request.
A t3.small runs ~$15/month. The equivalent Lambda invocations would cost roughly the same, but with worse latency.
The API server is deployed via a simple shell script on the EC2 instance:
# deploy-api.sh
#!/bin/bash
set -e
# Update and install Docker
sudo yum update -y
sudo yum install docker -y
sudo service docker start
sudo usermod -a -G docker ec2-user
# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Pull and run the latest Docker image
cd /home/ec2-user/api
docker-compose down # Stop existing services
docker-compose pull # Pull latest images
docker-compose up -d # Start new services in detached mode
echo "Deployment complete."The daily enrichment pipeline is triggered by an AWS EventBridge rule configured with a cron expression. This ensures the orchestrator Lambda runs reliably every morning.
# EventBridge cron configuration
Name: daily-enrichment-trigger
Description: Triggers the Orchestrator Lambda daily at 7 AM EST (12 PM UTC).
Schedule: cron(0 12 ? * MON-FRI *) # Monday to Friday, 12:00 UTC
Target: OrchestratorLambdaFunctionARN
Input: { "max_companies": 50, "stale_days": 7 }All sensitive API keys and credentials are securely stored and retrieved from AWS Secrets Manager.
# get_secrets() function
import boto3
import json
def get_secrets():
secret_name = "jay-portfolio-enrichment/secrets"
region_name = "us-east-1" # Or your specific region
# Create a Secrets Manager client
client = boto3.client(
service_name='secretsmanager',
region_name=region_name
)
try:
get_secret_value_response = client.get_secret_value(
SecretId=secret_name
)
except Exception as e:
# Handle exceptions, e.g., secret not found
raise e
# Decrypts secret using the associated KMS key.
secret = get_secret_value_response['SecretString']
return json.loads(secret)The worker Lambda function performs the core enrichment and risk scoring for each company, then synchronizes the results back to Notion.
# worker.py
import json
from firecrawl import Firecrawl
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from datetime import datetime
import os
# Assume RiskScorer and Notion syncing logic from previous card
# class CompanyInfo(...), CompanyNews(...), RiskScorer, sync_to_notion(...)
def handler(event: Dict, context: Any) -> Dict:
"""
Worker Lambda - performs enrichment, risk scoring, and Notion sync for a single company.
"""
# Load secrets (e.g., Firecrawl API key, Notion API key)
secrets = get_secrets() # Re-use the get_secrets() function
firecrawl_api_key = secrets['FIRECRAWL_API_KEY']
notion_api_key = secrets['NOTION_API_KEY']
firecrawl = Firecrawl(api_key=firecrawl_api_key)
risk_scorer = RiskScorer() # Initialize risk scorer
company_id = event['company_id']
company_name = event['company_name']
try:
# 1. Enrich company using Firecrawl (as shown in previous section)
info_result = firecrawl.agent(
prompt=f"Find current information about {company_name}: employee count, industry, company size, headquarters location.",
schema=CompanyInfo,
max_pages=3
)
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
)
enrichment_data = {
"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,
}
# 2. Convert news_result into CompanyEvent objects for risk scoring
# (This would involve parsing news_result.key_events and sources into a list of CompanyEvent instances)
# For brevity, let's assume a simplified event structure for demonstration
mock_events = []
if news_result.has_layoffs: mock_events.append({"event_type": "major_layoffs"})
if news_result.has_ma: mock_events.append({"event_type": "acquisition_mode"})
if news_result.is_expanding: mock_events.append({"event_type": "expansion_announced"})
risk_assessment = risk_scorer.calculate_risk(mock_events) # Use actual CompanyEvent objects here
# 3. Sync to Notion
sync_to_notion(
page_id=company_id,
enrichment=enrichment_data,
risk=risk_assessment.dict(), # Convert Pydantic model to dict
api_key=notion_api_key
)
return {
'statusCode': 200,
'body': json.dumps({
'company_id': company_id,
'status': 'success',
'risk_level': risk_assessment.level
})
}
except Exception as e:
print(f"Error enriching company {company_name} ({company_id}): {e}")
return {
'statusCode': 500,
'body': json.dumps({
'company_id': company_id,
'status': 'failed',
'error': str(e)
})
}Here's what this actually costs to run:
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 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.
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.
BrainTrust uses a concept called Hive: you upload an OpenAPI spec, and it automatically creates an MCP (Model Context Protocol) server. The AI agent can then call your API endpoints as tools, with full type safety and parameter validation.
┌─────────────────────────────────────────────────────────────────────────┐
│ USER INTERACTION LAYER │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │
│ │ Google Chat │────│ Hive Agent │────│ MCP Server (from OpenAPI)│ │
│ │ (User) │ │ (Prospector) │ │ sales-pipeline API │ │
│ └─────────────┘ └──────────────┘ └─────────────────────────┘ │
│ ↑ │
│ │ │
│ │ Other MCP Tools: │
│ │ - Gmail (send emails) │
│ │ - Google Docs (create docs) │
│ │ - Calendar (schedule calls) │
│ │ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ API LAYER (FastAPI) │
│ api_server.py - REST API on sales-pipeline.inceptapi.com │
│ │
│ Endpoints: │
│ • /actions/pending - Get actions needing attention │
│ • /actions/company/{n} - Get actions for specific company │
│ • /actions/{id}/approve - Approve action + track execution │
│ • /actions/{id}/deny - Deny action with reason │
│ • /insights/churn-risk - At-risk accounts by quarter │
│ • /insights/portfolio-summary - Executive overview │
│ • /enrich - Trigger AI enrichment │
└─────────────────────────────────────────────────────────────────────────┘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 ESW 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://sales-pipeline.inceptapi.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:
No LangChain. No custom tool definitions. No agent code at all.
When a user asks a question in Google Chat, here's the flow:
User (Google Chat): "Are there any actions to take?"
↓
Hive Agent: Calls getPendingActions via MCP
↓
API: Returns list of pending actions with reasoning
↓
Agent: "Yes, Acme Corp has a 'Schedule Call' action. Risk level is HIGH due to budget cuts mentioned in recent news. Would you like me to schedule a call?"
↓
User: "Yes, schedule it for next Tuesday"
↓
Agent: Uses Calendar MCP tool to schedule call
↓
Agent: Calls approveAction with execution_type="Call Scheduled"
↓
Notion: Action History updated, ready for next actionThe 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.
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 items
1.
- Calls getChurnRisk(quarter="Q1-2025") → 2 additional accounts
2.
- Synthesizes: "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?"
3.User: "Yes, draft something"
Prospector:
- Calls getActionsForCompany("Acme Corp") → Gets full context
1.
- Uses Gmail MCP to draft email with cost-optimization messaging
2.
- "Here's a draft email. I've highlighted the efficiency gains from consolidation. Send it?"
3.User: "Send it and mark the action done"
Prospector:
- Sends via Gmail MCP
1.
- Calls approveAction with execution_type="Email Sent", execution_link="<gmail link>"
2.
- "Done. Action marked complete. Next up is TechCorp..."
3.
The sales team isn't learning a new tool. They're having a conversation that happens to orchestrate five different systems.
Prospector: You have 4 pending actions across 3 priority levels:
1. CRITICAL: Acme Corp (churn risk)
- Action: Follow up on recent budget cuts.
- AI Reasoning: Firecrawl AI detected budget cut news; increased risk score.
2. HIGH: Innovate Solutions (expansion opportunity)
- Action: Prepare upsell proposal for new product line.
- AI Reasoning: LinkedIn Sales Navigator indicates new funding round and hiring surge.
3. WATCH: Global Dynamics (renewal upcoming)
- Action: Check in with account, ensure satisfaction.
- AI Reasoning: No recent activity or sentiment changes, proactive outreach recommended.
4. WATCH: Zenith Holdings (stalled deal)
- Action: Re-engage with decision-maker after recent acquisition news.
- AI Reasoning: Google Alerts shows acquisition, potential change in leadership.
Would you like to prioritize any of these, or should I draft a summary email for your manager?Prospector: Acme Corp is a CRITICAL churn risk account, with an ARR of $150K, and their renewal is in 45 days.
Key Risk Factors:
- Recent news of budget cuts (Firecrawl AI)
- Executive leadership changes (LinkedIn)
- Decreased product usage observed over last 2 months (internal data)
Last Action: Follow-up email sent on 2024-03-10 by John Doe.
Recommended Action: Schedule a call to discuss cost optimization and value alignment. Would you like me to draft a meeting invite?Prospector: Got it. Scheduling a 30-minute meeting with Acme Corp for next Tuesday (2024-03-26) at 2 PM PST. I'll include a placeholder agenda to discuss "Optimizing Your Investment with [Our Product]".
Confirm scheduling?The BrainTrust platform supports multiple agents. Prospector is specialized for sales intelligence, but it frequently collaborates with other internal agents to complete tasks.
This allows for complex, cross-functional workflows without manual coordination.
Integrating directly into Google Chat is crucial for adoption. Account managers spend their day in chat for quick, informal communication. By bringing the sales intelligence directly to them, we eliminate friction.
This approach maximizes utility and minimizes the learning curve, making the AI assistant an indispensable part of the sales workflow.
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.
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.
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.
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.
(based on evidence)
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.
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.
The technical stack (Firecrawl + FastAPI + MCP + BrainTrust) is less important than the design principles:
Don't ask people to change how they work
Actionable signals require verifiable events
AI recommends, humans approve
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.
From Notion Database to Sales Intelligence Agent