en un clic
app-builder
// Build and manage DenchClaw apps — self-contained web applications that run inside the workspace with access to DuckDB data, workspace objects, AI chat, and the full DenchClaw platform API.
// Build and manage DenchClaw apps — self-contained web applications that run inside the workspace with access to DuckDB data, workspace objects, AI chat, and the full DenchClaw platform API.
| name | app-builder |
| description | Build and manage DenchClaw apps — self-contained web applications that run inside the workspace with access to DuckDB data, workspace objects, AI chat, and the full DenchClaw platform API. |
| metadata | {"openclaw":{"inject":true,"always":true,"emoji":"🔨"}} |
You can build Dench Apps — self-contained web applications that run inside DenchClaw's workspace. Apps appear in the sidebar with their own icon and name, and open as tabs in the main content area. They run in a sandboxed iframe with allow-same-origin allow-scripts allow-popups allow-forms.
Every app is a folder ending in .dench.app/. The default location is /Users/collinpfeifer/.openclaw-dench/workspace/apps/, but apps can live anywhere in the workspace.
apps/
my-app.dench.app/
.dench.yaml # Required manifest
index.html # Entry point
style.css # Styles (optional, can inline)
app.js # Logic (optional, can inline)
assets/ # Images, sounds, models, etc.
sprite.png
bg-music.mp3
lib/ # Vendored libraries (optional)
p5.min.js
.dench.app.dench.yaml manifest is REQUIRED inside every .dench.app folderwindow.dench) auto-injected before </head>.dench.app folder root/api/apps/serve/<appPath>/<filePath> — relative references (CSS, JS, images) resolve correctlyallow-same-origin allow-scripts allow-popups allow-formsEvery .dench.app folder MUST contain a .dench.yaml manifest.
name: "My App" # Required. Display name shown in sidebar and tab bar
description: "What this app does" # Optional. Shown in tooltips and app info
icon: "gamepad-2" # Optional. Lucide icon name OR relative path to image
version: "1.0.0" # Optional. Shown as badge in app header
author: "agent" # Optional. Creator attribution
entry: "index.html" # Optional. Main entry point (default: index.html)
runtime: "static" # Optional. static | esbuild | build (default: static)
display: "full" # Optional. "full" (default) | "widget"
widget: # Only used when display: "widget"
width: 2 # Grid columns (1-4)
height: 1 # Grid rows (1-4)
refreshInterval: 60 # Auto-refresh seconds (optional)
permissions: # Optional. List of bridge API permissions
- database # db.query (SELECT only)
- database:write # db.execute (INSERT/UPDATE/DELETE/CREATE)
- objects # objects.* CRUD on workspace tables
- files # files.read, files.list
- files:write # files.write, files.delete, files.mkdir
- agent # chat.*, agent.send, tool.register, memory.get
- ui # ui.toast, ui.navigate, ui.openEntry, etc.
- store # store.* per-app KV storage
- http # http.fetch CORS proxy
- events # events.on/off real-time subscriptions
- apps # apps.send/on inter-app messaging
- cron # cron.schedule/list/cancel
- webhooks # webhooks.register/on
- clipboard # clipboard.read/write
tools: # Optional. Expose app functions as agent-invokable tools
- name: "my-tool"
description: "What this tool does"
inputSchema:
type: object
properties:
input: { type: string }
required: ["input"]
| Mode | When to Use | How It Works |
|---|---|---|
static | Vanilla HTML/CSS/JS apps, CDN-loaded libraries, games, dashboards | Serves files directly. Use this by default for everything. |
esbuild | React/TSX apps without npm dependencies | Server-side esbuild transpiles JSX/TSX on load. Requires esbuild.entry and esbuild.jsx fields. |
build | Complex apps with npm dependencies (rare) | Runs build.install then build.command. Serves from build.output directory. |
Always default to static runtime. It handles p5.js, Three.js, D3.js, Chart.js, and any CDN-loaded library perfectly. Only use esbuild or build when the user explicitly asks for React/TSX or npm-based tooling.
The icon field accepts:
"gamepad-2", "bar-chart-3", "users", "rocket", "calculator", "box", "palette""icon.png", "assets/logo.svg"Supported image formats: PNG, SVG, JPG, JPEG, WebP. Use square aspect ratio (128x128px or larger).
| Permission | Grants | Use When |
|---|---|---|
database | dench.db.query() | App reads workspace DuckDB data (SELECT) |
database:write | dench.db.execute() | App writes to DuckDB (INSERT/UPDATE/DELETE/CREATE) |
objects | dench.objects.* | App does CRUD on workspace objects (people, tasks, etc.) |
files | dench.files.read(), dench.files.list() | App reads workspace files |
files:write | dench.files.write(), dench.files.delete(), dench.files.mkdir() | App writes/deletes workspace files |
agent | dench.chat.*, dench.agent.send(), dench.tool.*, dench.memory.* | App interacts with the AI agent |
ui | dench.ui.* | App shows toasts, navigates, opens entries |
store | dench.store.* | App needs persistent key-value storage |
http | dench.http.fetch() | App fetches external URLs (CORS-free) |
events | dench.events.* | App subscribes to real-time workspace events |
apps | dench.apps.* | App communicates with other open apps |
cron | dench.cron.* | App schedules recurring agent tasks |
webhooks | dench.webhooks.* | App receives external webhooks |
clipboard | dench.clipboard.* | App reads/writes the clipboard |
Only request what you need. A game with no data access needs no permissions at all.
The bridge SDK is auto-injected into every app's HTML. It provides window.dench with the following namespaces. All methods return Promises with a 30-second timeout.
| Namespace | Permission | Methods | Details In |
|---|---|---|---|
dench.db | database / database:write | query(sql), execute(sql) | data-builder |
dench.objects | objects | list(), get(), create(), update(), delete(), bulkDelete(), getSchema(), getOptions() | data-builder |
dench.files | files / files:write | read(), list(), write(), delete(), mkdir() | below |
dench.app | (none) | getManifest(), getTheme() | below |
dench.chat | agent | createSession(), send(), getHistory(), getSessions(), abort(), isActive() | agent-builder |
dench.agent | agent | send(message) | agent-builder |
dench.tool | agent | register(name, handler) | agent-builder |
dench.memory | agent | get() | agent-builder |
dench.ui | ui | toast(), navigate(), openEntry(), setTitle(), confirm(), prompt() | platform-api |
dench.store | store | get(), set(), delete(), list(), clear() | platform-api |
dench.http | http | fetch(url, opts) | platform-api |
dench.events | events | on(channel, cb), off(channel) | platform-api |
dench.context | (none) | getWorkspace(), getAppInfo() | platform-api |
dench.apps | apps | send(), on(), list() | platform-api |
dench.cron | cron | schedule(), list(), run(), cancel() | platform-api |
dench.webhooks | webhooks | register(), on(), poll() | platform-api |
dench.clipboard | clipboard | read(), write() | platform-api |
// Get the app's own parsed manifest
const manifest = await dench.app.getManifest();
// Get current DenchClaw UI theme
const theme = await dench.app.getTheme();
// Returns: "dark" or "light"
files / files:write permission)// Read a workspace file
const content = await dench.files.read("path/to/file.md");
// List workspace directory tree (optionally scoped to a directory)
const tree = await dench.files.list();
const subTree = await dench.files.list("documents/");
// Write a file (files:write permission)
await dench.files.write("path/to/file.md", "# Hello\n\nFile content here.");
// Delete a file (files:write permission)
await dench.files.delete("path/to/old-file.md");
// Create a directory (files:write permission)
await dench.files.mkdir("path/to/new-dir");
The bridge script is injected into <head>, so it's available by the time your scripts run. However, if you use defer or type="module" scripts, you can safely access window.dench immediately since module scripts run after the document is parsed.
function whenDenchReady(fn) {
if (window.dench) return fn();
const check = setInterval(() => {
if (window.dench) { clearInterval(check); fn(); }
}, 50);
}
whenDenchReady(async () => {
const theme = await dench.app.getTheme();
document.body.className = theme;
});
Apps should respect the DenchClaw theme. The bridge provides the current theme ("dark" or "light"). Build your CSS to support both.
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background-color 0.2s, color 0.2s;
}
body.dark {
--app-bg: #0f0f1a;
--app-surface: #1a1a2e;
--app-surface-hover: #252540;
--app-border: #2a2a45;
--app-text: #e8e8f0;
--app-text-muted: #8888a8;
--app-accent: #6366f1;
--app-accent-hover: #818cf8;
--app-success: #22c55e;
--app-warning: #f59e0b;
--app-error: #ef4444;
background: var(--app-bg);
color: var(--app-text);
}
body.light {
--app-bg: #ffffff;
--app-surface: #f8f9fa;
--app-surface-hover: #f0f1f3;
--app-border: #e2e4e8;
--app-text: #1a1a2e;
--app-text-muted: #6b7280;
--app-accent: #6366f1;
--app-accent-hover: #4f46e5;
--app-success: #16a34a;
--app-warning: #d97706;
--app-error: #dc2626;
background: var(--app-bg);
color: var(--app-text);
}
Always apply the theme as the first action in your app:
async function initTheme() {
try {
const theme = await dench.app.getTheme();
document.body.className = theme;
} catch {
document.body.className = 'dark';
}
}
initTheme();
For p5.js, Three.js, or any canvas-based app, set the canvas background based on theme and make sure the body has no scrollbars:
body {
margin: 0;
padding: 0;
overflow: hidden;
width: 100vw;
height: 100vh;
}
canvas {
display: block;
}
Since apps use runtime: "static", load libraries via CDN <script> tags. The app iframe allows external script loading.
Use unpkg or cdnjs for reliability:
<!-- p5.js -->
<script src="https://unpkg.com/p5@1/lib/p5.min.js"></script>
<!-- Three.js -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.170/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.170/examples/jsm/"
}
}
</script>
<!-- D3.js -->
<script src="https://unpkg.com/d3@7/dist/d3.min.js"></script>
<!-- Chart.js -->
<script src="https://unpkg.com/chart.js@4/dist/chart.umd.min.js"></script>
<!-- Tone.js (audio) -->
<script src="https://unpkg.com/tone@15/build/Tone.js"></script>
<!-- Matter.js (2D physics) -->
<script src="https://unpkg.com/matter-js@0.20/build/matter.min.js"></script>
<!-- cannon-es (3D physics) -->
<script type="module">
import * as CANNON from 'https://unpkg.com/cannon-es@0.20/dist/cannon-es.js';
</script>
<!-- GSAP (animation) -->
<script src="https://unpkg.com/gsap@3/dist/gsap.min.js"></script>
<!-- Howler.js (audio) -->
<script src="https://unpkg.com/howler@2/dist/howler.min.js"></script>
For Three.js and other module-based libraries, use import maps:
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.170/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.170/examples/jsm/"
}
}
</script>
<script type="module" src="app.js"></script>
For complex apps, split code across multiple files:
apps/complex-app.dench.app/
.dench.yaml
index.html
css/
main.css
components.css
js/
app.js # Entry point
game.js # Game logic
renderer.js # Rendering
ui.js # UI overlays
utils.js # Helpers
assets/
sprites/
sounds/
models/
<script type="module" src="js/app.js"></script>
// js/app.js
import { Game } from './game.js';
import { Renderer } from './renderer.js';
import { UI } from './ui.js';
const game = new Game();
const renderer = new Renderer(game);
const ui = new UI(game);
async function init() {
if (window.dench) {
const theme = await dench.app.getTheme();
renderer.setTheme(theme);
}
game.start();
}
init();
// js/game.js
export class Game {
constructor() {
this.state = 'menu';
this.score = 0;
this.entities = [];
}
start() { this.state = 'playing'; this.loop(); }
loop() {
this.update();
requestAnimationFrame(() => this.loop());
}
update() { /* game logic */ }
}
Relative imports (./game.js) work because all files are served from the same /api/apps/serve/ base path.
All asset paths are relative to the .dench.app folder root:
// In p5.js
let img;
function preload() {
img = loadImage('assets/player.png');
}
// In Three.js (module)
const texture = new THREE.TextureLoader().load('assets/texture.jpg');
// In HTML
// <img src="assets/logo.png" />
// <audio src="assets/music.mp3"></audio>
The file server recognizes these extensions automatically:
| Extension | MIME Type |
|---|---|
.html, .htm | text/html |
.css | text/css |
.js, .mjs | application/javascript |
.json | application/json |
.png | image/png |
.jpg, .jpeg | image/jpeg |
.gif | image/gif |
.svg | image/svg+xml |
.webp | image/webp |
.woff, .woff2 | font/woff, font/woff2 |
.ttf, .otf | font/ttf, font/otf |
.wasm | application/wasm |
.mp3, .wav, .ogg | Served as application/octet-stream (works fine for <audio> and Howler) |
For games without pre-made art, generate sprites and textures programmatically:
// p5.js: Create a sprite at runtime
function createPlayerSprite(size) {
const g = createGraphics(size, size);
g.noStroke();
g.fill('#6366f1');
g.ellipse(size / 2, size / 2, size * 0.8);
g.fill('#818cf8');
g.ellipse(size / 2, size / 3, size * 0.3);
return g;
}
// Three.js: Create a texture from canvas
function createCheckerTexture(size = 256, divisions = 8) {
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
const cellSize = size / divisions;
for (let y = 0; y < divisions; y++) {
for (let x = 0; x < divisions; x++) {
ctx.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#cccccc';
ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
}
}
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
return texture;
}
runtime: "static" unless explicitly asked for React/TSX/npm.dench.app folderrequestAnimationFrame for all animation loops (p5.js does this automatically)pixelDensity(1) for pixel-art or retro-style games to avoid unnecessary high-DPI renderingnoSmooth() for pixel-art aestheticscreateGraphics() calls — create off-screen buffers once and reusep.frameRate(60) explicitly to cap FPSp.millis() or p.deltaTime for time-based movement instead of frame-basedrenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))geometry.dispose(), material.dispose(), texture.dispose()BufferGeometry (the default in modern Three.js)BufferGeometryUtils.mergeGeometries() for large scenesInstancedMesh) for many identical objects (trees, particles)THREE.LODfunction dispose() {
renderer.dispose();
scene.traverse((obj) => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
else obj.material.dispose();
}
});
}
Always wrap bridge calls in try/catch:
async function loadData() {
try {
const result = await dench.db.query("SELECT * FROM objects");
return result.rows || [];
} catch (err) {
console.error('Failed to load data:', err.message);
showError('Could not load workspace data. Check permissions.');
return [];
}
}
function showLoading(message = 'Loading...') {
const el = document.getElementById('loading');
if (el) { el.textContent = message; el.style.display = 'flex'; }
}
function hideLoading() {
const el = document.getElementById('loading');
if (el) el.style.display = 'none';
}
function showError(message) {
const el = document.getElementById('error');
if (el) { el.textContent = message; el.style.display = 'block'; }
}
async function init() {
try {
const theme = await dench.app.getTheme();
document.body.className = theme;
} catch {
document.body.className = 'dark';
}
try {
const data = await dench.db.query("SELECT * FROM objects");
renderDashboard(data.rows);
} catch {
renderEmptyState('No data available. Make sure the app has database permission.');
}
}
When asked to build an app, follow these steps:
dench.chat.* API — see agent-builder child skillapps/<name>.dench.app/.dench.yaml with manifest (always include name, entry, runtime, and needed permissions)index.html as the entry point with CDN script tagsdench.app.getTheme() on initresizeCanvas / update renderer)This skill covers app fundamentals. For specialized APIs, see these child skills (all inside the app-builder/ skill folder):
| Skill | Path | Covers |
|---|---|---|
| Game Builder | app-builder/game-builder/SKILL.md | 2D games with p5.js, 3D games with Three.js, physics (Matter.js), audio, sprites, particles, tilemaps, game state machines, complete game examples |
| Data Builder | app-builder/data-builder/SKILL.md | Workspace objects CRUD (dench.objects.*), DuckDB queries and mutations (dench.db.*), Chart.js and D3.js dashboards, stat cards, interactive tools, CRUD form patterns |
| Agent Builder | app-builder/agent-builder/SKILL.md | AI chat API (dench.chat.*), streaming responses, app-as-tool (dench.tool.*), agent memory access, Gateway WebSocket protocol, chat UI patterns |
| Platform API | app-builder/platform-api/SKILL.md | UI integration (dench.ui.*), per-app KV store (dench.store.*), HTTP proxy (dench.http.*), real-time events (dench.events.*), inter-app messaging (dench.apps.*), cron scheduling (dench.cron.*), webhooks (dench.webhooks.*), clipboard (dench.clipboard.*), widget mode, context |
All child skills are seeded into the workspace alongside this parent skill and can be read at /Users/collinpfeifer/.openclaw-dench/workspace/skills/app-builder/<child>/SKILL.md.
Connected app tool recipes for Composio integrations (Gmail, Slack, GitHub, Notion, Google Calendar, Linear)
Manage DuckDB CRM data, aggressive relation-linked fields, and synced markdown documents in the workspace. Use when creating or updating objects, fields, entries, foreign-table links, row notes, or entry-linked edit logs.
Find and connect with engineers working on ELK/observability stacks at ICP companies. Use when: (1) Researching individual contributors at a company that passed ICP company research, (2) Finding engineers with ELK/platform/SRE skills, (3) Verifying someone's work involves ELK/logstash/observability, (4) Connecting with practitioners who manage the stack day-to-day, (5) Discovering new ICP companies through engineer profiles. Workflow: Company from CRM → DuckDuckGo → PinchTab → LinkedIn MCP (verify ELK in skills/experience + connect) → Add to engineers CRM. Flipped discovery: If engineer has strong ELK signals but company isn't in CRM → add company as 'Discovered'. This skill DOES connect on LinkedIn.
Find and connect with team leads/VPs running observability/ELK stacks at ICP companies. Use when: (1) Researching decision-makers at a company that passed ICP company research, (2) Finding VP/Director/Team Lead contacts (VP Platform, Director SRE, Infrastructure Lead), (3) Verifying someone's role includes observability/platform, (4) Connecting with buyers who have budget authority, (5) Discovering new ICP companies through team lead profiles. Workflow: Company from CRM → DuckDuckGo → PinchTab → LinkedIn MCP (verify + connect) → Add to team_leads CRM. Flipped discovery: If team lead runs platform/observability but company isn't in CRM → add company as 'Discovered'. This skill DOES connect on LinkedIn.
Debugging CRM objects not showing entries despite data existing in database
Research companies against ICP criteria for ELK/observability tools. Use when: (1) Finding companies that match ICP (MSSPs, SOC-as-a-service, SIEM providers, cybersecurity where observability IS their product), (2) Evaluating if a company fits the ideal customer profile, (3) Researching company size, business model, and ELK criticality, (4) Building prospect lists for outreach. ICP: Small-medium companies (≤200 employees) where observability/ELK is mission-critical to revenue. Use DuckDuckGo first, then PinchTab for deeper research, then LinkedIn MCP ONLY for company/people lookup (never for outreach).