Back to The Times of Claw

Build a Custom Analytics App in DenchClaw

Build a custom analytics app in DenchClaw using the App Builder and DuckDB. Create live dashboards with Chart.js, D3, or plain JS on your own CRM data.

Mark Rachapoom
Mark Rachapoom
·8 min read
Build a Custom Analytics App in DenchClaw

Build a Custom Analytics App in DenchClaw

Most CRM analytics give you what the vendor decided to show you. DenchClaw gives you a platform to build whatever you actually need — using your own data, with full access to DuckDB, and no third-party BI tool required.

This guide walks through building a real analytics app from scratch using DenchClaw's App Builder. You don't need to know React or Node.js. You need HTML, JavaScript, and an idea of what you want to see.

What Is a Dench App?#

A Dench App is a folder ending in .dench.app/ containing:

  • A .dench.yaml manifest
  • An index.html entry point
  • Any additional CSS, JS, or asset files

Every app gets window.dench auto-injected — a JavaScript bridge API that lets you query DuckDB, talk to the AI, and access workspace data. No backend code required.

Apps appear in your DenchClaw sidebar and open as tabs.

What You're Building#

A pipeline analytics dashboard with:

  • Total pipeline value (KPI card)
  • Deals by stage (bar chart)
  • Win rate over time (line chart)
  • Top deals table
  • Revenue forecast by month

Step 1: Ask the AI to Build It#

The fastest path is to just describe what you want:

Build a pipeline analytics app that shows:
1. Total open pipeline value (big number, green)
2. Deals by stage as a horizontal bar chart
3. Monthly closed won revenue for the last 6 months as a line chart
4. A table of the top 10 open deals by value with columns: Deal Name, Company, Value, Stage, Close Date
5. Weighted pipeline forecast by close month
Use Chart.js for the charts. Auto-refresh every 5 minutes.

DenchClaw creates the .dench.app folder, writes the HTML/JS, and adds it to your sidebar. Open it and you have a live dashboard.

If the AI-built version doesn't match your vision, the manual path gives you full control.

Step 2: Manual Build — Create the App Folder#

Navigate to your workspace:

mkdir ~/.openclaw-dench/workspace/apps/pipeline-analytics.dench.app

Create the manifest:

# .dench.yaml
name: Pipeline Analytics
icon: bar-chart
version: 1.0.0
description: Sales pipeline analytics dashboard
permissions:
  - db:read
  - workspace:read
display: tab
refresh_interval: 300000

Step 3: Build the Dashboard HTML#

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pipeline Analytics</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, sans-serif; background: #0f0f0f; color: #fff; padding: 24px; }
    .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
    .kpi { background: #1a1a1a; border-radius: 12px; padding: 20px; }
    .kpi-label { font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
    .kpi-value { font-size: 32px; font-weight: 700; color: #22c55e; margin-top: 4px; }
    .chart-card { background: #1a1a1a; border-radius: 12px; padding: 20px; }
    .chart-card h3 { font-size: 14px; color: #aaa; margin-bottom: 16px; }
    .charts-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; }
    table { width: 100%; border-collapse: collapse; }
    th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #2a2a2a; font-size: 13px; }
    th { color: #888; font-weight: 500; font-size: 11px; text-transform: uppercase; }
    .stage-badge { padding: 2px 8px; border-radius: 4px; font-size: 11px; }
  </style>
</head>
<body>
  <h2 style="margin-bottom: 20px; font-size: 18px;">Pipeline Analytics</h2>
  
  <div class="grid">
    <div class="kpi">
      <div class="kpi-label">Open Pipeline</div>
      <div class="kpi-value" id="open-pipeline">—</div>
    </div>
    <div class="kpi">
      <div class="kpi-label">Deals This Month</div>
      <div class="kpi-value" id="deals-month" style="color: #3b82f6;">—</div>
    </div>
    <div class="kpi">
      <div class="kpi-label">Win Rate (90d)</div>
      <div class="kpi-value" id="win-rate" style="color: #f59e0b;">—</div>
    </div>
  </div>
 
  <div class="charts-row">
    <div class="chart-card">
      <h3>Deals by Stage</h3>
      <canvas id="stageChart" height="200"></canvas>
    </div>
    <div class="chart-card">
      <h3>Monthly Revenue (Closed Won)</h3>
      <canvas id="revenueChart" height="200"></canvas>
    </div>
  </div>
 
  <div class="chart-card">
    <h3>Top Open Deals</h3>
    <table>
      <thead>
        <tr><th>Deal</th><th>Company</th><th>Value</th><th>Stage</th><th>Close Date</th></tr>
      </thead>
      <tbody id="deals-table"></tbody>
    </table>
  </div>
 
  <script>
    const fmt = (n) => '$' + (n / 1000).toFixed(0) + 'k';
 
    async function loadData() {
      // KPIs
      const pipeline = await dench.db.query(`
        SELECT COALESCE(SUM(CAST("Value" AS DOUBLE)), 0) as total
        FROM v_deals
        WHERE "Stage" NOT IN ('Closed Won', 'Closed Lost')
      `);
      document.getElementById('open-pipeline').textContent = fmt(pipeline[0]?.total || 0);
 
      const monthDeals = await dench.db.query(`
        SELECT COUNT(*) as count FROM v_deals
        WHERE "Stage" = 'Closed Won'
        AND CAST("Close Date" AS DATE) >= CURRENT_DATE - INTERVAL 30 DAYS
      `);
      document.getElementById('deals-month').textContent = monthDeals[0]?.count || 0;
 
      const winRate = await dench.db.query(`
        SELECT 
          ROUND(100.0 * SUM(CASE WHEN "Stage" = 'Closed Won' THEN 1 ELSE 0 END) / 
          NULLIF(SUM(CASE WHEN "Stage" IN ('Closed Won', 'Closed Lost') THEN 1 ELSE 0 END), 0), 1) as rate
        FROM v_deals
        WHERE CAST("Close Date" AS DATE) >= CURRENT_DATE - INTERVAL 90 DAYS
      `);
      document.getElementById('win-rate').textContent = (winRate[0]?.rate || 0) + '%';
 
      // Stage chart
      const stages = await dench.db.query(`
        SELECT "Stage", COUNT(*) as count, SUM(CAST("Value" AS DOUBLE)) as total
        FROM v_deals WHERE "Stage" NOT IN ('Closed Won', 'Closed Lost')
        GROUP BY "Stage" ORDER BY total DESC
      `);
      new Chart(document.getElementById('stageChart'), {
        type: 'bar',
        data: {
          labels: stages.map(s => s.Stage),
          datasets: [{ label: 'Value', data: stages.map(s => s.total || 0), backgroundColor: '#3b82f6' }]
        },
        options: { responsive: true, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#888' } }, y: { ticks: { color: '#888', callback: fmt } } } }
      });
 
      // Revenue chart
      const revenue = await dench.db.query(`
        SELECT strftime(CAST("Close Date" AS DATE), '%Y-%m') as month,
               SUM(CAST("Value" AS DOUBLE)) as total
        FROM v_deals WHERE "Stage" = 'Closed Won'
        AND CAST("Close Date" AS DATE) >= CURRENT_DATE - INTERVAL 6 MONTHS
        GROUP BY month ORDER BY month
      `);
      new Chart(document.getElementById('revenueChart'), {
        type: 'line',
        data: {
          labels: revenue.map(r => r.month),
          datasets: [{ label: 'Revenue', data: revenue.map(r => r.total || 0), borderColor: '#22c55e', tension: 0.3, fill: true, backgroundColor: 'rgba(34,197,94,0.1)' }]
        },
        options: { responsive: true, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#888' } }, y: { ticks: { color: '#888', callback: fmt } } } }
      });
 
      // Top deals
      const deals = await dench.db.query(`
        SELECT "Deal Name", "Company", "Value", "Stage", "Close Date"
        FROM v_deals WHERE "Stage" NOT IN ('Closed Won', 'Closed Lost')
        ORDER BY CAST("Value" AS DOUBLE) DESC LIMIT 10
      `);
      const tbody = document.getElementById('deals-table');
      tbody.innerHTML = deals.map(d => `
        <tr>
          <td>${d['Deal Name'] || '—'}</td>
          <td>${d['Company'] || '—'}</td>
          <td>${fmt(d['Value'] || 0)}</td>
          <td><span class="stage-badge">${d['Stage'] || '—'}</span></td>
          <td>${d['Close Date'] || '—'}</td>
        </tr>
      `).join('');
    }
 
    loadData();
    setInterval(loadData, 300000);
  </script>
</body>
</html>

Step 4: Register the App#

Tell DenchClaw:

Register the pipeline-analytics app from my workspace apps folder

Or restart DenchClaw — it auto-discovers .dench.app folders on startup.

The app appears in your sidebar with the bar-chart icon. Click to open.

Customizing Your Dashboard#

Change the chart colors: Edit the backgroundColor and borderColor values in the Chart.js config.

Add new charts: Use dench.db.query() with any SQL query against your DuckDB. The pivot views (v_deals, v_people, v_companies) are available by default.

Add filters: Add an <input> or <select> element and pass its value into your SQL query with a parameterized template string.

Add AI summaries:

const summary = await dench.chat.oneShot(
  `Summarize the current pipeline status: ${JSON.stringify(stages)}. Highlight any concerns.`
);
document.getElementById('ai-summary').textContent = summary;

Widget Mode#

For at-a-glance metrics on a dashboard:

# .dench.yaml
display: widget
widget_width: 2
widget_height: 1
refresh_interval: 60000

The app renders as a compact card in your workspace dashboard, showing KPIs and auto-refreshing every minute.

See what DenchClaw is for the full App Builder documentation, or check the custom reporting dashboard guide for more report patterns.

Frequently Asked Questions#

Do I need to know React or a framework to build Dench Apps?#

No. Dench Apps are plain HTML/CSS/JS. You can use any CDN-hosted library (Chart.js, D3, Plotly, etc.) but you don't need a build system or framework.

Can I use my own external APIs in a Dench App?#

Yes. Use dench.http.fetch() to make cross-origin requests from your app without CORS issues. Dench proxies the request server-side.

How do I share a Dench App with my team?#

Export the .dench.app folder and share it. They place it in their apps/ directory and it appears in their DenchClaw instance. Or publish it to clawhub.ai.

How does the dench.db.query() bridge access DuckDB?#

The bridge API routes through the DenchClaw backend, which has direct access to workspace.duckdb. Queries run server-side and results are returned as JSON arrays.

Can I access real-time data in a Dench App?#

Yes. Use dench.events.on('entry:created', callback) to subscribe to CRM events. Combined with setInterval(), your app can stay current without manual refresh.

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