Back to The Times of Claw

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
Mark Rachapoom
·6 min read
Build a Document Generator App

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: tab

Step 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();
Mark Rachapoom

Written by

Mark Rachapoom

Building the future of AI CRM software.

Continue reading

DENCH

© 2026 DenchHQ · San Francisco, CA