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.
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: tabStep 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... 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 →
