Back to The Times of Claw

Build a Meeting Scheduler App with DenchClaw

Build a meeting scheduler app in DenchClaw that reads your CRM contact list, generates meeting prep notes using AI, and saves follow-up tasks automatically.

Mark Rachapoom
Mark Rachapoom
·7 min read
Build a Meeting Scheduler App with DenchClaw

Build a Meeting Scheduler App with DenchClaw

The friction before a sales meeting is real: you pull up the contact record, skim their company, remember what you last discussed, figure out what you want to accomplish — and you do all of this in a 2-minute scramble right before the call.

A meeting scheduler app in DenchClaw solves this. It lets you select an upcoming meeting, auto-generates a prep brief from CRM data, and creates a post-meeting follow-up task automatically when you mark the meeting as complete.

What You're Building#

  • Contact selector with upcoming meeting dates
  • AI-generated meeting prep brief (company context, last discussion, goals)
  • Pre-meeting checklist
  • Post-meeting notes field that auto-creates follow-up tasks
  • Calendar integration (if Google Calendar is configured)

Step 1: App Setup#

mkdir -p ~/.openclaw-dench/workspace/apps/meeting-scheduler.dench.app

.dench.yaml:

name: Meeting Prep
description: AI meeting prep briefs and automatic follow-up task creation
icon: calendar
version: 1.0.0
permissions:
  - read:crm
  - write:crm
  - chat:create
display: tab

Step 2: HTML Layout#

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Meeting Prep</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; display: grid; grid-template-columns: 320px 1fr; height: 100vh; }
    .sidebar { overflow-y: auto; border-right: 1px solid #1e293b; padding: 20px; background: #0a0f1e; }
    .main { overflow-y: auto; padding: 24px; }
    h3 { font-size: 12px; color: #64748b; text-transform: uppercase; margin: 16px 0 8px 0; }
    input, select, textarea { width: 100%; background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 8px 12px; border-radius: 8px; font-size: 13px; margin-bottom: 10px; }
    textarea { resize: vertical; font-family: inherit; line-height: 1.5; }
    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; }
    .contact-card { background: #1e293b; border-radius: 8px; padding: 12px; margin-bottom: 10px; cursor: pointer; border: 1px solid transparent; }
    .contact-card:hover, .contact-card.selected { border-color: #6366f1; }
    .contact-name { font-weight: 600; font-size: 13px; }
    .contact-sub { font-size: 11px; color: #64748b; margin-top: 2px; }
    .section { background: #1e293b; border-radius: 12px; padding: 16px; margin-bottom: 16px; }
    .section h2 { font-size: 15px; margin: 0 0 12px; font-weight: 700; }
    .prep-content { font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
    .checklist-item { display: flex; gap: 10px; align-items: center; padding: 6px 0; border-bottom: 1px solid #334155; font-size: 13px; }
    .checklist-item:last-child { border-bottom: none; }
    .checklist-item input[type="checkbox"] { width: 16px; height: 16px; accent-color: #6366f1; }
    .status-badge { padding: 2px 8px; border-radius: 999px; font-size: 11px; }
    .generating { color: #f59e0b; font-style: italic; font-size: 13px; }
    .empty-state { text-align: center; padding: 60px 20px; color: #475569; }
  </style>
</head>
<body>
  <div class="sidebar">
    <h3>Search Contacts</h3>
    <input type="text" id="contactSearch" placeholder="Search contacts..." oninput="searchContacts(this.value)">
    <h3>Upcoming Meetings</h3>
    <div id="contactList"><p style="color:#64748b;font-size:13px">Loading contacts...</p></div>
  </div>
  <div class="main" id="mainPanel">
    <div class="empty-state">
      <div style="font-size:40px;margin-bottom:12px">📅</div>
      <div>Select a contact to generate your meeting prep brief.</div>
    </div>
  </div>
  <script src="scheduler.js"></script>
</body>
</html>

Step 3: Scheduler Logic#

scheduler.js:

let allContacts = [];
let selectedContact = null;
 
async function init() {
  allContacts = await dench.db.query(`
    SELECT p.id, p."Full Name", p."Email Address", p."Company", p."Title", p."Status",
           p."Last Contacted", p."Notes",
           COUNT(d.id) AS deal_count,
           SUM(CAST(d."Value" AS DOUBLE)) AS total_value
    FROM v_people p
    LEFT JOIN v_deals d ON d."Contact" = p.id
    GROUP BY p.id, p."Full Name", p."Email Address", p."Company", p."Title", p."Status", p."Last Contacted", p."Notes"
    ORDER BY p."Full Name"
    LIMIT 100
  `);
  renderContactList(allContacts);
}
 
function renderContactList(contacts) {
  const list = document.getElementById('contactList');
  list.innerHTML = contacts.slice(0, 30).map(c => `
    <div class="contact-card ${selectedContact?.id === c.id ? 'selected' : ''}" onclick="selectContact('${c.id}')">
      <div class="contact-name">${c['Full Name'] || 'Unknown'}</div>
      <div class="contact-sub">${c.Company || '—'} · ${c.Title || c.Status}</div>
      ${c.deal_count > 0 ? `<div class="contact-sub" style="color:#10b981">$${Number(c.total_value || 0).toLocaleString()} in deals</div>` : ''}
    </div>
  `).join('') || '<p style="color:#64748b;font-size:13px">No contacts found.</p>';
}
 
function searchContacts(query) {
  const q = query.toLowerCase();
  const filtered = allContacts.filter(c =>
    (c['Full Name'] || '').toLowerCase().includes(q) ||
    (c.Company || '').toLowerCase().includes(q)
  );
  renderContactList(filtered);
}
 
async function selectContact(id) {
  selectedContact = allContacts.find(c => c.id === id);
  renderContactList(allContacts); // Re-render to update selection
 
  document.getElementById('mainPanel').innerHTML = `
    <div class="section">
      <h2>Meeting with ${selectedContact['Full Name']}</h2>
      <div style="font-size:13px;color:#94a3b8">
        ${selectedContact.Company || '—'} · ${selectedContact.Title || selectedContact.Status} · 
        ${selectedContact['Email Address'] || 'no email'}
      </div>
      <div style="margin-top:12px;display:flex;gap:8px">
        <input type="datetime-local" id="meetingDate" style="width:220px">
        <input type="text" id="meetingPurpose" placeholder="Meeting purpose..." style="flex:1">
      </div>
    </div>
    <div class="section">
      <h2>Meeting Prep Brief</h2>
      <p class="generating" id="prepStatus">Generating prep brief...</p>
      <div class="prep-content" id="prepContent"></div>
    </div>
    <div class="section">
      <h2>Pre-Meeting Checklist</h2>
      <div id="checklist">
        ${generateChecklist(selectedContact).map(item => `
          <div class="checklist-item">
            <input type="checkbox">
            <span>${item}</span>
          </div>
        `).join('')}
      </div>
    </div>
    <div class="section">
      <h2>Post-Meeting Notes</h2>
      <textarea id="meetingNotes" rows="6" placeholder="What was discussed? What did you commit to?"></textarea>
      <div style="display:flex;gap:8px">
        <button class="btn-primary" onclick="completeMeeting()" style="flex:1">Complete & Create Follow-up</button>
        <button class="btn-secondary" onclick="saveDraft()" style="flex:1">Save Draft</button>
      </div>
    </div>
  `;
 
  await generatePrepBrief();
}
 
function generateChecklist(contact) {
  const items = [
    `Review ${contact.Company || 'their company'}'s recent news`,
    `Check last email thread with ${contact['Full Name']}`,
    `Review deal history (${contact.deal_count || 0} deals)`
  ];
  if (contact.Notes) items.push('Review contact notes');
  if (contact['Last Contacted']) items.push(`Refresh on last discussion (${contact['Last Contacted']})`);
  items.push('Prepare 2-3 questions to ask');
  items.push('Know your ask / next step before the call');
  return items;
}
 
async function generatePrepBrief() {
  const session = await dench.chat.createSession();
  const c = selectedContact;
 
  const prompt = `Generate a concise meeting prep brief for a sales meeting. Format with clear sections.
 
Contact: ${c['Full Name']}
Company: ${c.Company || 'Unknown'}
Title: ${c.Title || 'Unknown'}
Status: ${c.Status}
Last Contacted: ${c['Last Contacted'] || 'Never'}
Deal History: ${c.deal_count || 0} deals worth $${Number(c.total_value || 0).toLocaleString()}
Notes: ${c.Notes || 'None'}
 
Write a brief with these sections:
1. Company Context (2-3 sentences about what they likely care about)
2. Relationship Summary (where we are in the relationship)
3. Goals for this Meeting (2-3 concrete objectives)
4. Potential Objections & Responses (2 most likely objections)
5. Recommended Next Step (specific ask to end the meeting with)
 
Keep it practical and under 300 words total.`;
 
  try {
    const brief = await dench.chat.send(session.id, prompt);
    document.getElementById('prepStatus').style.display = 'none';
    document.getElementById('prepContent').textContent = brief;
  } catch (err) {
    document.getElementById('prepStatus').textContent = 'Failed to generate brief. Check your AI configuration.';
  }
}
 
async function completeMeeting() {
  const notes = document.getElementById('meetingNotes').value;
  if (!notes) { await dench.ui.toast({ message: 'Add meeting notes first', type: 'warning' }); return; }
 
  // Update Last Contacted and save notes
  await dench.agent.run(`Update contact ${selectedContact.id}: set Last Contacted to today. Add note: "${notes.substring(0, 300)}"`);
  
  // Create follow-up task
  await dench.agent.run(`Create a task: Follow up with ${selectedContact['Full Name']} from ${selectedContact.Company || 'company'}, due in 3 days, linked to contact ${selectedContact.id}`);
  
  await dench.ui.toast({ message: 'Meeting completed. Follow-up task created.', type: 'success' });
}
 
async function saveDraft() {
  const notes = document.getElementById('meetingNotes').value;
  await dench.store.set(`meeting_draft_${selectedContact.id}`, notes);
  await dench.ui.toast({ message: 'Draft saved', type: 'success' });
}
 
init();

Frequently Asked Questions#

How do I integrate with Google Calendar?#

If you have the gog skill configured, call dench.agent.run("Show me my meetings for today") to load calendar events. The agent can return meeting attendees, and you can pre-populate the contact selector based on attendee emails.

How do I add the meeting to my calendar from this app?#

Add a button that calls dench.agent.run("Create a calendar event: meeting with ${contact} at ${datetime}"). The gog skill handles the Google Calendar API call.

Can I save meeting prep templates?#

Yes. Add a "Save as Template" button that saves the current checklist and prep prompt to dench.store as named templates. Load templates for similar meeting types (discovery call, renewal, upsell).

How do I track meeting outcomes over time?#

Add a Meeting Outcome field to your contacts object (Won, Lost, Follow-up needed, Not interested) and update it post-meeting. Then query meeting outcome trends in DuckDB.

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