Real-Time Updates in Dench Apps with the Events API
Learn how to use dench.events.on() in DenchClaw App Builder apps to receive real-time CRM updates, subscribe to entry events, and build live dashboards that auto-refresh.
Real-Time Updates in Dench Apps with the Events API
A dashboard that shows stale data is almost worse than no dashboard at all — you make decisions based on information that's already out of date. DenchClaw's Events API gives your apps a real-time update mechanism: instead of polling every N seconds, your app subscribes to events and re-renders only when the CRM actually changes.
This guide covers the full dench.events API: available events, subscription patterns, debouncing, and how to combine events with scheduled refresh for reliable live dashboards.
The Events API Overview#
// Subscribe to an event
dench.events.on('entry:created', handler);
dench.events.on('entry:updated', handler);
dench.events.on('entry:deleted', handler);
dench.events.on('message:received', handler);
// Unsubscribe
dench.events.off('entry:created', handler);
// One-time subscription
dench.events.once('entry:created', handler);Events fire whenever the DenchClaw agent or any app modifies CRM data. They're delivered to all active app instances simultaneously.
Available Events#
entry:created#
Fires when a new entry is added to any CRM object.
dench.events.on('entry:created', (event) => {
console.log('New entry created:', event);
// event.objectName — which object (e.g., 'people', 'deals')
// event.entryId — the new entry's ID
// event.fields — the entry's initial field values (if available)
});When to use: Update counts, add new rows to tables, show "New lead added" notifications.
entry:updated#
Fires when any field on an existing entry changes.
dench.events.on('entry:updated', (event) => {
console.log('Entry updated:', event.entryId, event.objectName);
// event.changedFields — array of field names that changed (if available)
});When to use: Refresh pipeline boards when deal stages change, update contact details in real time.
entry:deleted#
Fires when an entry is deleted from the CRM.
dench.events.on('entry:deleted', (event) => {
// Remove the entry from your rendered list
removeEntryFromUI(event.entryId);
});message:received#
Fires when the DenchClaw agent receives a new message on any channel (Telegram, web chat, etc.).
dench.events.on('message:received', (event) => {
console.log('New message from:', event.channel, event.from);
});When to use: Build a unified inbox widget, show notification when the agent gets a message.
Pattern 1: Simple Reload on Any Change#
The simplest pattern — reload all data whenever anything changes:
async function loadData() {
const deals = await dench.db.query("SELECT * FROM v_deals WHERE \"Stage\" NOT IN ('Closed Won', 'Closed Lost')");
renderDeals(deals);
}
dench.events.on('entry:created', loadData);
dench.events.on('entry:updated', loadData);
dench.events.on('entry:deleted', loadData);
loadData(); // Initial loadThis is fine for small datasets and simple apps. For large datasets, use the debouncing pattern below.
Pattern 2: Debounced Reload#
If multiple events fire in quick succession (e.g., bulk import of 50 contacts), you don't want 50 reload calls:
let reloadTimeout = null;
function scheduleReload() {
if (reloadTimeout) clearTimeout(reloadTimeout);
reloadTimeout = setTimeout(() => {
reloadTimeout = null;
loadData();
}, 500); // Wait 500ms after the last event before reloading
}
dench.events.on('entry:created', scheduleReload);
dench.events.on('entry:updated', scheduleReload);The 500ms window collapses a burst of 50 events into one reload call.
Pattern 3: Optimistic Updates (Targeted Refresh)#
For faster perceived performance, update the UI immediately with the known change, then verify with a targeted query:
dench.events.on('entry:updated', async (event) => {
if (event.objectName !== 'deals') return; // Only care about deals
// Re-query just this one deal
const updated = await dench.db.query(
`SELECT * FROM v_deals WHERE id = '${event.entryId}' LIMIT 1`
);
if (updated[0]) {
updateDealCard(event.entryId, updated[0]);
}
});
function updateDealCard(id, deal) {
const card = document.getElementById(`deal-${id}`);
if (!card) {
// New card — reload the full list
loadData();
return;
}
// Update just this card's content
card.querySelector('.deal-stage').textContent = deal.Stage;
card.querySelector('.deal-value').textContent = '$' + Number(deal.Value || 0).toLocaleString();
}This avoids reloading the full dataset when only one item changed.
Pattern 4: Combining Events and Polling#
Events are best-effort in the current implementation. For critical dashboards, combine event-driven updates with a fallback polling mechanism:
const POLL_INTERVAL = 5 * 60 * 1000; // 5 minutes
// Event-driven fast path
dench.events.on('entry:created', scheduleReload);
dench.events.on('entry:updated', scheduleReload);
// Polling fallback (in case events miss something)
setInterval(loadData, POLL_INTERVAL);
// Initial load
loadData();The events give you near-instant updates for most changes. The poll catches anything that slipped through.
Pattern 5: Object-Specific Subscriptions#
Filter events to only react to changes on the objects your app cares about:
dench.events.on('entry:updated', (event) => {
// Only process deal updates
if (event.objectName !== 'deals') return;
refreshDealsPipeline();
});
dench.events.on('entry:created', (event) => {
// Show different notifications per object
if (event.objectName === 'people') {
showNewLeadNotification(event);
} else if (event.objectName === 'deals') {
showNewDealNotification(event);
}
});Using dench.cron for Scheduled Refreshes#
For data that needs to refresh on a schedule (not just on changes), use dench.cron.schedule(). This requires the cron:schedule permission:
// Refresh enrichment data every hour
await dench.cron.schedule(
{ everyMs: 60 * 60 * 1000 },
async () => {
await refreshEnrichmentData();
console.log('Hourly enrichment refresh completed');
}
);
// Daily digest at 9am
await dench.cron.schedule(
{ daily: '09:00' },
async () => {
const digest = await buildDailyDigest();
await dench.agent.run(`Send me a daily CRM digest: ${digest}`);
}
);Cron jobs run in the DenchClaw backend process, even when the app frontend isn't open.
Building a Live Dashboard: Full Example#
Combining events, debouncing, and a fallback poll for a reliable live dashboard:
const DEBOUNCE_MS = 300;
const FALLBACK_POLL_MS = 2 * 60 * 1000;
let debounceTimer = null;
let isLoading = false;
function scheduleRefresh(reason) {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => refresh(reason), DEBOUNCE_MS);
}
async function refresh(reason = 'manual') {
if (isLoading) return; // Prevent concurrent refreshes
isLoading = true;
try {
const [pipeline, leads, closedThisMonth] = await Promise.all([
dench.db.query("SELECT SUM(CAST(\"Value\" AS DOUBLE)) AS total FROM v_deals WHERE \"Stage\" NOT IN ('Closed Won', 'Closed Lost')"),
dench.db.query("SELECT COUNT(*) AS count FROM v_people WHERE \"Status\" = 'Lead'"),
dench.db.query("SELECT SUM(CAST(\"Value\" AS DOUBLE)) AS total FROM v_deals WHERE \"Stage\" = 'Closed Won' AND DATE_TRUNC('month', CAST(\"Close Date\" AS DATE)) = DATE_TRUNC('month', CURRENT_DATE)")
]);
updateUI({ pipeline, leads, closedThisMonth });
updateTimestamp(reason);
} finally {
isLoading = false;
}
}
function updateTimestamp(reason) {
const el = document.getElementById('last-updated');
if (el) el.textContent = `Updated ${new Date().toLocaleTimeString()} (${reason})`;
}
// Subscribe to events
dench.events.on('entry:created', () => scheduleRefresh('new entry'));
dench.events.on('entry:updated', () => scheduleRefresh('entry updated'));
// Fallback poll
setInterval(() => refresh('scheduled'), FALLBACK_POLL_MS);
// Initial load
refresh('initial');Frequently Asked Questions#
How quickly do events fire after a CRM change?#
Events are delivered within ~100-500ms of the change. Network latency and the DenchClaw gateway's event dispatch add some delay. For most use cases, this feels instantaneous.
Do events fire for changes made by the AI agent?#
Yes. The agent uses the same DuckDB write path, so all agent-initiated changes trigger events.
Can I subscribe to events in a background cron job?#
Events are frontend-only (delivered to active app instances). Cron jobs run server-side and don't receive events — use polling within your cron job if you need to check for changes.
What happens if my app is closed when an event fires?#
Events are not queued for offline apps. If your app is closed when a change happens, it won't receive that event. The next time the app opens, it should load fresh data from DuckDB rather than relying on events for the initial state.
Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →
