Build a Document Generator App
Build a document generator app in DenchClaw that creates contracts, NDAs, SOWs, and onboarding docs from CRM data with template variables and AI customization.
Mark Rachapoom
·6 min read
Build a Document Generator App
Generating business documents manually is one of those tasks that should have been automated years ago. You have the data — it's in your CRM. You have the templates. The only missing piece is something that merges them automatically and lets you review before sending.
This guide builds a document generator Dench App: select a document type, pick a CRM entry (contact or deal), preview the filled document, and export it as PDF or save it back to DenchClaw.
What You're Building#
- Document type selector: NDA, SOW, Onboarding Checklist, Invoice
- CRM entry picker (contact or deal)
- Template variable system with live preview
- AI customization of template sections
- Export to HTML/PDF and save to entry document
Step 1: App Setup#
mkdir -p ~/.openclaw-dench/workspace/apps/document-generator.dench.app.dench.yaml:
name: Document Generator
description: Generate contracts, NDAs, and docs from CRM data
icon: file-plus
version: 1.0.0
permissions:
- read:crm
- write:crm
- chat:create
display: tabStep 2: HTML Layout and Templates#
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document 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; height: 100vh; }
.config { padding: 20px; border-right: 1px solid #1e293b; overflow-y: auto; background: #0a0f1e; }
.preview { padding: 0; overflow-y: auto; }
.preview-doc { background: white; color: #1a1a1a; padding: 48px; min-height: 100%; font-family: Georgia, serif; font-size: 14px; line-height: 1.7; }
.preview-doc h1 { font-size: 24px; margin-bottom: 8px; }
.preview-doc h2 { font-size: 16px; margin: 24px 0 8px; }
.preview-doc .section { margin-bottom: 20px; }
h3 { font-size: 12px; color: #64748b; text-transform: uppercase; margin: 16px 0 8px; }
select, input { width: 100%; background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 8px 12px; border-radius: 8px; font-size: 13px; margin-bottom: 10px; }
button { padding: 10px 16px; border: none; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 600; width: 100%; margin-bottom: 8px; }
.btn-primary { background: #6366f1; color: white; }
.btn-secondary { background: #334155; color: #94a3b8; }
.var-preview { background: #1e293b; border-radius: 8px; padding: 12px; font-size: 11px; color: #94a3b8; margin-bottom: 10px; }
.var-preview dt { color: #475569; }
.var-preview dd { color: #e2e8f0; margin: 0 0 4px; font-weight: 600; }
@media print { .config { display: none; } .preview-doc { padding: 20px; } }
</style>
</head>
<body>
<div class="config">
<h3>Document Type</h3>
<select id="docType" onchange="updatePreview()">
<option value="nda">NDA (Non-Disclosure Agreement)</option>
<option value="sow">Statement of Work</option>
<option value="onboarding">Customer Onboarding Checklist</option>
<option value="invoice">Invoice</option>
</select>
<h3>CRM Entry</h3>
<select id="objectType" onchange="loadEntries()">
<option value="deals">Deal</option>
<option value="people">Contact</option>
<option value="companies">Company</option>
</select>
<select id="entrySelect" onchange="updatePreview()">
<option value="">Select entry...</option>
</select>
<div class="var-preview" id="varPreview">Select an entry to see variables.</div>
<button class="btn-primary" onclick="generateWithAI()">Customize with AI</button>
<button class="btn-secondary" onclick="window.print()">Export PDF</button>
<button class="btn-secondary" onclick="saveDocument()">Save to CRM Entry</button>
</div>
<div class="preview">
<div class="preview-doc" id="previewDoc">
<p style="color:#999;font-style:italic">Select a document type and CRM entry to preview.</p>
</div>
</div>
<script src="generator.js"></script>
</body>
</html>Step 3: Generator Logic#
generator.js:
const TEMPLATES = {
nda: (vars) => `
<h1>Non-Disclosure Agreement</h1>
<p>This Non-Disclosure Agreement ("Agreement") is entered into as of <strong>${vars.date}</strong> between <strong>${vars.company_name}</strong> ("Receiving Party") and <strong>Your Company, Inc.</strong> ("Disclosing Party").</p>
<h2>1. Confidential Information</h2>
<p>For purposes of this Agreement, "Confidential Information" means any non-public information disclosed by the Disclosing Party to ${vars.company_name || 'the Receiving Party'}, either directly or indirectly...</p>
<h2>2. Obligations</h2>
<p>${vars.contact_name || 'The Receiving Party'} agrees to: (a) hold the Confidential Information in strict confidence; (b) not disclose the Confidential Information to third parties without prior written consent; (c) use the Confidential Information solely for the purpose of evaluating a potential business relationship.</p>
<h2>3. Term</h2>
<p>This Agreement shall remain in effect for a period of two (2) years from the date first written above.</p>
<h2>4. Signatures</h2>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:40px;margin-top:40px">
<div>
<p>For ${vars.company_name || 'Receiving Party'}:</p>
<div style="border-bottom:1px solid #ccc;margin:20px 0 4px"></div>
<p>${vars.contact_name || 'Name'}</p>
<p style="color:#666">${vars.contact_title || 'Title'}</p>
</div>
<div>
<p>For Your Company, Inc.:</p>
<div style="border-bottom:1px solid #ccc;margin:20px 0 4px"></div>
<p>Authorized Signatory</p>
</div>
</div>`,
sow: (vars) => `
<h1>Statement of Work</h1>
<p><strong>Project:</strong> ${vars.deal_name || 'Project Name'}<br>
<strong>Client:</strong> ${vars.company_name || 'Client Name'}<br>
<strong>Contact:</strong> ${vars.contact_name || '—'}<br>
<strong>Date:</strong> ${vars.date}<br>
<strong>Value:</strong> ${vars.deal_value || 'TBD'}</p>
<h2>Scope of Work</h2>
<p>This Statement of Work describes the services to be provided by Your Company to ${vars.company_name || 'Client'} under this engagement.</p>
<h2>Deliverables</h2>
<ul><li>Phase 1: Discovery and Requirements</li><li>Phase 2: Implementation</li><li>Phase 3: Testing and Launch</li><li>Phase 4: Training and Handoff</li></ul>
<h2>Timeline</h2>
<p>Estimated project duration: 8-12 weeks from signed agreement.</p>
<h2>Investment</h2>
<p>Total project investment: <strong>${vars.deal_value || '$0'}</strong><br>Payment schedule: 50% upon signing, 50% upon delivery.</p>`,
onboarding: (vars) => `
<h1>Customer Onboarding Checklist</h1>
<p><strong>Customer:</strong> ${vars.company_name || '—'}<br>
<strong>Contact:</strong> ${vars.contact_name || '—'}<br>
<strong>Start Date:</strong> ${vars.date}</p>
<h2>Week 1: Setup</h2>
<p>☐ Send welcome email to ${vars.contact_name || 'customer'}<br>☐ Schedule kickoff call<br>☐ Provision account access<br>☐ Share onboarding resources</p>
<h2>Week 2-3: Implementation</h2>
<p>☐ Complete initial configuration<br>☐ Import existing data<br>☐ Configure integrations<br>☐ User training session</p>
<h2>Week 4: Launch</h2>
<p>☐ Go-live review call<br>☐ Confirm all users are active<br>☐ Share support resources<br>☐ Schedule 30-day check-in</p>`,
invoice: (vars) => `
<h1>Invoice</h1>
<p><strong>Invoice #:</strong> INV-${Date.now().toString().slice(-6)}<br>
<strong>Date:</strong> ${vars.date}<br>
<strong>Due Date:</strong> ${vars.due_date}</p>
<h2>Bill To</h2>
<p>${vars.contact_name || '—'}<br>${vars.company_name || '—'}<br>${vars.email || '—'}</p>
<h2>Services</h2>
<table style="width:100%;border-collapse:collapse">
<tr style="border-bottom:1px solid #ddd"><th style="text-align:left;padding:8px">Description</th><th style="text-align:right;padding:8px">Amount</th></tr>
<tr><td style="padding:8px">${vars.deal_name || 'Services'}</td><td style="text-align:right;padding:8px">${vars.deal_value || '$0'}</td></tr>
<tr style="border-top:2px solid #333;font-weight:bold"><td style="padding:8px">Total</td><td style="text-align:right;padding:8px">${vars.deal_value || '$0'}</td></tr>
</table>
<p style="margin-top:30px;color:#666">Payment due within 30 days. Bank transfer or check accepted.</p>`
};
let currentEntry = null;
let currentVars = {};
async function loadEntries() {
const objectType = document.getElementById('objectType').value;
const entries = await dench.db.query(`
SELECT id, "Full Name", "Deal Name", "Company", "Name"
FROM v_${objectType} LIMIT 100
`);
const select = document.getElementById('entrySelect');
select.innerHTML = '<option value="">Select entry...</option>';
entries.forEach(e => {
const opt = document.createElement('option');
opt.value = e.id;
opt.textContent = e['Full Name'] || e['Deal Name'] || e['Name'] || e['Company'] || e.id;
opt.dataset.entry = JSON.stringify(e);
select.appendChild(opt);
});
}
async function updatePreview() {
const select = document.getElementById('entrySelect');
const selectedOpt = select.options[select.selectedIndex];
if (!selectedOpt?.dataset?.entry) return;
const entry = JSON.parse(selectedOpt.dataset.entry);
currentEntry = entry;
// Load full entry details
const objectType = document.getElementById('objectType').value;
const full = await dench.db.query(`SELECT * FROM v_${objectType} WHERE id = '${entry.id}' LIMIT 1`);
const e = full[0] || entry;
const today = new Date();
const dueDate = new Date(today);
dueDate.setDate(dueDate.getDate() + 30);
currentVars = {
date: today.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }),
due_date: dueDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }),
company_name: e.Company || e.Name || 'Client',
contact_name: e['Full Name'] || '',
contact_title: e.Title || '',
email: e['Email Address'] || '',
deal_name: e['Deal Name'] || 'Engagement',
deal_value: e.Value ? '$' + Number(e.Value).toLocaleString() : 'TBD'
};
document.getElementById('varPreview').innerHTML = Object.entries(currentVars)
.map(([k, v]) => `<dt>{{${k}}}</dt><dd>${v || '—'}</dd>`)
.join('');
renderTemplate();
}
function renderTemplate() {
const docType = document.getElementById('docType').value;
const template = TEMPLATES[docType];
if (!template) return;
document.getElementById('previewDoc').innerHTML = template(currentVars);
}
async function generateWithAI() {
if (!currentEntry) { await dench.ui.toast({ message: 'Select a CRM entry first', type: 'warning' }); return; }
const docType = document.getElementById('docType').value;
const session = await dench.chat.createSession();
const current = document.getElementById('previewDoc').innerHTML;
const customized = await dench.chat.send(session.id,
`Improve this ${docType} document to be more professional and specific to ${currentVars.company_name}. Keep the same HTML structure but enhance the language. Return only the HTML content: ${current.substring(0, 2000)}`
);
document.getElementById('previewDoc').innerHTML = customized;
}
async function saveDocument() {
if (!currentEntry) return;
const content = document.getElementById('previewDoc').innerText;
await dench.agent.run(`Save a document for entry ${currentEntry.id}: ${content.substring(0, 500)}`);
await dench.ui.toast({ message: 'Document saved to CRM entry', type: 'success' });
}
loadEntries();