Skip to Content

Alerts react to things that already happened. Predictions anticipate things that will happen. The deal prediction system classifies open deals into three risk categories, runs nightly, and surfaces recommendations before problems become crises.

Three Prediction Types

// src/domain/predictions/dealPredictionService.ts // 1. slip_risk: Deal will miss its close date // 2. loss_risk: Deal matches patterns of past losses // 3. champion_cold: Key contact has gone silent or is missing

Slip Risk

A deal is at slip risk when it’s stalling — the close date is approaching but nothing is happening:

// Slip risk triggers when: // close_date dimension ≥60 OR (stage_duration ≥60 AND activity_recency ≥60) if ( deal.dimensions.close_date >= 60 || (deal.dimensions.stage_duration >= 60 && deal.dimensions.activity_recency >= 60) ) { const confidence = Math.round( deal.dimensions.close_date * 0.6 + deal.dimensions.stage_duration * 0.4 ); predictions.push({ predictionType: 'slip_risk', confidence: Math.min(100, Math.max(0, confidence)), predictedDate: computePredictedSlipDate(deal, stageBaselines), recommendedAction: 'Review timeline and re-engage stakeholders', }); }

Close date dimension >= 60 means the close date is within 2 weeks or already passed. Stage duration >= 60 means the deal has been in its current stage longer than average for that stage. Activity recency >= 60 means no activity in 2+ weeks.

The confidence formula weights close date (60%) more than stage duration (40%) because a looming close date is a stronger predictor of slip than stage duration alone.

Loss Risk

A deal is at loss risk when it matches patterns from your historical losses:

// Loss risk triggers when: // risk_score ≥60 AND matching loss patterns const patternStrength = lossPatterns.get(deal.deal_id)?.strength || 0; if (deal.risk_score >= 60 && patternStrength > 0) { const confidence = Math.round( deal.risk_score * 0.6 + patternStrength * 0.4 ); predictions.push({ predictionType: 'loss_risk', confidence, recommendedAction: 'Analyze loss pattern and adjust strategy', actionParams: { risk_score: deal.risk_score, risk_tier: deal.risk_tier, similar_loss_count: Math.ceil(patternStrength / 25), }, }); }

Pattern strength measures how similar this deal’s characteristics are to your past losses. It considers industry, company size, deal value, stage duration, and contact coverage. A strength of 100 means “this deal looks exactly like deals you’ve lost.”

Champion Cold

A deal without an active champion is a deal without an advocate:

// Champion cold triggers when: // contact_coverage ≥40 OR no champions at all if (deal.dimensions.contact_coverage >= 40 || deal.champion_count === 0) { const confidence = Math.round( deal.dimensions.contact_coverage * 0.5 + deal.dimensions.activity_recency * 0.5 ); predictions.push({ predictionType: 'champion_cold', confidence, recommendedAction: 'Identify and engage executive champion', }); }

Contact coverage >= 40 means fewer than 60% of the buying committee is covered. Champion count = 0 is the worst case — no one inside the company is advocating for the deal. This prediction fires even for new deals where champions haven’t been identified yet.

The Risk Scoring Dimensions

Each deal is scored across six dimensions, each 0-100:

DimensionWhat It MeasuresHigh = Bad
stage_durationDays in current stage vs. baselineStalling
activity_recencyDays since last activityGoing cold
close_dateDays until close date (or past due)Slipping
signal_decayHow stale intent signals areLosing interest
contact_coverage% of buying committee coveredSingle-threaded
competitor_threatCompetitor presence on dealCompetitive risk

The overall risk score is a weighted combination:

Overall Risk Score
Risk = (Stage Ă— 25%) + (Activity Ă— 25%) + (Close Ă— 20%) + (Signal Ă— 10%) + (Coverage Ă— 10%) + (Competitor Ă— 10%)
ComponentWeightDescription
Stage Duration25%Days in current stage vs. baseline
Activity Recency25%Days since last activity
Close Date20%Days until close date or past due
Signal Decay10%How stale intent signals are
Contact Coverage10%% of buying committee covered
Competitor Threat10%Competitor presence on deal
risk_score = stage_duration x 0.25 + activity_recency x 0.25 + close_date x 0.20 + signal_decay x 0.10 + contact_coverage x 0.10 + competitor_threat x 0.10

Risk tiers:

  • CRITICAL (>=80): Immediate attention needed
  • HIGH (>=60): At risk, action recommended
  • MEDIUM (>=40): Monitor closely
  • LOW (under 40): On track

Nightly Processing

Predictions run nightly via cron with sophisticated timeout management:

// src/pages/api/cron/process-deal-predictions.ts const REQUEST_DEADLINE_MS = 25_000; // Total cron budget: 25 seconds const USER_TIMEOUT_MS = 20_000; // Per-user timeout: 20 seconds for (const { user_id: userId } of usersRes.rows) { if (Date.now() - startTime > REQUEST_DEADLINE_MS) { timedOut = true; break; } const count = await Promise.race([ processUserPredictions(userId), new Promise((_, reject) => setTimeout(() => reject(new Error(`User ${userId} timeout`)), USER_TIMEOUT_MS) ), ]); }

Promise.race is the timeout pattern — whichever resolves first wins. If processUserPredictions takes longer than 20 seconds, the timeout promise rejects and we move to the next user.

The REQUEST_DEADLINE_MS check prevents the cron from exceeding its overall time budget (AWS Lambda/Amplify has a 30-second limit for API routes).

Confidence History

Predictions aren’t one-shot. The confidence_history JSONB column tracks how confidence changes over time:

[ { "confidence": 45, "timestamp": "2024-03-15T00:00:00Z" }, { "confidence": 62, "timestamp": "2024-03-16T00:00:00Z" }, { "confidence": 71, "timestamp": "2024-03-17T00:00:00Z" } ]

A rising confidence trend means the situation is getting worse. A falling trend means the rep’s actions are working. The UI can show this as a sparkline.

Upsert Pattern

Predictions are upserted nightly. The same deal can have the same prediction type updated with new confidence:

INSERT INTO deal_predictions (user_id, deal_id, prediction_type, confidence, ...) VALUES ($1, $2, $3, $4, ...) ON CONFLICT (user_id, deal_id, prediction_type) DO UPDATE SET confidence = EXCLUDED.confidence, confidence_history = deal_predictions.confidence_history || $5::jsonb, updated_at = NOW()

The || operator appends the new confidence entry to the existing history array.

Key Takeaways

  1. Three prediction types catch different failure modes: slip (timing), loss (pattern matching), and champion cold (relationship gaps).

  2. Six risk dimensions combine into an overall risk score. Each dimension is 0-100.

  3. Nightly cron with timeout budgets processes all users without exceeding infrastructure limits.

  4. Confidence history tracks trends over time — rising confidence = worsening situation.

  5. Upsert with history append preserves the trend while updating the current score.

Next chapter: autonomous actions — the AI-drafted responses that turn predictions into action.

Last updated on