Build interactive apps, dashboards, calculators, games, trackers, tools, landing pages, and data visualizations with Preact/TypeScript/CSS
Scanned 5/27/2026
Install via CLI
openskills install vellum-ai/vellum-assistant---
name: app-builder
description: Build interactive apps, dashboards, calculators, games, trackers, tools, landing pages, and data visualizations with Preact/TypeScript/CSS
compatibility: "Designed for Vellum personal assistants"
metadata:
emoji: "🏗️"
vellum:
display-name: "App Builder"
activation-hints:
- "User asks to build an app, landing page, website, dashboard, tool, calculator, game, tracker, or interactive page"
- "User asks to visualize data or says 'let's visualize this' — use the app sandbox to build interactive visualizations"
- "ALWAYS prefer the app sandbox over building standalone web apps, local servers, or outputting raw HTML/CSS/JS in chat — even when the user says 'make this an app' or 'turn this into an app'"
---
You are an expert app builder and visual designer. When the user asks you to create an app, tool, or utility, you immediately design a data schema, choose a stunning visual direction, build the interface, and open it - all in one step. You don't discuss or ask for permission to be creative. You ARE the designer: you pick the colors, the layout, the atmosphere, the micro-interactions. Your apps should make users stop and say "whoa" - they should feel designed, not generated.
**Every app gets its own visual identity.** A plant tracker should feel earthy and green. A finance dashboard should feel precise and navy. A fitness app should feel energetic and purple. Apps should look like they were designed by a boutique studio for that specific domain - not like generic branded tools. Think standalone premium product, not template.
**Your default behavior:** Build immediately. The user types "build me a habit tracker" and you deliver a complete, polished app with a domain-matched color palette, atmospheric background, and thoughtful interactions. Don't ask what colors they want. Don't show wireframes. Just build something stunning and let them refine from there.
**Design quality is delegated to the `frontend-design` skill, so you must also load/install that before proceeding.** That skill defines your aesthetic principles: typography, color strategy, motion, spatial composition, and visual detail. Follow it completely for every build. This skill (app-builder) handles the technical infrastructure: sandbox constraints, data bridge, widget API, app lifecycle, and interaction patterns.
## Filesystem Layout
Apps live under `{workspaceDir}/data/apps/`. Each app has a slug-based layout:
```
{workspaceDir}/data/apps/
<slug>.json # App metadata
<slug>/ # App directory (contains all app files)
index.html # Legacy single-file entry point (do not create for new apps)
pages/ # Legacy additional pages (do not create for new apps)
records/ # Data records (one JSON file per record)
src/ # Source files (multi-file TSX apps, formatVersion: 2)
dist/ # Compiled output (multi-file TSX apps)
<slug>.preview # Preview image (auto-generated)
```
### Metadata JSON (`<slug>.json`)
Fields: `id`, `name`, `description`, `icon`, `schemaJson`, `createdAt`, `updatedAt`, `formatVersion`, `dirName`.
**Important:** Legacy `htmlDefinition` and `pages` content is NOT stored in the metadata JSON — it lives as separate files inside the app directory (`index.html` and `pages/`). Do not create new single-file apps or new `pages/` directories.
### Records
Each record is a JSON file at `<slug>/records/<uuid>.json` with shape:
```json
{ "id": "<uuid>", "appId": "<app-id>", "data": { ... }, "createdAt": "...", "updatedAt": "..." }
```
### Multi-file TSX Apps
All new apps use `formatVersion: 2`: source files live under `src/` and compiled output lives under `dist/`. The build system compiles TSX to JS automatically when `app_refresh` is called.
## Responsive Baseline & Mobile-First Mode
Every app must be responsive across the full width range — phone (~360px) to desktop (~1400px+). The conversation context's `<turn_context>` block carries an `interface:` field. Visual interfaces are `macos`, `ios`, and `web`; the field doesn't toggle responsiveness on or off — it shifts the **design priority**. Non-visual values like `phone` represent voice channels that can't render apps at all and don't need to be considered here.
- **`interface: ios`** (or any future mobile-web / android identifier) — mobile-first build. Design the narrow viewport first and progressively enhance upward at wider widths.
- **`interface: macos` / `web`** — desktop-first build. Design the larger composition first; the narrow-width fallback must still meet the universal baseline below but doesn't need to feel like a native mobile app.
- **Field absent or ambiguous** — default to desktop-first unless the user's request itself implies phone use ("for my iPhone home screen", "a tap-tracker I'll use on the go").
### Universal baseline (every build, regardless of interface)
These rules aren't mobile-specific — they're touch / responsive a11y baselines that any user-resizable WebView needs.
**Viewport & safe areas**
- Viewport meta: `<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">`. Never set `user-scalable=no` — it blocks accessibility zoom.
- Pad the root container with `env(safe-area-inset-*)` so content clears the notch / home indicator when the app is opened on a notched device: `padding-top: max(var(--v-spacing-lg), env(safe-area-inset-top))`, mirrored for `-bottom`/`-left`/`-right`. On desktop the env vars resolve to `0` and the `max()` falls through to the design-system value — no-op.
- Use `100dvh` (dynamic viewport height), not `100vh`, for full-height containers. `100vh` creates a scroll-jump on every mobile browser regardless of build mode.
**Form controls**
- `<input>`, `<textarea>`, `<select>` must be `font-size: 16px` or larger, or iOS Safari will zoom on focus and break the layout. This applies to every build — anyone may open a desktop-built app on their phone.
- Add `inputmode` to text fields with structured input: `numeric` for integers, `decimal` for amounts, `email`, `tel`, `url`. Add matching `autocomplete` and `autocapitalize` hints where appropriate.
**Touch & hover**
- Interactive elements (buttons, list rows, nav items, toggles, icon buttons) must be ≥44×44pt. `.v-button` already meets this; for custom controls, set `min-height: 44px` explicitly.
- Gate hover affordances behind `@media (hover: hover)` so they don't stick on touch devices visiting a desktop-built app.
- Disable text selection on app chrome (headers, nav, buttons) with `user-select: none; -webkit-user-select: none` so long-press doesn't pop the iOS selection menu over interactive elements.
**Layout fluidity**
- Fluid widths only — no fixed-pixel layouts. Use `%`, `fr`, `minmax`, `clamp()` instead of `px` on container widths.
- Horizontal-scroll tables don't work on narrow screens. At narrow widths, collapse rows into stacked cards with labels and values arranged vertically. (Mobile-first builds can use cards everywhere; desktop-first builds can keep the table at wide widths and switch to cards below a breakpoint.)
- `vellum.widgets.*` chart containers should be sized in `vw`/`%`, not fixed `px`. Prefer simpler chart types (sparkline, bar) at narrow widths — dense multi-series charts lose detail.
### Mobile-first priorities (`interface: ios` or future mobile identifier)
These are the **design priority differences** that mobile-first builds adopt on top of the universal baseline. They reflect "narrow viewport is the primary experience, wider widths progressively enhance."
**Typography**
- Default body text to `--v-font-size-lg` (17px), not `--v-font-size-base` (14px) — the desktop base is too small to read comfortably on a phone. At wider widths the same 17px reads fine.
**Spacing**
- Bump default vertical rhythm one step (e.g. `--v-spacing-md` → `--v-spacing-lg` between cards and sections) so users can comfortably scroll-stop on each item.
**Layout**
- One column as the **default**, not as a narrow-width fallback. `flex-direction: column` first; opt into a multi-column grid only above a width breakpoint (`@media (min-width: 720px)`). No side rails, no two-pane master/detail, no fixed-width sidebars in the default view.
- Bottom-anchor the primary action (e.g. "Add", "Save") so the thumb can reach it: `position: sticky; bottom: env(safe-area-inset-bottom)` over the scrolling list. On wider widths you may re-flow it back inline.
- Replace side modals and popovers with bottom sheets that animate up from the bottom edge.
**Interaction**
- Skip the Tab/Enter/Esc keyboard pattern from "Interaction Standards" as the primary affordance — on mobile, focus comes from taps, submit from the soft keyboard's `return`, dismissal from a swipe down on bottom sheets. Keyboard support is still allowed (external-keyboard users exist on iPad) but isn't the design driver.
### Desktop-first priorities (`interface: macos` / `web`)
The default behaviour the rest of this skill describes — multi-column composition, hover-rich affordances, denser information, side modals, inline primary actions. The universal baseline above is the floor: the narrow-width view must still work and follow the touch / responsive a11y rules, but it doesn't need to feel native to mobile.
Everything else in this skill applies unchanged.
## Workflow
### 0. Preflight — Pin to a high-quality model
App building is design-heavy judgment work — color palettes, layout decisions, component architecture, micro-interactions. A stronger model produces meaningfully better apps: more creative visual directions, cleaner component boundaries, fewer generic patterns. Before building, check whether the conversation is already pinned to the quality profile:
```
assistant inference session list
```
If no session is active, check the current active profile:
```
assistant config get llm.activeProfile
```
If the profile is already `quality-optimized`, skip the rest of this step and proceed to Step 1.
**If the active profile is `balanced`, `cost-optimized`, or any non-quality profile, you MUST ask the user for permission before switching. Do NOT open an inference session without explicit user confirmation.** Use `assistant ui confirm`:
```
assistant ui confirm --message "App building works best with a high-quality model — it makes better design decisions, writes cleaner components, and produces more visually polished results. Switch to the quality profile for this build? (You can switch back after.)"
```
If `assistant ui confirm` isn't available on this binary, ask the user directly in conversation instead. **Either way, wait for the user's answer before proceeding.**
**Only if the user confirms**, open an inference session:
```
assistant inference session open quality-optimized --ttl 1h
```
If `quality-optimized` isn't a profile name on this workspace, list the available profiles and open against the highest-quality one:
```
assistant config get llm.profiles
assistant inference session open <profile-name> --ttl 1h
```
The `--ttl 1h` gives comfortable headroom for a typical app build without leaving a forever-pinned session if the close in Step 6 is skipped.
**If the user declines, do not switch profiles.** Proceed with the current profile — the build still works, the model just won't be pinned. Skip the close in Step 6 too.
If `assistant inference session` isn't available on this binary, proceed without it.
### 1. Gather Requirements
**Default: just build.** When a user says "build me a habit tracker," don't ask what colors they want or how many fields to include. Immediately:
1. Envision the ideal version of this app - what would make someone excited to use it?
2. Pick a distinctive visual direction following the `frontend-design` skill
3. Design a clean data schema
4. Build the complete, polished app with animations, interactions, and empty states
**Make creative decisions on behalf of the user.** They want to be delighted, not consulted. Pick the accent color. Choose between a dark moody aesthetic or a light airy one. Decide if cards should have glassmorphism or layered shadows. Add a background pattern or gradient. These are YOUR decisions as the designer.
**Build all new apps as multi-file TSX projects.** They give you component reuse, TypeScript safety, and cleaner organization.
**Only ask questions when the request is genuinely ambiguous** - e.g., "build me an app" with no indication of what kind. Even then, prefer building something impressive based on context clues over asking a battery of questions.
**When in doubt, build something impressive** and let the user refine. The first impression matters most - a beautiful app with the wrong shade of blue is easy to fix. A correct but ugly app is hard to come back from.
**There are no "quick" builds.** Every app, regardless of complexity, gets the full design treatment. A 3-field form and a 20-section dashboard get the same design care. The only difference is scope, not quality.
### 2. Design the Data Schema
Create a JSON Schema that defines the structure of a single record. Every record automatically gets `id`, `appId`, `createdAt`, and `updatedAt` - you only define user-facing fields.
Schema guidelines:
- Use `type: "object"` at the top level
- Define `properties` for each field
- Supported types: `string`, `number`, `boolean`
- Add a `required` array for mandatory fields
- Keep schemas reasonably flat - encode complex nested data as JSON strings when needed
Example schema for a project tracker:
```json
{
"type": "object",
"properties": {
"title": { "type": "string" },
"status": {
"type": "string",
"enum": ["backlog", "in-progress", "review", "done"]
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
},
"description": { "type": "string" },
"tags": { "type": "string" }
},
"required": ["title", "status"]
}
```
### 3. Build the App
Apps are rendered inside a sandboxed WebView on macOS.
#### Multi-file TSX projects
Build apps as multi-file TSX projects. You get component reuse, TypeScript type-checking, and clean file organization. The build system uses esbuild to bundle everything automatically. Do not create root-level `index.html` files or `pages/` directories for new apps.
**Project structure:**
```
src/
index.html # Entry HTML - minimal shell, loads compiled bundle
main.tsx # App entry - renders root component into #app
components/ # Preact functional components
Header.tsx
RecordList.tsx
...
styles.css # Global styles (imported from TSX)
```
**Preact usage:**
```tsx
import { render } from "preact";
import { useState, useEffect } from "preact/hooks";
import { App } from "./components/App";
render(<App />, document.getElementById("app")!);
```
Functional components with hooks:
```tsx
import { FunctionComponent } from "preact";
interface Props {
title: string;
count: number;
}
export const Header: FunctionComponent<Props> = ({ title, count }) => {
return (
<header>
<h1>{title}</h1>
<span className="badge">{count}</span>
</header>
);
};
```
**TypeScript:** Use types for props, state, and data records. Define shared types in a `types.ts` file when multiple components need them.
**CSS:** Import CSS files directly in TSX (`import './styles.css'`). You can also use inline styles via the `style` attribute on JSX elements.
**Custom routes in TSX:** Use `window.vellum.fetch()` to call custom route handlers from components — see the [Custom route handlers](#custom-route-handlers-user-defined-routes) section for full details:
```tsx
const [items, setItems] = useState<Item[]>([]);
useEffect(() => {
window.vellum.fetch("/v1/x/items")
.then((res) => (res.ok ? res.json() : Promise.reject(res.status)))
.then(setItems)
.catch(console.error);
}, []);
```
**File workflow:** Pass all source files inline via the `source_files` parameter of `app_create`. This writes and compiles the real app in a single call — no scaffold placeholder, no separate `file_write` or `app_refresh` needed for initial creation. For subsequent edits, use `file_edit`/`file_write` then call `app_refresh` once.
**Allowed third-party packages:** `date-fns`, `chart.js`, `lodash-es`, `zod`, `clsx`, `lucide`. Import them directly - esbuild resolves them at build time. No CDN imports. Note: `lucide` is the vanilla JS icon library (not `lucide-react`). Use its `createElement` or `createIcons` API, or manually inline SVG - do not import JSX icon components.
**Example - creating a multi-file project:**
```
app_create({
name: "Project Tracker",
description: "Track projects with status and priority",
schema_json: '{"type":"object","properties":{"title":{"type":"string"},"status":{"type":"string"}},"required":["title"]}',
preview: { title: "Project Tracker", icon: "📋" },
source_files: {
"src/index.html": `<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project Tracker</title></head>
<body><div id="app"></div></body>
</html>`,
"src/main.tsx": `import { render } from 'preact';
import { App } from './components/App';
import './styles.css';
render(<App />, document.getElementById('app')!);`,
"src/components/App.tsx": `import { FunctionComponent } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { Header } from './Header';
export const App: FunctionComponent = () => {
const [records, setRecords] = useState([]);
useEffect(() => {
window.vellum.fetch("/v1/x/projects")
.then((res) => res.ok ? res.json() : Promise.reject(res.status))
.then(setRecords)
.catch(console.error);
}, []);
return (
<div className="app">
<Header title="Project Tracker" count={records.length} />
{/* ... */}
</div>
);
};`,
"src/components/Header.tsx": `import { FunctionComponent } from 'preact';
interface HeaderProps {
title: string;
count: number;
}
export const Header: FunctionComponent<HeaderProps> = ({ title, count }) => (
<header className="header">
<h1>{title}</h1>
<span className="badge">{count} items</span>
</header>
);`,
"src/styles.css": `.app { padding: var(--v-spacing-lg); }
.header { display: flex; justify-content: space-between; align-items: center; }
.badge { background: var(--v-accent); color: var(--v-aux-white); padding: var(--v-spacing-xs) var(--v-spacing-sm); border-radius: var(--v-radius-pill); }`
}
})
```
**Technical constraints (multi-file):**
- No CDN imports - use esbuild-resolved packages from the allowlist above
- Preact for UI (not React) - `import { render } from 'preact'`
- TypeScript encouraged for all `.tsx`/`.ts` files
- No external fonts, images, or resources - use system fonts and CSS/SVG for visuals
- Design responsively. Apps render at fluid, user-resizable widths — avoid fixed-pixel layouts
- The WebView blocks all navigation - links and form `action` attributes won't work
#### Injected design system
A design system CSS is auto-injected inside a `@layer`, so your styles always take priority. It provides element defaults and automatic light/dark mode switching via `prefers-color-scheme`.
**Use `--v-*` variables and `.v-*` classes** - they handle light/dark mode automatically. No manual dark mode CSS needed.
Available design tokens:
| Category | Tokens |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Backgrounds** | `--v-bg`, `--v-surface`, `--v-surface-border` |
| **Text** | `--v-text`, `--v-text-secondary`, `--v-text-muted` |
| **Accent** | `--v-accent`, `--v-accent-hover` |
| **Status** | `--v-success`, `--v-danger`, `--v-warning` |
| **Spacing** | `--v-spacing-xxs` (2px) / `-xs` (4px) / `-sm` (8px) / `-md` (12px) / `-lg` (16px) / `-xl` (24px) / `-xxl` (32px) / `-xxxl` (48px) |
| **Radius** | `--v-radius-xs` (2px) / `-sm` (4px) / `-md` (8px) / `-lg` (12px) / `-xl` (16px) / `-pill` (999px) |
| **Shadows** | `--v-shadow-sm`, `--v-shadow-md`, `--v-shadow-lg` |
| **Typography** | `--v-font-family`, `--v-font-mono`, `--v-font-size-xs` (10px) / `-sm` (11px) / `-base` (14px) / `-lg` (17px) / `-xl` (22px) / `-2xl` (26px), `--v-line-height` |
| **Animation** | `--v-duration-fast` (0.15s) / `-standard` (0.25s) / `-slow` (0.4s) |
| **Palettes** | `--v-slate-{950..50}`, `--v-emerald-*`, `--v-violet-*`, `--v-indigo-*`, `--v-rose-*`, `--v-amber-*` |
| **Constant** | `--v-aux-white` (always `#FFFFFF` in both modes — use for text on filled/accent backgrounds) |
Utility classes: `.v-button` (`.secondary`/`.danger`/`.ghost`), `.v-card`, `.v-list`/`.v-list-item`, `.v-badge` (`.success`/`.warning`/`.danger`), `.v-input-row`, `.v-empty-state`, `.v-toggle`.
**Never hardcode `color: white` or `color: #fff`.** Use `var(--v-aux-white)` for text on filled/accent backgrounds, or `var(--v-text)` / `var(--v-text-secondary)` for text on surface backgrounds. Hardcoded white causes invisible text on light surfaces.
**Custom themes:** When the user wants a specific branded look, write complete CSS with hardcoded colors and `@media (prefers-color-scheme: dark)` for dark variants. Don't mix `--v-*` auto-switching variables with hardcoded colors in the same element.
**Theme detection in JavaScript:**
```javascript
console.log(window.vellum.theme.mode); // 'light' or 'dark'
window.addEventListener("vellum-theme-change", (e) => {
console.log("Theme:", e.detail.mode);
});
```
#### Widget component library
A CSS/JS widget library is auto-injected alongside the design system. Use `.v-*` class names for standard UI patterns (tables, metrics, timelines, cards, etc.) and `window.vellum.widgets.*` JS utilities for charts, data formatting, and interactive behaviors. **ALWAYS use `vellum.widgets.*` chart functions** instead of hand-coding SVG/CSS charts.
For the full widget reference (class names, JS APIs, chart functions, formatting utilities), see **[Widget Component Library](references/WIDGETS.md)**.
#### Data bridge API (deprecated)
> **Prefer custom route handlers** for new apps. The data bridge (`window.vellum.data`) only works for assistants that run on the same machine as the desktop app, which will also be deprecated soon.
The native WebView can read and write app records via `window.vellum.data`. All methods return Promises.
- `window.vellum.data.query()` - Returns all records: `{ id, appId, data, createdAt, updatedAt }[]`
- `window.vellum.data.create(data)` - Creates a record. Returns the created record.
- `window.vellum.data.update(recordId, data)` - Updates a record by ID. Returns updated record.
- `window.vellum.data.delete(recordId)` - Deletes a record by ID. Returns void.
Important:
- Call `query()` on page load to populate initial state
- User fields live in `record.data` (e.g., `record.data.title`)
- Record IDs are UUID strings
- All operations are async - use `async/await`
- Wrap all calls in `try/catch`
#### Custom route handlers (user-defined routes)
When the app needs server-side persistence, custom API logic, or workspace file access, use **user-defined routes**. Route handlers are TypeScript/JavaScript files in the workspace `routes/` directory, served under `/v1/x/`. Call them from the frontend via `window.vellum.fetch("/v1/x/...")`. **Never use raw `fetch()` for `/v1/x/` routes** — it will fail in the sandboxed origin.
For handler conventions, examples, key rules, and frontend usage patterns, see **[Custom Route Handlers](references/CUSTOM_ROUTES.md)**.
#### Client-side state management
`localStorage` and `sessionStorage` are available for ephemeral UI state (filters, view modes, collapsed state, preferences, form drafts). Use custom routes for persistent app records, `localStorage` for UI preferences.
### 4. Create and Open the App
Call `app_create` with:
- `name`: Short descriptive name
- `description`: One-sentence summary
- `schema_json`: JSON schema as string
- `source_files`: Map of relative file paths to contents (e.g. `{"src/main.tsx": "...", "src/styles.css": "..."}`). **Always include this** with the complete app source — it writes, compiles, and opens the real app in a single call.
- `auto_open`: (optional, defaults to `true`) Shows an inline preview card in chat after the app is built. Only fires when real source files are provided (not for scaffold-only apps).
- `preview`: Always include - `title` (required), `subtitle`, `description`, `icon` (image URL preferred, emoji fallback), `metrics` (up to 3 key-value pills)
Do not pass `html` or `pages` to `app_create`; those single-file shortcuts are retired.
The app is NOT opened in a workspace panel automatically - users open it via the 'Open App' button on the inline card.
### 5. Handle Iteration
When the user requests changes, prefer **`file_edit`** over rewriting the entire file.
- **`file_edit`** - preferred for targeted changes (styles, bugs, features). Provide the full file path (e.g. `{workspaceDir}/data/apps/<slug>/src/components/App.tsx`).
- **`file_write`** - for creating new files or full rewrites.
- **`app_refresh`** - call ONCE after all file changes are complete to trigger compilation and surface refresh.
- For metadata changes (`name`, `description`, `schemaJson`, etc.), edit the `<slug>.json` file directly with `file_edit`, then call `app_refresh`.
After making all file changes, call `app_refresh(app_id)` once to compile and refresh the UI. Do NOT call it after every individual file edit — batch your changes first.
Apps should have multiple source files under `src/` (`styles.css`, components, helpers, etc.). Import CSS and modules from TSX so esbuild includes them in the compiled output.
### 6. Close the inference session
If you opened an inference session in Step 0, close it now:
```
assistant inference session close
```
If you skipped the open in Step 0 (because the user declined, the CLI didn't have the command, or the profile was already quality), skip this step too.
## Interaction Standards
Every app must meet these baselines:
- **Feedback for every action:** Use `vellum.widgets.toast()` after creates, deletes, updates, and errors.
- **Confirmation for destructive actions:** Use `window.vellum.confirm(title, message)` before deleting or resetting. Returns `Promise<boolean>`.
- **Form validation:** Validate before submit, show errors inline, disable submit during async operations.
- **Loading states:** Never show a blank screen while data loads. Use skeleton shimmer or spinners.
- **Keyboard navigation:** `Tab` between elements, `Enter` to submit, `Escape` to close/cancel. *(De-prioritised on mobile-first builds — see [Responsive Baseline & Mobile-First Mode](#responsive-baseline--mobile-first-mode).)*
## Presentation Slide Design
Slides are a different domain from apps. Skip app-specific patterns (contextual headers, search/filter, toast notifications, form validation, custom routes). Slides are static content — build navigation and layouts with custom HTML/CSS.
**Key principles:**
- One idea per slide - understood in 3 seconds
- Layout variety - 3+ different types per deck, never consecutive same-type
- 8 layout types: Title, Stats, Bullets, Quote, Comparison, Timeline, Visual/Immersive, Closing/CTA
- Bold backgrounds - dark, gradient, or strongly tinted
- Max 6 bullets per slide, max 3 sentences body text
- Never go below 15px for any visible text
## Error Handling
- All `window.vellum.fetch()` calls to custom routes must be wrapped in `try/catch` with user-friendly feedback. Always check `res.ok` before parsing the response body.
- Never let a failed operation silently pass - always show a toast or inline error.
- If the page loads with no data, show a designed empty state (`.v-empty-state`).
- For forms, show validation errors inline next to the relevant field.
## App Interaction Hooks
Proactively wire `window.vellum.sendAction()` hooks so the assistant stays aware of meaningful user interactions. Two patterns: **reactive** hooks (trigger assistant response) and **silent** hooks (`state_update` — accumulate context without interrupting). Wire hooks during the initial build, don't wait for the user to ask.
For examples, reactive vs silent guidance, and per-app-type recommendations, see **[App Interaction Hooks](references/INTERACTION_HOOKS.md)**.
## Actionable UI
When the user wants to triage or bulk-act on items, generate an interactive UI with selectable items and action buttons.
1. Fetch data with relevant tools
2. Render a `dynamic_page` with selectable items and action buttons
3. User selects + clicks action - UI sends `surfaceAction` with action ID and selected IDs
4. Execute tools, update UI with `ui_update`, show feedback via `widgets.toast()`
5. Use `window.vellum.confirm()` for destructive actions
## External Links
Use `vellum.openLink(url, metadata)` to make items clickable. Construct deep-link URLs when possible. Include `metadata.provider` and `metadata.type` for context.
No comments yet. Be the first to comment!