App Platform API
This skill documents the platform-level APIs available to DenchClaw apps. For core app structure, see the parent app-builder skill. For data/objects, see data-builder. For AI chat, see agent-builder.
UI Integration (ui permission required)
// Show a toast notification in the parent DenchClaw UI
await dench.ui.toast("Record saved successfully", { type: "success" });
await dench.ui.toast("Something went wrong", { type: "error" });
await dench.ui.toast("Processing...", { type: "info" });
// Navigate DenchClaw to a workspace path (opens object, file, or app)
await dench.ui.navigate("/people"); // open the people object
await dench.ui.navigate("/apps/my-app.dench.app"); // open another app
// Open an entry detail modal
await dench.ui.openEntry("people", "entry_id_here");
// Update the app's tab title dynamically
await dench.ui.setTitle("My App — 5 results");
// Show a confirmation dialog
const confirmed = await dench.ui.confirm("Delete this record?");
if (confirmed) { /* proceed */ }
// Show a prompt dialog
const name = await dench.ui.prompt("Enter a name:", "Default Name");
if (name !== null) { /* use name */ }
Per-App KV Store (store permission required)
Persistent key-value storage scoped to each app. Data survives app reloads and is stored in the workspace.
// Store a value (any JSON-serializable value)
await dench.store.set("lastQuery", { sql: "SELECT * FROM people", timestamp: Date.now() });
await dench.store.set("theme", "custom-dark");
await dench.store.set("counter", 42);
// Read a value
const lastQuery = await dench.store.get("lastQuery");
// Returns the value, or null if not found
// Delete a key
await dench.store.delete("lastQuery");
// List all keys
const keys = await dench.store.list();
// Returns ["theme", "counter"]
// Clear all stored data
await dench.store.clear();
Storage is backed by a JSON file at {workspace}/.dench-app-data/{appName}/store.json. Each app gets its own isolated namespace.
HTTP Proxy (http permission required)
Make HTTP requests from apps without CORS restrictions. Requests are proxied through the DenchClaw server.
// Simple GET
const data = await dench.http.fetch("https://api.example.com/data");
// Returns { status, statusText, headers, body }
// POST with headers and body
const result = await dench.http.fetch("https://api.example.com/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer sk-..."
},
body: JSON.stringify({ query: "test" })
});
console.log(result.status); // 200
console.log(result.body); // response body as string
Security: requests to localhost, private IPs, and internal DenchClaw URLs are blocked.
Real-time Events (events permission required)
Subscribe to workspace events for live updates.
// Subscribe to theme changes
dench.events.on("theme.changed", (data) => {
document.body.className = data.theme;
});
// Subscribe to object entry changes
dench.events.on("object.entry.created", (data) => {
console.log(`New entry in ${data.objectName}: ${data.entryId}`);
refreshList();
});
dench.events.on("object.entry.updated", (data) => {
console.log(`Entry ${data.entryId} updated in ${data.objectName}`);
});
dench.events.on("object.entry.deleted", (data) => {
console.log(`Entry ${data.entryId} deleted from ${data.objectName}`);
});
// App visibility events
dench.events.on("app.visible", () => { resumeAnimations(); });
dench.events.on("app.hidden", () => { pauseAnimations(); });
// File change events
dench.events.on("file.changed", (data) => {
console.log(`File changed: ${data.path}`);
});
// Unsubscribe
dench.events.off("theme.changed");
Context (no permission required)
// Get workspace info
const workspace = await dench.context.getWorkspace();
// Returns { name, path, agentId }
// Get app info
const app = await dench.context.getAppInfo();
// Returns { appPath, folderName, permissions, manifest }
Inter-App Messaging (apps permission required)
Apps can communicate with other open apps for composite workflows.
// Send a message to another app
await dench.apps.send("analytics-dashboard.dench.app", {
action: "refresh",
filter: { status: "Active" }
});
// Listen for messages from other apps
dench.apps.on("message", (event) => {
console.log(`Message from ${event.from}:`, event.message);
if (event.message.action === "refresh") {
reloadData(event.message.filter);
}
});
// List currently active (open) apps
const activeApps = await dench.apps.list();
// Returns [{ name: "analytics-dashboard.dench.app", manifest: {...} }, ...]
Cron Scheduling (cron permission required)
Schedule recurring tasks that send messages to the agent.
// Schedule a cron job
const { jobId } = await dench.cron.schedule({
expression: "0 9 * * *", // 9 AM daily
message: "Generate the daily sales report and save it to workspace",
channel: "announce" // "announce" (delivers result) or "none" (silent)
});
// List all cron jobs
const jobs = await dench.cron.list();
// Returns array of { id, expression, message, enabled, nextRunAt, ... }
// Run a job immediately
await dench.cron.run(jobId);
// Cancel/remove a job
await dench.cron.cancel(jobId);
Webhooks (webhooks permission required)
Receive external HTTP webhooks inside your app.
// Register a webhook endpoint
const hook = await dench.webhooks.register("github-push");
console.log(hook.url);
// e.g. "https://your-denchclaw-host/api/apps/webhooks/my-app.dench.app/github-push"
// Listen for incoming webhooks
dench.webhooks.on("github-push", (payload) => {
console.log("Received webhook:", payload);
// payload: { method: "POST", headers: {...}, body: "...", receivedAt: 1234567890 }
processGithubPush(JSON.parse(payload.body));
});
// Poll for webhooks (useful for catching events received while app was closed)
const events = await dench.webhooks.poll("github-push", { since: lastTimestamp });
Clipboard (clipboard permission required)
// Write to clipboard
await dench.clipboard.write("Copied text content");
// Read from clipboard
const text = await dench.clipboard.read();
Note: clipboard operations are proxied through the parent DenchClaw window.
Widget Mode
Apps can render as compact widgets in a dashboard grid instead of full-page tabs.
Manifest Configuration
name: "Quick Stats"
display: "widget"
widget:
width: 2 # Grid columns (1-4)
height: 1 # Grid rows (1-4)
refreshInterval: 60 # Auto-refresh in seconds (optional)
permissions:
- database
Widget Design Guidelines
- Keep the UI compact — widgets have limited space
- Use large, readable numbers and minimal text
- Avoid scroll bars — all content should be visible
- Support both light and dark themes
- Use the refresh interval for auto-updating data
Widget Example
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, sans-serif;
padding: 16px;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
}
body.dark { background: #0f0f1a; color: #e8e8f0; }
body.light { background: #fff; color: #1a1a2e; }
.metric { font-size: 48px; font-weight: 700; }
.label { font-size: 13px; opacity: 0.6; margin-bottom: 4px; }
</style>
</head>
<body>
<div class="label">Total Records</div>
<div class="metric" id="count">—</div>
<script>
async function init() {
const theme = await dench.app.getTheme().catch(() => "dark");
document.body.className = theme;
const result = await dench.db.query("SELECT SUM(entry_count) as total FROM objects");
document.getElementById("count").textContent = result.rows[0]?.total ?? 0;
}
init();
</script>
</body>
</html>
Widget-mode apps appear in the DenchClaw dashboard view alongside other widgets, arranged in a responsive grid.
Patterns
Multi-App Dashboard
Build a dashboard that aggregates data from multiple widget apps:
// In the main dashboard app
async function loadWidgetData() {
const apps = await dench.apps.list();
const widgets = apps.filter(a => a.manifest.display === "widget");
for (const widget of widgets) {
// Request data from each widget app
await dench.apps.send(widget.name, { action: "getData" });
}
}
dench.apps.on("message", (event) => {
if (event.message.action === "dataResponse") {
updateDashboardPanel(event.from, event.message.data);
}
});
External API Integration
// Fetch data from an external API via proxy
async function loadWeather(city) {
const result = await dench.http.fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=YOUR_KEY`
);
if (result.status === 200) {
const weather = JSON.parse(result.body);
displayWeather(weather);
}
}
Automation Workflow
// Set up a cron job that processes data and sends results
async function setupAutomation() {
const { jobId } = await dench.cron.schedule({
expression: "0 */6 * * *",
message: "Check the tasks object for overdue items and send a summary to Telegram"
});
await dench.store.set("automationJobId", jobId);
dench.ui.toast("Automation scheduled every 6 hours", { type: "success" });
}