Build Your Own Business Apps Without a Developer
DenchClaw's App Builder lets you create custom dashboards, tools, and workflow apps using plain HTML and the Dench Bridge API — no build tools required.
Build Your Own Business Apps Without a Developer
Custom software used to mean hiring a developer, waiting months, and spending five figures. For small teams, the alternative was living with the limitations of off-the-shelf tools and duct-taping together integrations that never quite did what you wanted.
DenchClaw's App Builder changes this equation. Because every Dench App is just a folder of HTML, CSS, and JavaScript files with access to the Dench Bridge API, the barrier to building custom tools is almost nothing. If you've ever written a webpage, you can build a Dench App. And if you haven't, you can ask your DenchClaw AI agent to build one for you.
This guide walks through building a real Dench App from scratch, covering the project structure, the bridge API, common patterns, and how to use the AI agent to accelerate the process.
Before You Start#
Make sure DenchClaw is running (npx denchclaw if you haven't set it up yet). The app builder works entirely within your local workspace — no deployment, no cloud, no external accounts needed.
Your apps live in:
~/.openclaw-dench/workspace/apps/
You can create .dench.app folders anywhere inside the workspace, but apps/ is the conventional location.
Step 1: The Manifest File#
Every Dench App starts with a .dench.yaml file. This is the manifest — it tells DenchClaw the app's name, icon, and what platform capabilities it needs.
# ~/.openclaw-dench/workspace/apps/pipeline-dashboard.dench.app/.dench.yaml
name: Pipeline Dashboard
icon: bar-chart-2
description: Real-time view of your sales pipeline
version: "1.0"
permissions:
- db.read
- ui.toast
- store.readwrite
display:
defaultView: fullscreenPermission options (only request what you need):
db.read— Run SELECT queries against DuckDBdb.write— Run INSERT, UPDATE, DELETE queriesobjects.read— List objects and fieldsobjects.write— Create/modify objects and entriesfiles.read— Read workspace filesfiles.write— Write workspace fileschat.session— Create AI chat sessionshttp.fetch— Fetch external URLs (proxied)ui.toast— Show toast notificationsui.confirm— Show confirmation dialogsstore.readwrite— Read/write persistent app stateevents.subscribe— Subscribe to real-time CRM eventscron.schedule— Schedule background tasks
Icon options use Lucide icon names: bar-chart-2, users, briefcase, zap, star, calendar, database, etc.
Step 2: The HTML Entry Point#
Create index.html in the same folder. This is a normal HTML file — use any libraries you want.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pipeline Dashboard</title>
<!-- Load any external library you want -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0f0f0f;
color: #e5e5e5;
padding: 24px;
}
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.card {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 20px;
}
.metric { font-size: 2.5rem; font-weight: 700; color: #7c3aed; }
.label { font-size: 0.875rem; color: #888; margin-top: 4px; }
canvas { margin-top: 16px; }
</style>
</head>
<body>
<div class="grid">
<div class="card">
<div class="metric" id="total-deals">—</div>
<div class="label">Active Deals</div>
</div>
<div class="card">
<div class="metric" id="pipeline-value">—</div>
<div class="label">Total Pipeline Value</div>
</div>
<div class="card" style="grid-column: span 2">
<canvas id="stage-chart"></canvas>
</div>
</div>
<script>
// dench is auto-injected by DenchClaw
async function loadDashboard() {
try {
// Query 1: counts and total value
const summary = await dench.db.query(`
SELECT
COUNT(*) AS total,
SUM(CAST("Deal Value" AS DOUBLE)) AS pipeline_value
FROM v_deals
WHERE "Status" NOT IN ('Closed Won', 'Closed Lost')
`);
document.getElementById('total-deals').textContent =
summary[0]?.total ?? 0;
document.getElementById('pipeline-value').textContent =
'$' + ((summary[0]?.pipeline_value ?? 0) / 1000).toFixed(0) + 'k';
// Query 2: deals by stage
const byStage = await dench.db.query(`
SELECT
COALESCE("Stage", 'No Stage') AS stage,
COUNT(*) AS count,
SUM(CAST("Deal Value" AS DOUBLE)) AS value
FROM v_deals
WHERE "Status" NOT IN ('Closed Won', 'Closed Lost')
GROUP BY "Stage"
ORDER BY value DESC NULLS LAST
`);
new Chart(document.getElementById('stage-chart'), {
type: 'bar',
data: {
labels: byStage.map(r => r.stage),
datasets: [{
label: 'Pipeline Value ($)',
data: byStage.map(r => r.value ?? 0),
backgroundColor: '#7c3aed88',
borderColor: '#7c3aed',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: { legend: { labels: { color: '#888' } } },
scales: {
x: { ticks: { color: '#888' } },
y: { ticks: { color: '#888' } }
}
}
});
} catch (err) {
console.error('Dashboard load failed:', err);
await dench.ui.toast({ message: 'Failed to load data', type: 'error' });
}
}
loadDashboard();
</script>
</body>
</html>Once you save these two files, DenchClaw will detect the new app within a few seconds and add it to your sidebar. Click the bar chart icon to open your dashboard.
Step 3: Add Interactivity#
Static dashboards are useful. Interactive ones are better. Here's how to add a date range filter and real-time updates:
// Add to your <script> section
// Filter state
let filterDays = 30;
// Refresh when filter changes
document.getElementById('filter-select').addEventListener('change', (e) => {
filterDays = parseInt(e.target.value);
loadDashboard();
});
// Subscribe to real-time entry changes
dench.events.on('entry:created', (event) => {
if (event.objectName === 'deals') {
loadDashboard(); // Refresh when a new deal is added
}
});
// Persist filter preference
async function saveFilter() {
await dench.store.set('dashboardFilterDays', filterDays);
}
async function loadFilter() {
filterDays = await dench.store.get('dashboardFilterDays') ?? 30;
}
// Call on init
loadFilter().then(loadDashboard);The dench.events.on subscription means your dashboard updates automatically when someone adds a new deal — no manual refresh needed.
Step 4: Using the AI Agent Inside Your App#
Dench Apps can talk to the DenchClaw AI agent directly. This is powerful for workflow-specific AI tools:
// Example: AI deal analysis panel
async function analyzeDeal(dealId) {
const deal = await dench.db.query(
`SELECT * FROM v_deals WHERE id = ${dealId}`
);
const company = await dench.db.query(
`SELECT * FROM v_companies WHERE "Name" = '${deal[0]?.Company}'`
);
const session = await dench.chat.createSession({
systemPrompt: `You are a deal coach analyzing a sales opportunity.
Deal: ${JSON.stringify(deal[0])}
Company: ${JSON.stringify(company[0])}
Be specific, practical, and brief.`
});
const analysis = await dench.chat.send(
session.id,
'What are the 3 highest-priority actions to close this deal?'
);
document.getElementById('analysis').textContent = analysis.text;
}Using the AI Agent to Build Your App#
You don't have to write all this code yourself. DenchClaw's AI agent can build apps for you.
Open web chat or your connected Telegram/WhatsApp and say:
"Build me a Dench App that shows a weekly activity report — emails sent, calls logged, and new contacts added, grouped by day for the last 30 days. Use Chart.js with a dark theme."
The agent will:
- Read the App Builder skill documentation
- Create the
.dench.appfolder in your workspace - Write the
.dench.yamlmanifest - Write the
index.htmlwith appropriate SQL queries and Chart.js rendering - Tell you the app is ready in your sidebar
For more complex apps, you can give the agent an iterative brief:
"Build me a lead enrichment app. It should show all leads with empty Company fields, let me click each one to search for their LinkedIn profile using the browser agent, and save the company info back to DuckDB."
The agent will build a more complex app with db.write permissions, a data table, and integration with the browser automation layer.
Common App Patterns#
Data Table with Actions#
async function renderTable() {
const rows = await dench.db.query(
`SELECT id, "Full Name", "Email", "Company", "Status"
FROM v_people WHERE "Status" = 'Lead'
ORDER BY "Created At" DESC LIMIT 50`
);
const tbody = document.querySelector('tbody');
tbody.innerHTML = rows.map(row => `
<tr>
<td>${row['Full Name']}</td>
<td>${row['Email']}</td>
<td>${row['Company'] ?? '—'}</td>
<td>
<button onclick="enrichLead(${row.id})">Enrich</button>
</td>
</tr>
`).join('');
}
async function enrichLead(id) {
await dench.ui.toast({ message: 'Enriching...', type: 'info' });
// Call external API or agent tool
const result = await dench.http.fetch(`https://api.yourservice.com/enrich?id=${id}`);
// Update the entry
await dench.db.query(`
UPDATE entry_fields
SET value = '${result.company}'
WHERE entry_id = ${id} AND field_id = (
SELECT id FROM fields WHERE name = 'Company' LIMIT 1
)
`);
await dench.ui.toast({ message: 'Enriched!', type: 'success' });
renderTable(); // Refresh
}CSV Importer#
<input type="file" id="csv-file" accept=".csv">
<div id="preview"></div>
<button onclick="importData()">Import</button>
<script>
document.getElementById('csv-file').addEventListener('change', async (e) => {
const text = await e.target.files[0].text();
const rows = parseCSV(text);
document.getElementById('preview').innerHTML =
`<p>${rows.length} rows found. Fields: ${Object.keys(rows[0]).join(', ')}</p>`;
window._importRows = rows;
});
async function importData() {
if (!window._importRows) return;
let imported = 0;
for (const row of window._importRows) {
// Use the objects API for safe entry creation
await dench.objects.createEntry('people', {
'Full Name': row.name,
'Email': row.email,
'Company': row.company
});
imported++;
}
await dench.ui.toast({
message: `Imported ${imported} contacts`,
type: 'success'
});
}
</script>Deploying to Widget Mode#
Once your app is working as a full-page view, adding widget support is straightforward:
# In .dench.yaml
display:
defaultView: fullscreen
widget:
enabled: true
width: 3
height: 2
refreshMs: 60000// In index.html, detect widget mode
const isWidget = window.self !== window.top &&
new URLSearchParams(window.location.search).get('mode') === 'widget';
if (isWidget) {
// Render compact version
renderWidget();
} else {
// Render full dashboard
renderDashboard();
}Troubleshooting#
App doesn't appear in sidebar: Check that the folder name ends in .dench.app and .dench.yaml is valid YAML. Restart DenchClaw if needed.
dench is not defined: The bridge is only injected when the app runs inside DenchClaw's iframe. During local development in a browser tab, window.dench won't be available. Test inside DenchClaw.
Query returns no data: Run your SQL directly in the DuckDB shell to verify it's correct: duckdb ~/.openclaw-dench/workspace/workspace.duckdb.
CORS errors: Don't use fetch() directly for external APIs. Use dench.http.fetch() instead — it proxies through DenchClaw's server.
Frequently Asked Questions#
Can I use React or Next.js for a Dench App?#
Yes, but you'll need to build your app first and serve the build output as the index.html and associated assets. For most use cases, vanilla JS or a CDN-loaded library (like React from a CDN) is simpler and works without a build step.
Can multiple people share the same Dench App?#
Apps live in the workspace, which is currently single-user. When team workspaces ship, apps will be shared within a team automatically. For now, you can share .dench.app folders on GitHub.
Is there a limit on the number of apps?#
No hard limit. DenchClaw scans the workspace for .dench.app folders at startup and watches for new ones. Add as many as you want.
Can apps write to the CRM database?#
Yes, with db.write permission. Be careful — you're writing to your actual CRM data. Test with a backup or a test object first.
Related: What is a Dench App? → | The Bridge API explained → | Dench App examples →
Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →
