Back to The Times of Claw

Build a Proposal Generator App in DenchClaw

Build a proposal generator app in DenchClaw that pulls deal and contact data from DuckDB and uses AI to draft client proposals you can export as PDF.

Mark Rachapoom
Mark Rachapoom
·7 min read
Build a Proposal Generator App in DenchClaw

Build a Proposal Generator App in DenchClaw

Writing proposals manually is slow and error-prone. You pull data from your CRM, copy it into a document template, customize the language, format it, and export it — and inevitably something gets out of sync between your CRM and the doc.

A Dench App can close that loop. This guide builds a proposal generator that reads deal and contact data directly from DuckDB, populates a template, uses AI to refine the language, and outputs a formatted proposal ready to share.

What You're Building#

  • Deal selector that loads from v_deals
  • Template editor with variable interpolation ({{contact_name}}, {{company}}, etc.)
  • AI-assisted proposal body generation
  • Preview pane with print/PDF export
  • Save proposal back to DenchClaw as an entry document

Step 1: App Setup#

mkdir -p ~/.openclaw-dench/workspace/apps/proposal-generator.dench.app

.dench.yaml:

name: Proposal Generator
description: Generate client proposals from deal data with AI assistance
icon: file-text
version: 1.0.0
permissions:
  - read:crm
  - write:crm
  - chat:create
display: tab

Step 2: HTML Structure#

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Proposal Generator</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; display: grid; grid-template-columns: 300px 1fr 1fr; height: 100vh; }
    .panel { padding: 20px; overflow-y: auto; border-right: 1px solid #1e293b; }
    h3 { font-size: 12px; color: #64748b; text-transform: uppercase; margin: 0 0 12px; }
    select, input, textarea { width: 100%; background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 8px 12px; border-radius: 8px; font-size: 13px; margin-bottom: 12px; }
    textarea { resize: vertical; font-family: monospace; }
    button { padding: 10px 16px; border: none; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 600; }
    .btn-primary { background: #6366f1; color: white; width: 100%; margin-bottom: 8px; }
    .btn-secondary { background: #334155; color: #94a3b8; width: 100%; margin-bottom: 8px; }
    .deal-info { background: #1e293b; border-radius: 8px; padding: 12px; margin-bottom: 12px; font-size: 12px; }
    .deal-info dt { color: #64748b; }
    .deal-info dd { color: #e2e8f0; margin: 0 0 6px; }
    .preview-panel { padding: 40px; background: white; color: #1a1a1a; font-family: Georgia, serif; overflow-y: auto; }
    .preview-panel h1 { font-size: 28px; margin-bottom: 8px; }
    .preview-panel p { line-height: 1.7; margin-bottom: 16px; }
    .preview-panel h2 { font-size: 18px; margin: 24px 0 8px; }
    @media print { .panel { display: none; } .preview-panel { padding: 20px; } }
  </style>
</head>
<body>
  <div class="panel">
    <h3>Deal</h3>
    <select id="dealSelect" onchange="loadDeal()">
      <option value="">Select a deal...</option>
    </select>
    <div id="dealInfo" class="deal-info" style="display:none"></div>
    <h3 style="margin-top:16px">Template</h3>
    <select id="templateSelect" onchange="loadTemplate()">
      <option value="standard">Standard Proposal</option>
      <option value="enterprise">Enterprise Proposal</option>
      <option value="short">Short-form Proposal</option>
    </select>
    <button class="btn-primary" onclick="generateProposal()">Generate Proposal</button>
    <button class="btn-secondary" onclick="window.print()">Export PDF</button>
    <button class="btn-secondary" onclick="saveProposal()">Save to CRM</button>
  </div>
  <div class="panel">
    <h3>AI Instructions</h3>
    <textarea id="instructions" rows="5" placeholder="Custom instructions for the AI...&#10;e.g. Focus on ROI, mention our 99.9% uptime, include case study from Stripe"></textarea>
    <h3>Variables</h3>
    <div id="variablesPanel" style="font-size:12px;color:#64748b">Select a deal to see available variables.</div>
  </div>
  <div class="preview-panel" id="previewPanel">
    <p style="color:#999;font-style:italic">Select a deal and click Generate Proposal to preview.</p>
  </div>
  <script src="generator.js"></script>
</body>
</html>

Step 3: Generator Logic#

generator.js:

let currentDeal = null;
 
const TEMPLATES = {
  standard: `# Proposal for {{company}}
 
Dear {{contact_name}},
 
Thank you for your interest in working with us. This proposal outlines how we can help {{company}} achieve its goals.
 
## The Challenge
 
[AI: Describe the challenge this company likely faces based on their industry and size]
 
## Our Solution
 
[AI: Describe how our product addresses this challenge specifically for {{company}}]
 
## What You Get
 
- [AI: List 3-5 specific deliverables based on the deal value and context]
 
## Pricing
 
Based on our conversations, we're proposing the following:
 
**Total Investment: {{deal_value}}**
 
[AI: Add a brief justification for the pricing]
 
## Next Steps
 
1. Review this proposal
2. Schedule a call to discuss any questions
3. Sign the agreement
 
We look forward to working with {{company}}.
 
Best regards,
The DenchClaw Team`,
 
  enterprise: `# Enterprise Partnership Proposal\n## {{company}} × DenchClaw\n\n[AI: Write a formal enterprise proposal focusing on security, compliance, SLAs, and dedicated support]`,
 
  short: `# Quick Proposal for {{company}}\n\n[AI: Write a concise 3-paragraph proposal focusing on key value, pricing {{deal_value}}, and CTA]`
};
 
async function init() {
  const deals = await dench.db.query(`
    SELECT id, "Deal Name", "Company", "Value", "Stage", "Contact", "Notes"
    FROM v_deals
    WHERE "Stage" NOT IN ('Closed Won', 'Closed Lost')
    ORDER BY CAST("Value" AS DOUBLE) DESC NULLS LAST
  `);
 
  const select = document.getElementById('dealSelect');
  deals.forEach(d => {
    const opt = document.createElement('option');
    opt.value = d.id;
    opt.textContent = `${d['Deal Name'] || d.Company} — $${Number(d.Value || 0).toLocaleString()}`;
    opt.dataset.deal = JSON.stringify(d);
    select.appendChild(opt);
  });
}
 
async function loadDeal() {
  const select = document.getElementById('dealSelect');
  const selectedOpt = select.options[select.selectedIndex];
  if (!selectedOpt.dataset.deal) return;
 
  currentDeal = JSON.parse(selectedOpt.dataset.deal);
 
  // Load contact info
  let contact = null;
  if (currentDeal.Contact) {
    const contacts = await dench.db.query(`SELECT * FROM v_people WHERE id = '${currentDeal.Contact}' LIMIT 1`);
    contact = contacts[0];
  }
  currentDeal._contact = contact;
 
  document.getElementById('dealInfo').style.display = 'block';
  document.getElementById('dealInfo').innerHTML = `
    <dl>
      <dt>Deal</dt><dd>${currentDeal['Deal Name'] || '—'}</dd>
      <dt>Company</dt><dd>${currentDeal.Company || '—'}</dd>
      <dt>Value</dt><dd>$${Number(currentDeal.Value || 0).toLocaleString()}</dd>
      <dt>Stage</dt><dd>${currentDeal.Stage || '—'}</dd>
      <dt>Contact</dt><dd>${contact?.['Full Name'] || '—'}</dd>
    </dl>
  `;
 
  document.getElementById('variablesPanel').innerHTML = `
    <code>{{company}}</code> → ${currentDeal.Company}<br>
    <code>{{contact_name}}</code> → ${contact?.['Full Name'] || 'N/A'}<br>
    <code>{{deal_value}}</code> → $${Number(currentDeal.Value || 0).toLocaleString()}<br>
    <code>{{stage}}</code> → ${currentDeal.Stage}
  `;
}
 
function loadTemplate() {
  // Just triggers re-generate when user switches templates
}
 
async function generateProposal() {
  if (!currentDeal) {
    await dench.ui.toast({ message: 'Select a deal first', type: 'warning' });
    return;
  }
 
  const template = TEMPLATES[document.getElementById('templateSelect').value];
  const instructions = document.getElementById('instructions').value;
  const contact = currentDeal._contact;
 
  // Replace simple variables
  let populated = template
    .replace(/{{company}}/g, currentDeal.Company || 'your company')
    .replace(/{{contact_name}}/g, contact?.['Full Name'] || 'there')
    .replace(/{{deal_value}}/g, '$' + Number(currentDeal.Value || 0).toLocaleString())
    .replace(/{{stage}}/g, currentDeal.Stage || '');
 
  document.getElementById('previewPanel').innerHTML = '<p style="color:#999">Generating...</p>';
 
  const session = await dench.chat.createSession();
  const prompt = `You are writing a business proposal. Fill in the [AI: ...] sections in this proposal template with appropriate content. Keep the overall structure but write the AI sections in a professional, compelling way.
 
Deal context:
- Company: ${currentDeal.Company}
- Deal value: $${Number(currentDeal.Value || 0).toLocaleString()}
- Stage: ${currentDeal.Stage}
- Notes: ${currentDeal.Notes || 'None'}
- Contact: ${contact?.['Full Name'] || 'Unknown'}, ${contact?.['Title'] || 'Unknown title'}
 
${instructions ? `Additional instructions: ${instructions}` : ''}
 
Template to fill:
${populated}
 
Return the completed proposal in Markdown format. Replace all [AI: ...] placeholders with actual content.`;
 
  const result = await dench.chat.send(session.id, prompt);
 
  // Render markdown to HTML (simple version)
  const html = result
    .replace(/^# (.*)/gm, '<h1>$1</h1>')
    .replace(/^## (.*)/gm, '<h2>$1</h2>')
    .replace(/^### (.*)/gm, '<h3>$1</h3>')
    .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
    .replace(/^- (.*)/gm, '<li>$1</li>')
    .replace(/\n\n/g, '</p><p>')
    .replace(/^(?!<[h|l|p])/gm, '<p>');
 
  document.getElementById('previewPanel').innerHTML = html;
}
 
async function saveProposal() {
  if (!currentDeal) return;
  const content = document.getElementById('previewPanel').innerText;
  await dench.agent.run(`Save a proposal document for deal ${currentDeal.id}: attach this content as a note`);
  await dench.ui.toast({ message: 'Proposal saved to CRM', type: 'success' });
}
 
init();

Frequently Asked Questions#

How do I export to PDF?#

The window.print() call triggers the browser's print dialog. Use Chrome's "Save as PDF" option. For better PDF export, consider adding a print stylesheet that hides the panels and formats the preview cleanly.

Can I create custom templates?#

Yes. Add new entries to the TEMPLATES object in generator.js, and add corresponding <option> elements to the template selector. You can also save templates to dench.store for persistence.

How do I add my company's branding?#

Add your logo and colors to the .preview-panel styles in index.html. For letterhead, add a header with your company name, address, and logo URL inside generateProposal() before rendering.

Can I pull pricing from a product catalog?#

Yes. Create a products object in DenchClaw CRM, then query v_products in the proposal generator to populate pricing tables dynamically based on which products are attached to the deal.

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