Skip to main content

Build a Revenue Operations Dashboard with Claude Code [2026]

· 8 min read
sunder
Founder, marketbetter.ai

Your CRO asks: "Are we going to hit the number this month?"

You spend 4 hours pulling data from HubSpot, cross-referencing with finance, adjusting for pipeline weighting, and building a slide deck. By the time you present it, the data is 3 days old.

This is RevOps in 2025. It doesn't have to be RevOps in 2026.

With Claude Code, you can build a real-time revenue dashboard that:

  • Pulls live data from CRM, marketing, and finance systems
  • Automatically weights pipeline by historical close rates
  • Surfaces risks before they become surprises
  • Updates continuously, not monthly

Here's exactly how to build it.

RevOps dashboard architecture

Why Your Current Dashboards Fail

Problem 1: Data silos Pipeline is in HubSpot. Bookings are in the finance system. Marketing attribution is in Marketo. Usage data is in the product. Getting a complete picture requires manual assembly.

Problem 2: Stale data Weekly pipeline reviews use data that's already a week old. Monthly board decks are historical artifacts by the time they're presented.

Problem 3: No intelligence Dashboards show what happened, not what's likely to happen. They can't answer "should I be worried about this deal?"

Problem 4: Too many dashboards HubSpot has reports. Tableau has dashboards. Looker has boards. Nobody knows which one is "the truth."

The RevOps Dashboard Architecture

Here's what we're building:

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│ HubSpot │ │ Stripe │ │ Marketo │
│ (CRM) │ │ (Revenue) │ │ (Marketing) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────┐
│ Data Normalization Layer │
│ (Claude Code - ETL + Enrichment) │
└───────────────────────┬────────────────────────────┘


┌────────────────────────────────────────────────────┐
│ Analytics Engine │
│ - Pipeline weighting │
│ - Forecasting models │
│ - Risk scoring │
│ - Attribution analysis │
└───────────────────────┬────────────────────────────┘


┌────────────────────────────────────────────────────┐
│ Executive Dashboard │
│ - Real-time metrics │
│ - Drill-down capability │
│ - AI-generated insights │
└────────────────────────────────────────────────────┘

Step 1: Data Integration with Claude Code

Let Claude Code build your data pipeline:

claude "Create a TypeScript module that:
1. Pulls deal data from HubSpot (all deals, all stages, with history)
2. Pulls revenue data from Stripe (MRR, ARR, churn, expansion)
3. Pulls campaign data from HubSpot Marketing (attribution, source)
4. Normalizes dates and amounts to consistent formats
5. Handles pagination and rate limiting
6. Stores in PostgreSQL with proper indexing

Use environment variables for API keys. Include comprehensive error handling."

Claude's 200K context window means you can provide full API documentation and get back production-ready code—not snippets that need assembly.

Sample Data Normalization

// data-pipeline.ts - Claude Code generated

interface UnifiedDeal {
id: string;
name: string;
company: string;
amount: number;
currency: 'USD';
stage: string;
stageHistory: StageChange[];
probability: number;
owner: User;
source: MarketingSource;
closeDate: Date;
createdAt: Date;
daysInStage: number;
lastActivity: Date;
contacts: Contact[];
}

const normalizeDeal = (hubspotDeal: HubSpotDeal): UnifiedDeal => {
return {
id: hubspotDeal.id,
name: hubspotDeal.properties.dealname,
company: hubspotDeal.associations?.companies?.[0]?.name || 'Unknown',
amount: normalizeAmount(
hubspotDeal.properties.amount,
hubspotDeal.properties.deal_currency_code
),
currency: 'USD',
stage: mapStage(hubspotDeal.properties.dealstage),
stageHistory: parseStageHistory(hubspotDeal),
probability: calculateProbability(hubspotDeal),
owner: await getOwner(hubspotDeal.properties.hubspot_owner_id),
source: await getAttribution(hubspotDeal),
closeDate: new Date(hubspotDeal.properties.closedate),
createdAt: new Date(hubspotDeal.properties.createdate),
daysInStage: calculateDaysInStage(hubspotDeal),
lastActivity: await getLastActivity(hubspotDeal.id),
contacts: await getContacts(hubspotDeal.id)
};
};

Step 2: Intelligent Pipeline Weighting

Raw pipeline is a fantasy number. Weighted pipeline predicts reality:

// pipeline-weighting.ts

interface WeightingModel {
byStage: Record<string, number>; // Historical close rates by stage
byAgeInStage: (stage: string, days: number) => number; // Decay factor
bySource: Record<string, number>; // Source quality multiplier
byDealSize: (amount: number) => number; // Large deal discount
}

const buildWeightingModel = async (): Promise<WeightingModel> => {
// Analyze last 12 months of closed deals
const closedWon = await getClosedWonDeals('12m');
const closedLost = await getClosedLostDeals('12m');

// Calculate actual close rates by stage
const byStage = calculateStageConversion(closedWon, closedLost);
// Example result:
// { 'Discovery': 0.12, 'Demo': 0.34, 'Proposal': 0.62, 'Negotiation': 0.78 }

// Calculate age decay
const byAgeInStage = buildDecayFunction(closedWon, closedLost);
// Example: Deals 2x average time in stage close at 40% the rate

// Calculate source quality
const bySource = calculateSourceQuality(closedWon);
// Example: { 'Inbound': 1.2, 'Outbound': 0.8, 'Partner': 1.1, 'Event': 0.7 }

// Calculate deal size impact
const byDealSize = buildSizeFunction(closedWon, closedLost);
// Example: Deals >$100K close at 70% the rate of average deals

return { byStage, byAgeInStage, bySource, byDealSize };
};

const weightDeal = (deal: UnifiedDeal, model: WeightingModel): number => {
const baseWeight = model.byStage[deal.stage] || 0.1;
const ageMultiplier = model.byAgeInStage(deal.stage, deal.daysInStage);
const sourceMultiplier = model.bySource[deal.source.channel] || 1;
const sizeMultiplier = model.byDealSize(deal.amount);

return deal.amount * baseWeight * ageMultiplier * sourceMultiplier * sizeMultiplier;
};

Step 3: Risk Scoring

Identify deals that need attention before they slip:

// risk-scoring.ts

interface DealRisk {
dealId: string;
score: number; // 0-100, higher = more risk
factors: RiskFactor[];
recommendation: string;
}

const calculateDealRisk = async (deal: UnifiedDeal): Promise<DealRisk> => {
const factors: RiskFactor[] = [];
let score = 0;

// Time in stage risk
const avgTimeInStage = await getAverageTimeInStage(deal.stage);
if (deal.daysInStage > avgTimeInStage * 1.5) {
score += 25;
factors.push({
type: 'stale',
description: `${deal.daysInStage} days in ${deal.stage} (avg: ${avgTimeInStage})`,
severity: 'high'
});
}

// Activity recency risk
const daysSinceActivity = daysBetween(deal.lastActivity, new Date());
if (daysSinceActivity > 14) {
score += 20;
factors.push({
type: 'inactive',
description: `No activity in ${daysSinceActivity} days`,
severity: 'medium'
});
}

// Close date risk
const daysToClose = daysBetween(new Date(), deal.closeDate);
if (daysToClose < 14 && deal.stage !== 'Negotiation') {
score += 30;
factors.push({
type: 'unrealistic_date',
description: `Closing in ${daysToClose} days but still in ${deal.stage}`,
severity: 'high'
});
}

// Champion identified
const hasChampion = deal.contacts.some(c => c.role === 'Champion');
if (!hasChampion && deal.amount > 50000) {
score += 15;
factors.push({
type: 'no_champion',
description: 'No champion identified on $50K+ deal',
severity: 'medium'
});
}

// Generate recommendation
const recommendation = await generateRecommendation(deal, factors);

return { dealId: deal.id, score, factors, recommendation };
};

Step 4: The Dashboard

Build a clean, executive-ready interface:

// dashboard.ts

interface ExecutiveDashboard {
// The Numbers
currentMonth: {
target: number;
closed: number;
committed: number; // High-confidence pipeline
bestCase: number; // Weighted pipeline
gap: number;
};

// Pipeline Health
pipeline: {
total: number;
weighted: number;
byStage: Record<string, { count: number; value: number; weighted: number }>;
created: { thisMonth: number; lastMonth: number; change: number };
velocity: { avgDaysToClose: number; trend: 'faster' | 'slower' | 'stable' };
};

// Risks & Opportunities
risks: DealRisk[];
stuckDeals: UnifiedDeal[];
pushedDeals: UnifiedDeal[]; // Close date moved out
bigMoves: UnifiedDeal[]; // Stage changed this week

// Trends
trends: {
winRate: { current: number; previous: number; change: number };
avgDealSize: { current: number; previous: number; change: number };
salesCycle: { current: number; previous: number; change: number };
};

// AI Insights
insights: string[];
}

const buildDashboard = async (): Promise<ExecutiveDashboard> => {
const deals = await getAllDeals();
const model = await buildWeightingModel();

// Calculate metrics
const currentMonth = await calculateMonthMetrics(deals, model);
const pipeline = await analyzePipeline(deals, model);
const risks = await Promise.all(
deals
.filter(d => d.stage !== 'Closed Won' && d.stage !== 'Closed Lost')
.map(d => calculateDealRisk(d))
);

// Generate AI insights
const insights = await generateInsights({
currentMonth,
pipeline,
risks: risks.filter(r => r.score > 50)
});

return {
currentMonth,
pipeline,
risks: risks.sort((a, b) => b.score - a.score).slice(0, 10),
stuckDeals: deals.filter(d => isStuck(d)).slice(0, 5),
pushedDeals: await getPushedDeals('7d'),
bigMoves: await getBigMoves('7d'),
trends: await calculateTrends(),
insights
};
};

RevOps dashboard data flow

Step 5: AI-Generated Insights

The dashboard doesn't just show data—it explains it:

// insights.ts

const generateInsights = async (data: DashboardData): Promise<string[]> => {
const prompt = `
You are a RevOps analyst. Based on this data, provide 3-5 key insights
that would be valuable for a CRO in their Monday morning review.

Be specific. Use numbers. Flag concerns. Highlight wins.

Data:
- Target: $${data.currentMonth.target.toLocaleString()}
- Closed: $${data.currentMonth.closed.toLocaleString()} (${(data.currentMonth.closed / data.currentMonth.target * 100).toFixed(0)}% of target)
- Committed: $${data.currentMonth.committed.toLocaleString()}
- High-risk deals: ${data.risks.filter(r => r.score > 70).length}
- Deals pushed this week: ${data.pushedDeals.length}
- Win rate trend: ${data.trends.winRate.change > 0 ? 'up' : 'down'} ${Math.abs(data.trends.winRate.change)}%

Format as bullet points, no headers.
`;

const response = await claude.complete(prompt);
return response.split('\n').filter(line => line.startsWith('-') || line.startsWith('•'));
};

Example output:

On track but tight: At 68% of target with 12 days left. Need $127K from committed pipeline that's currently weighted at $143K. No buffer.

Acme Corp is the swing deal: $85K deal in Negotiation, but 23 days in stage (avg: 11). Risk score 78. Recommend: exec-to-exec call before Friday.

Win rate improving: 34% this month vs 28% last month. Driven by better qualification—MQL rejection rate up 15%.

Pipeline generation concern: Only $340K created this month vs $520K target. Marketing sourced leads down 22%. Check campaign performance.

3 deals pushed close dates this week totaling $124K. Pattern: all had unrealistic close dates set during initial discovery. Consider discovery checklist update.

Automated Distribution

Push insights where people actually look:

// distribution.ts

// Monday morning CRO briefing
const mondayBriefing = async () => {
const dashboard = await buildDashboard();

await slack.postMessage({
channel: '#revenue-leadership',
blocks: [
{
type: 'header',
text: { type: 'plain_text', text: '📊 Monday Revenue Brief' }
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Closed:* $${dashboard.currentMonth.closed.toLocaleString()}` },
{ type: 'mrkdwn', text: `*Target:* $${dashboard.currentMonth.target.toLocaleString()}` },
{ type: 'mrkdwn', text: `*Committed:* $${dashboard.currentMonth.committed.toLocaleString()}` },
{ type: 'mrkdwn', text: `*Gap:* $${dashboard.currentMonth.gap.toLocaleString()}` }
]
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*AI Insights:*\n${dashboard.insights.join('\n')}`
}
}
]
});
};

// Daily risk alerts
const dailyRiskAlerts = async () => {
const dashboard = await buildDashboard();
const criticalRisks = dashboard.risks.filter(r => r.score > 70);

if (criticalRisks.length > 0) {
for (const risk of criticalRisks) {
const deal = await getDeal(risk.dealId);

await slack.postMessage({
channel: deal.owner.slackId,
text: `⚠️ Risk alert on *${deal.name}* ($${deal.amount.toLocaleString()})\n\n${risk.factors.map(f => `${f.description}`).join('\n')}\n\n*Recommendation:* ${risk.recommendation}`
});
}
}
};

Implementation Timeline

Week 1: Data Layer

  • Set up PostgreSQL database
  • Build HubSpot integration
  • Build Stripe integration (if applicable)
  • Create normalization layer

Week 2: Analytics

  • Implement weighting model from historical data
  • Build risk scoring
  • Create trend calculations

Week 3: Dashboard

  • Build dashboard API
  • Create frontend (or use existing BI tool)
  • Integrate AI insights

Week 4: Distribution

  • Set up Slack integrations
  • Configure automated briefings
  • Train team on using the dashboard

The ROI

MetricBeforeAfter
Time to answer "how are we doing?"4 hours10 seconds
Pipeline accuracy60%85%
Deals slipping unnoticed5-7/month0-1/month
Forecast variance±25%±10%

What's Next?

Once your RevOps dashboard is running:

  1. Add product usage data — See which customers are actually using what they bought
  2. Build scenario modeling — "What if we slip these 3 deals?"
  3. Connect to capacity planning — Does pipeline support hiring plan?
  4. Enable board reporting — Auto-generate monthly investor updates

Ready to stop guessing and start knowing? Book a demo to see how MarketBetter combines real-time intelligence with AI-powered workflows.

Related reading: