Back to The Times of Claw

Build a Deal Value Calculator App

Build a deal value calculator app in DenchClaw that computes weighted pipeline value, win probability, and revenue forecasts from your CRM data.

Mark Rachapoom
Mark Rachapoom
·6 min read
Build a Deal Value Calculator App

Build a Deal Value Calculator App

Most sales teams track deal value but not expected deal value. A $100k deal at 20% probability is worth $20k in your forecast — very different from a $20k deal at 90% probability worth $18k. Knowing the difference changes how you prioritize your week.

This guide builds a deal value calculator as a Dench App: an interactive tool that loads your deals from DuckDB, lets you adjust probability estimates, and calculates weighted pipeline value with a forecast breakdown.

What You're Building#

  • Interactive table of open deals with editable probability sliders
  • Real-time weighted pipeline total
  • Monthly forecast breakdown (by close date)
  • Win-rate analysis by deal source or owner
  • Save probability estimates back to DenchClaw

Step 1: App Setup#

mkdir -p ~/.openclaw-dench/workspace/apps/deal-calculator.dench.app

.dench.yaml:

name: Deal Calculator
description: Weighted pipeline value and revenue forecast calculator
icon: calculator
version: 1.0.0
permissions:
  - read:crm
  - write:crm
display: tab

Step 2: HTML Layout#

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Deal Calculator</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 20px; }
    .metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
    .metric-card { background: #1e293b; border-radius: 12px; padding: 20px; }
    .metric-label { font-size: 12px; color: #64748b; text-transform: uppercase; margin-bottom: 4px; }
    .metric-value { font-size: 28px; font-weight: 800; }
    .metric-sub { font-size: 12px; color: #64748b; margin-top: 4px; }
    table { width: 100%; border-collapse: collapse; background: #1e293b; border-radius: 12px; overflow: hidden; }
    th { padding: 12px 16px; text-align: left; font-size: 11px; color: #64748b; text-transform: uppercase; background: #0f172a; }
    td { padding: 12px 16px; border-top: 1px solid #334155; font-size: 14px; }
    .slider { width: 100%; accent-color: #6366f1; }
    .prob-display { min-width: 40px; text-align: right; font-weight: 700; }
    .value-green { color: #10b981; }
    .value-yellow { color: #f59e0b; }
    .stage-badge { padding: 2px 8px; border-radius: 999px; font-size: 11px; background: #6366f120; color: #6366f1; }
    button { padding: 10px 20px; background: #6366f1; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; margin-top: 16px; }
    .forecast-section { margin-top: 24px; }
    .forecast-section h2 { font-size: 16px; margin-bottom: 16px; }
    .month-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; font-size: 13px; }
    .bar-fill { height: 24px; background: #6366f1; border-radius: 4px; min-width: 2px; transition: width 0.3s; }
    .bar-label { min-width: 80px; color: #94a3b8; }
    .bar-value { color: #e2e8f0; font-weight: 600; }
  </style>
</head>
<body>
  <div class="metrics">
    <div class="metric-card">
      <div class="metric-label">Total Pipeline</div>
      <div class="metric-value" id="totalPipeline">$0</div>
      <div class="metric-sub">All open deals</div>
    </div>
    <div class="metric-card">
      <div class="metric-label">Weighted Value</div>
      <div class="metric-value value-green" id="weightedValue">$0</div>
      <div class="metric-sub">Probability adjusted</div>
    </div>
    <div class="metric-card">
      <div class="metric-label">Deals</div>
      <div class="metric-value" id="dealCount">0</div>
      <div class="metric-sub">Open opportunities</div>
    </div>
    <div class="metric-card">
      <div class="metric-label">Avg. Probability</div>
      <div class="metric-value value-yellow" id="avgProbability">0%</div>
      <div class="metric-sub">Across all deals</div>
    </div>
  </div>
  <table>
    <thead>
      <tr>
        <th>Deal / Company</th>
        <th>Stage</th>
        <th>Deal Value</th>
        <th>Probability</th>
        <th>Weighted Value</th>
        <th>Close Date</th>
      </tr>
    </thead>
    <tbody id="dealsTable"></tbody>
  </table>
  <button onclick="saveProbabilities()">Save Probabilities to CRM</button>
  <div class="forecast-section">
    <h2>Monthly Forecast</h2>
    <div id="forecastBars"></div>
  </div>
  <script src="calculator.js"></script>
</body>
</html>

Step 3: Calculator Logic#

calculator.js:

// Default probabilities by stage (customize to match your process)
const STAGE_PROBABILITIES = {
  'Prospecting': 10,
  'Qualified': 25,
  'Proposal': 50,
  'Negotiation': 75,
  'Verbal Commit': 90,
  'Closed Won': 100,
  'Closed Lost': 0
};
 
let deals = [];
let localProbabilities = {};
 
async function loadDeals() {
  deals = await dench.db.query(`
    SELECT id, "Deal Name", "Company", "Value", "Stage", "Close Date", "Probability", "Owner"
    FROM v_deals
    WHERE "Stage" NOT IN ('Closed Won', 'Closed Lost')
    ORDER BY CAST("Value" AS DOUBLE) DESC NULLS LAST
  `);
 
  // Initialize probabilities from CRM or defaults
  deals.forEach(deal => {
    localProbabilities[deal.id] = deal.Probability
      ? Number(deal.Probability)
      : (STAGE_PROBABILITIES[deal.Stage] || 30);
  });
 
  renderTable();
  renderForecast();
}
 
function formatCurrency(val) {
  return '$' + Number(val || 0).toLocaleString('en-US', { maximumFractionDigits: 0 });
}
 
function renderTable() {
  const tbody = document.getElementById('dealsTable');
  tbody.innerHTML = deals.map(deal => {
    const value = Number(deal.Value || 0);
    const prob = localProbabilities[deal.id] || 0;
    const weighted = value * prob / 100;
 
    return `
      <tr>
        <td>
          <div style="font-weight:600">${deal['Deal Name'] || 'Unnamed'}</div>
          <div style="font-size:12px;color:#64748b">${deal.Company || '—'}</div>
        </td>
        <td><span class="stage-badge">${deal.Stage || '—'}</span></td>
        <td style="font-weight:700;color:#10b981">${formatCurrency(value)}</td>
        <td>
          <div style="display:flex;align-items:center;gap:8px">
            <input type="range" class="slider" min="0" max="100" value="${prob}"
                   oninput="updateProbability('${deal.id}', this.value)">
            <span class="prob-display" id="prob-${deal.id}">${prob}%</span>
          </div>
        </td>
        <td id="weighted-${deal.id}" style="font-weight:700;color:${prob >= 75 ? '#10b981' : prob >= 40 ? '#f59e0b' : '#ef4444'}">
          ${formatCurrency(weighted)}
        </td>
        <td style="color:#64748b">${deal['Close Date'] || '—'}</td>
      </tr>
    `;
  }).join('');
 
  updateMetrics();
}
 
function updateProbability(dealId, newProb) {
  localProbabilities[dealId] = Number(newProb);
  document.getElementById(`prob-${dealId}`).textContent = `${newProb}%`;
 
  const deal = deals.find(d => d.id === dealId);
  const weighted = Number(deal?.Value || 0) * Number(newProb) / 100;
  const weightedEl = document.getElementById(`weighted-${dealId}`);
  if (weightedEl) {
    weightedEl.textContent = formatCurrency(weighted);
    weightedEl.style.color = Number(newProb) >= 75 ? '#10b981' : Number(newProb) >= 40 ? '#f59e0b' : '#ef4444';
  }
 
  updateMetrics();
  renderForecast();
}
 
function updateMetrics() {
  const totalPipeline = deals.reduce((sum, d) => sum + Number(d.Value || 0), 0);
  const weightedTotal = deals.reduce((sum, d) => {
    return sum + (Number(d.Value || 0) * (localProbabilities[d.id] || 0) / 100);
  }, 0);
  const avgProb = Object.values(localProbabilities).reduce((s, p) => s + p, 0) / (deals.length || 1);
 
  document.getElementById('totalPipeline').textContent = formatCurrency(totalPipeline);
  document.getElementById('weightedValue').textContent = formatCurrency(weightedTotal);
  document.getElementById('dealCount').textContent = deals.length;
  document.getElementById('avgProbability').textContent = Math.round(avgProb) + '%';
}
 
function renderForecast() {
  const monthlyTotals = {};
  deals.forEach(deal => {
    const closeDate = deal['Close Date'];
    if (!closeDate) return;
    const month = closeDate.substring(0, 7); // YYYY-MM
    const weighted = Number(deal.Value || 0) * (localProbabilities[deal.id] || 0) / 100;
    monthlyTotals[month] = (monthlyTotals[month] || 0) + weighted;
  });
 
  const months = Object.keys(monthlyTotals).sort();
  const maxVal = Math.max(...Object.values(monthlyTotals), 1);
 
  document.getElementById('forecastBars').innerHTML = months.map(month => `
    <div class="month-bar">
      <span class="bar-label">${month}</span>
      <div class="bar-fill" style="width:${(monthlyTotals[month] / maxVal) * 400}px"></div>
      <span class="bar-value">${formatCurrency(monthlyTotals[month])}</span>
    </div>
  `).join('') || '<p style="color:#64748b">No deals with close dates set.</p>';
}
 
async function saveProbabilities() {
  const updates = Object.entries(localProbabilities);
  for (const [dealId, prob] of updates) {
    await dench.agent.run(`Update deal ${dealId}: set Probability to ${prob}`);
  }
  await dench.ui.toast({ message: `Saved ${updates.length} probability estimates`, type: 'success' });
}
 
loadDeals();

Frequently Asked Questions#

How do I set default probabilities for my stages?#

Edit the STAGE_PROBABILITIES object at the top of calculator.js. Map each of your stage names to a percentage. These are used as defaults when a deal doesn't have an explicit probability set.

Can I filter by owner or time period?#

Add a filter bar with owner and date range selects. Pass the filters as WHERE clauses in your DuckDB query: WHERE "Owner" = 'Sarah' AND "Close Date" >= '2026-01-01'.

How do I add this as a widget on my DenchClaw home screen?#

Change display: tab to display: widget in .dench.yaml and add widget dimensions. In widget mode, show just the three key metrics (total, weighted, avg probability) without the full table.

What's the difference between this and DenchClaw's built-in reports?#

The built-in reports use static SQL queries. This app lets you interactively adjust probability estimates without changing your CRM data, then optionally save those estimates back. It's a "what-if" calculator combined with a write-back tool.

Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →

Mark Rachapoom

Written by

Mark Rachapoom

Building the future of AI CRM software.

Continue reading

DENCH

© 2026 DenchHQ · San Francisco, CA