---
name: App Builder SDK
slug: app-builder-sdk
category: AI Engineering
description: App Builder SDK builds new holaOS apps using five backend primitives (connection, resource, action, sync, start) and optionally adds a shadcn dashboard UI. Use it when a user requests an integration-only module like Slack or Discord, or a dashboard app with a workspace pane.
github: "https://github.com/holaboss-ai/holaOS/tree/main/runtime/harnesses/src/embedded-skills/app-builder-sdk"
language: TypeScript
stars: 7387
forks: 641
install: "npx degit https://github.com/holaboss-ai/holaOS/tree/main/runtime/harnesses/src/embedded-skills/app-builder-sdk ~/.claude/skills/app-builder-sdk"
installs_to: ~/.claude/skills/app-builder-sdk
source_path: runtime/harnesses/src/embedded-skills/app-builder-sdk/SKILL.md
collection_size: 25
category_size: 2451
collection_url: "https://dirskills.com/collections/holaboss-ai/holaOS"
added: 2026-08-15T06:51:35.456Z
last_synced: 2026-08-15T06:51:35.456Z
canonical_url: "https://dirskills.com/skills/app-builder-sdk"
---

# App Builder SDK

App Builder SDK builds new holaOS apps using five backend primitives (connection, resource, action, sync, start) and optionally adds a shadcn dashboard UI. Use it when a user requests an integration-only module like Slack or Discord, or a dashboard app with a workspace pane.

**Install:**

```bash
npx degit https://github.com/holaboss-ai/holaOS/tree/main/runtime/harnesses/src/embedded-skills/app-builder-sdk ~/.claude/skills/app-builder-sdk
```

## README

# App Builder (SDK)

Use this skill whenever the user wants a new holaOS app. Two shapes both ship through the same SDK; pick the one the request needs:

1. **Integration-only module** — Slack, Discord, Notion, Stripe, Linear, anything whose value is "talk to one external service via MCP tools, agent drives, no per-app dashboard". The SDK's default web stub is fine; no `src/client/` directory.
2. **Dashboard app** — vibe-coded content planners, CRMs, kanban-style trackers, podcast-guest managers, anything where the user expects a workspace pane they can look at and click around in. **Has a real shadcn UI** authored under `src/client/` (TanStack Start). The MCP tools are still there — they're how the agent drives the same data the dashboard surfaces.

The SDK core (5 primitives below) is identical for both shapes. The dashboard shape adds a `src/client/` directory; that's the only structural delta.

All supplemental files named in this skill are bundled beside this `SKILL.md`. Treat those paths as skill-local references that are safe to use in packaged runtimes; do not guess at repo-root paths.

## Tooling discipline for this skill

- In the normal build turn, use surfaced workspace file tools for file work: `read` / `search` / `find` / `list` for inspection, `edit` for targeted changes, and `write` for new files or full rewrites.
- Do **not** use `bash` heredocs for ordinary app file creation or mutation in the build turn. That includes `package.json`, `app.ts`, `server.ts`, `app.runtime.yaml`, `workspace.yaml`, and non-`src/client/` assets such as `game.html`.
- Use `bash` only for shell-native work such as `bun install`, starting or probing processes, checking logs, or other commands the surfaced file/runtime tools cannot express directly.
- The only heredoc exception in this skill is the auto-queued polish pass rule below, and it applies **only** to `apps/<app_id>/src/client/*.tsx` and `.css` files in that separate polish turn.

## When NOT to use this skill

- The user already has a working module app and wants to extend it → modify it in place; don't rewrite as SDK. (The legacy app-builder skill that used to live alongside this one has been removed; all new app work goes through this SDK.)

## The 5 primitives

Every SDK app composes exactly these:

```ts
app.connection()             // declares "this app needs an integration binding"
app.resource(name, {...})    // declares a row type (status machine, schema, emit rules)
app.action(resource, name, { fromStates, toState, run, [reversible], [steps], [schema] })
app.sync(name, { schedule, attachTo, fetch, upsert, normalize })
app.start()                  // validate config; no scheduling — automations layer does that
```

Mental model:
- `resource` = a row in the app's SQLite (e.g. `message`, `event`, `issue`, `pin`)
- `action` = state transition + upstream API call (e.g. `send_message: draft → sent`)
- `sync` = periodic upstream read that upserts records keyed by external id
- HOW (steps / states / reversal) lives in the SDK. WHEN (scheduling, retry) lives in Holaboss automations — **the SDK never schedules**.

Full type contract: `sdk-package/src/types.ts`. Public exports: `sdk-package/src/index.ts`.

### `provider.id` MUST be the Composio toolkit slug

There is ONE provider identifier; the same value flows through every layer of the connect + proxy chain:

- `app.runtime.yaml`'s `integration.destination`
- `pending_integrations[].provider_id` (runtime emits this to drive the chat Connect card)
- Hono `/api/composio/connect`'s `body.provider` (Hono uses it verbatim as Composio's `toolkit_slug`)
- `integration_connections.provider_id` (DB row created at OAuth finalize)
- `integration_bindings.integration_key` (DB row created when the user clicks Bind)
- `createRuntimeBrokerTransport({ provider })` at runtime (broker keys the binding lookup on it)

`provider.id` in `ProviderRegistry` IS this value. It MUST be the canonical Composio toolkit slug — the exact string in Composio's catalog at https://platform.composio.dev — not a "user-friendly" alias. Common ones that bite:

- Discord bot: **`discordbot`** (NOT `discord` — that slug, if it exists, grants only `identify` scope and cannot post messages → `POST /channels/.../messages` returns 401, which the SDK maps to `not_connected`)
- Google Calendar: **`googlecalendar`** (NOT `gcal` or `google`)
- Google Sheets: **`googlesheets`**
- Google Drive: **`googledrive`**
- Slack / GitHub / Gmail / Notion / Stripe / Linear / Figma / Calendly / Mailchimp / Reddit / Twitter / Instagram / YouTube / LinkedIn: **lowercase brand name** (verify in catalog).

If unsure, verify against the **integration store catalog** BEFORE writing `provider.ts` — the runtime will reject `workspace_apps_register` on any `provider` that isn't in this list with a "did you mean '<x>'?" suggestion. The store catalog is the curated subset of Composio toolkits we explicitly support; Composio has 1000+ toolkits but only the ones in `runtime/api-server/src/integration-store-catalog.ts` (Hero + Supported tiers) are accepted.

```bash
# Look up supported slugs from the runtime (preferred — single source of truth):
curl -sS http://127.0.0.1:8080/api/v1/capabilities/runtime-tools/integrations/catalog | jq '.provider_ids'

# Or grep the catalog file directly if you have the repo open.
```

Composio's own catalog (`https://backend.composio.dev/api/v3/toolkits`) is a useful reference for slug spelling but is **not** the source of truth — a slug existing on Composio does NOT mean we support it. If you want to add a new toolkit, the workflow is: add a row to `integration-store-catalog.ts`, not bake the unsupported slug into your app.

The legacy `composioToolkit` field on `ProviderRegistry` is **deprecated**. Do not set it. If a reference still does, replace `id` with the same value and drop `composioToolkit`. Splitting them was a misreading of the runtime — the broker proxy uses ONLY `provider` (= `cfg.id`); `composioToolkit` is dead code, currently used only by `manifest.ts` as a fallback that should never trigger when `id` is correct.

### Connection readiness: ask the runtime, never the upstream host

If your app needs to show "connected / needs connection" status in the UI, you **MUST** call `getIntegrationStatus()` from `@holaboss/app-builder-sdk` on mount (via a TanStack Start server function or loader), and re-call it after the user finishes any Connect flow. There is **no other supported way** to detect connectivity. Pinging the upstream host (`https://api.twitter.com/...`, `https://api.notion.com/...`) is not just suboptimal — it is the exact failure mode that left every previous vibe-coded dashboard stuck on "needs connection" the moment Composio rerouted the toolkit (api.twitter.com → api.x.com, Discord scope-only slug, etc.). The register-time lint rejects hardcoded upstream hosts; `getIntegrationStatus()` is the only way through.

```ts
// src/client/lib/integration-status.ts (TanStack Start server function)
import { getIntegrationStatus } from "@holaboss/app-builder-sdk"

export const integrationStatus = createServerFn().handler(async () => {
  return getIntegrationStatus()
})

// or narrow to one provider for a per-toolkit badge:
export const twitterStatus = createServerFn().handler(async () => {
  return getIntegrationStatus({ provider: "twitter" })
})
```

The helper reads `HOLABOSS_APP_GRANT` + `WORKSPACE_API_URL` (both injected by the runtime when your app starts) and calls the runtime's `/api/v1/integrations/readiness` endpoint. Response shape: `{ ready: boolean, issues: [{ provider, integrationKey, code, message }] }`. `code` is one of `ready | integration_not_bound | integration_not_connected | integration_needs_reauth` — let the UI pick the affordance from that code (e.g. show "Connect" for `integration_not_connected` and "Reconnect" for `integration_needs_reauth`).

There is **no legitimate reason** for an SDK app to ping the upstream API host as a connectivity test. If something looks like it needs that, you want `getIntegrationStatus` instead.

The runtime enforces this at `workspace_apps_register` time: a source-tree scan rejects any app whose `src/` contains hardcoded toolkit hosts like `api.twitter.com`, `api.x.com`, `api.github.com`, `slack.com/api`, `api.notion.com`, `api.linear.app`, `gmail.googleapis.com`, etc. The error names the file, line, and the provider you should be routing through instead. The right shape is **always** `createRuntimeBrokerTransport({ provider })` — no upstream host belongs in your app code.

### SDK actions are deterministic provider effects only

The SDK supports one execution model: deterministic provider effects through `providerEffectAction(...)`. If the app needs open-ended agentic work, that belongs in a first-class workflow agent node or an issue, not inside an app action. App code does not have a delegated-task surface.

- Use `providerEffectAction(...)` for "send/post/create/update this exact thing in provider X".
- Use plain `app.action(...)` for non-provider local effects on app state.
- Do **not** wire raw `fetch` calls to `/api/v1/issues` or `/api/v1/capabilities/runtime-tools/tasks/*` from app code to fake delegation. If an action is agentic in nature, redesign the flow so the workflow / issue surface owns it.

### Deterministic provider effects: use `providerEffectAction(...)`

If an action is an app-owned, deterministic provider side effect, use `providerEffectAction(...)` from `@holaboss/app-builder-sdk`.

What it encodes:

- the app owns the provider call directly
- readiness is checked through `getIntegrationStatus()`
- missing auth/binding becomes a blocked app state
- the action does **not** silently swallow a missing connection

Minimal shape:

```ts
import {
  createRuntimeBrokerTransport,
  providerEffectAction,
} from "@holaboss/app-builder-sdk"

const bridge = createRuntimeBrokerTransport({ provider: "gmail" })

app.action(draft, "send", providerEffectAction({
  provider: "gmail",
  fromStates: ["approved"],
  toState: "sent",
  blockedState: "send_blocked",
  failedState: "send_failed",
  buildRequest: ({ row }) => ({
    to: row.email,
    subject: row.subject,
    body: row.body,
  }),
  execute: async ({ bridge, request }) =>
    bridge.call("POST", "/messages/send", request),
  persistBlocked: (blocked) => ({
    blocker_code: blocked.code,
    blocker_message: blocked.message,
  }),
  persistSuccess: ({ result }) => ({
    provider_message_id: result.id,
  }),
}))
```

### Multi-step orchestration belongs in workflows

If the app needs multi-step orchestration, model it as a first-class workflow. App actions stay single-step provider effects; the workflow surface owns sequencing, agent steps, and durable state across steps.

## Dashboard / workspace-pane UI (vibe-coded apps)

The SDK's default `startMcpServer({ httpPort, ... })` ships a one-screen "headless module" placeholder on the http port. That placeholder is **only acceptable for integration-only modules** (Slack-style MCP-driven flows). The moment the user asks for a dashboard / list view / kanban / calendar / "let me see my X", you must replace the placeholder with a real dashboard built on `@holaboss/ui`.

### Polish pass: handled by a separate auto-queued turn

For dashboard apps (those with `src/client/`), the runtime auto-queues a polish-only input on the main session after `workspace_apps_ensure_running` returns `ready: true`. You do **not** have to invoke `interface-design` or refactor `src/client/` inside the same turn as the build. The response from `workspace_apps_ensure_running` includes a `polish_pass_queued` array listing the queued input(s); the polish turn dispatches automatically as the next turn on the user's chat.

In this build turn: finish wrapping up cleanly — tell the user the app is built, mention that a polish pass will run next. That's it.

In the auto-queued polish turn (you'll see a `text` payload starting with `[Auto-queued post-build polish pass]`):

1. Invoke `skill({ name: "interface-design" })` and read its full output.
2. For each `.tsx` / `.css` file under `apps/<app_id>/src/client/`: **REWRITE the whole file via `bash` heredoc** (`cat > path/to/file <<'EOF' ... EOF`), NOT via `edit`. Whole-file rewrite is mandatory for this pass — incremental edits repeatedly produce checkbox-compliant no-changes.
3. Re-run `workspace_apps_build` + `workspace_apps_restart_and_wait_ready`.
4. Take a `browser_screenshot` of the rendered dashboard. Compare it against the `interface-design` rules you just loaded. If the rendered output doesn't match those rules, return to step 2 and rewrite again.
5. Only after the screenshot is right, declare the polish pass done.

Why this is a separate auto-queued turn and not part of the build turn:

- Doing both in one turn consistently produced "skill invoked, 1 trivial edit, ready" — the agent's task-complete mindset and ~80-tool-call context fatigue defeated every prompt-strength escalation we tried. Forensic at `holaOS/docs/plans/2026-05-22-interface-design-skill-noop-forensic.md`.
- A separate turn restores fresh context, narrow scope, and no build-time inertia. Empirically this matches the one observed successful polish, which the user manually triggered as a second turn.

What this gate is NOT:

- Not optional for dashboard apps — the input is queued mechanically; you can't skip it. Integration-only modules (no `src/client/`) get no queued input.
- Not satisfied by ceremony — the runtime can verify file mtimes / screenshot, and the user checks the rendered UI either way.
- Not replaced by `frontend-design` — that one targets marketing pages and drifts the output the wrong way.

### Visual decisions belong to `interface-design`, not here

Every visual decision — density, hierarchy, typography, color usage, layout shape — is delegated to the `interface-design` skill that runs in the auto-queued polish turn. This file deliberately does NOT prescribe what a dashboard should look like.

The reasoning is empirical: previous versions of this skill listed concrete visual rules and named the failure modes to avoid. Observed output consistently reproduced the named failure modes — naming an anti-pattern is enough to anchor on it. Removing them from this file leaves `interface-design` as the sole authority on look-and-feel.

If your output looks wrong, the fix lives in the polish turn (re-invoke `interface-design`, rewrite via heredoc, screenshot, iterate). It does not live in this SKILL.md.

### The rule: import `@holaboss/ui`, do not redefine primitives

`@holaboss/ui` is a public npm package. It provides every primitive and CSS token your dashboard needs. **Do not generate shadcn primitives, copy a `components/ui/` directory, write your own Card, or import any other component library**. If `@holaboss/ui` is missing something, surface it to the SDK team instead of inventing a local replacement — visual drift is the failure mode the library exists to prevent.

Layout itself is your call. There is no `DashboardShell` / `PageHeader` / `DataTable` / `StatPill` / etc. — those were removed in 0.3.0. Compose page chrome from the raw primitives (Card, Tabs, Sheet, Sidebar, Table, Skeleton, EmptyState…). What the layout should look like is decided in the `interface-design` polish turn, not here.

Install:

```bash
cd <app-dir>
bun add @holaboss/ui
```

Both `@holaboss/app-builder-sdk` and `@holaboss/ui` are public npm packages. The resulting `package.json` looks like:

```json
"dependencies": {
  "@holaboss/app-builder-sdk": "latest",
  "@holaboss/ui": "latest"
}
```

**Always use `"latest"` for both.** These packages are lockstep-evolving alongside this skill — pre-1.0 caret semver (`^0.1.0`) only matches `0.1.x`, so any pinned dep silently drifts behind the skill when a new minor is published. `"latest"` keeps every fresh `bun install` aligned with the runtime's current expectations. Do NOT install via `file:` paths, git refs, or pinned versions — `"latest"` is the only supported form.

### Mount the styles — one import, done

`@holaboss/ui` ships a pre-compiled stylesheet that contains:
- the holaOS design tokens (`--background`, `--foreground`, `--primary`, `--radius`, etc.)
- the default theme palette
- every Tailwind utility class used by the library's primitives + layouts

Import it once at the dashboard root:

```tsx
// src/client/routes/__root.tsx
import "@holaboss/ui/styles.css";
```

That's it. **Do not** try to add `@holaboss/ui` to your own Tailwind `@source` list — the utilities are already baked in. **Do not** mount `tokens.css` + `themes/holaos.css` separately unless you have an explicit reason (those exports exist as an escape hatch).

Visual rules: colors / spacing / radii come from these CSS variables. No inline `style={{ color: "#f12711" }}`. No custom CSS files. No new Tailwind colors. If a value is missing from the token palette, escalate to the SDK team — do not patch it locally.

### Catalog of what `@holaboss/ui` ships

A full base-ui-flavoured shadcn surface — ~55 primitives. The ones you reach for most for a dashboard:

- **Containers**: `Card` (+ Header/Title/Description/Content/Footer/Action), `Sheet`, `Drawer`, `Dialog`, `AlertDialog`, `HoverCard`, `Popover`, `Tabs`
- **Lists / tables**: `Table` (+ Header/Body/Row/Cell/Caption/Footer), `Sidebar` family, `Accordion`, `Collapsible`
- **Form**: `Input`, `Textarea`, `Select`, `NativeSelect`, `Checkbox`, `RadioGroup`, `Switch`, `Slider`, `Combobox`, `Field` family (FieldGroup, FieldLabel, FieldSet, FieldLegend, FieldDescription, FieldError), `InputGroup`, `InputOTP`, `Label`
- **Charts**: `Chart` family — `ChartContainer`, `ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent` (wraps Recharts)
- **States**: `EmptyState`, `Skeleton`, `Spinner`, `Progress`, `Alert`
- **Atoms**: `Button`, `Badge`, `Avatar`, `StatusDot`, `Kbd`, `Separator`, `Tooltip`, `Toggle`, `ToggleGroup`, `ButtonGroup`, `Item`
- **Nav / IA**: `Breadcrumb`, `Pagination`, `NavigationMenu`, `Menubar`, `DropdownMenu`, `ContextMenu`, `Command`
- **Layout helpers**: `AspectRatio`, `Resizable`, `Calendar`, `Carousel`

**Utility**: `cn(...)` for class merging. **Toast**: import `Toaster` and use `toast()` from `sonner` (re-exported).

### Wiring the client app

1. Start TanStack Start (or simple Bun.serve serving a Vite-built dashboard) on `env.PORT` from the same `server.ts` that boots the MCP server on `env.MCP_PORT`. The desktop's iframe loads whatever the http port serves.
2. The dashboard reads the app's own SQLite (the table `app.resource()` declared) via TanStack Start server functions — same DB the MCP tools mutate. **Never duplicate state.**
3. Mount `@holaboss/ui/styles.css` at the top of `__root.tsx`. That single import covers the tokens, the default theme, and every Tailwind utility class the library uses. Without it the tokens fall back to defaults and the components render with no styling.

Beyond those three wiring points, **the layout is yours**. The `interface-design` skill output (delivered in the auto-queued polish turn) is your design brief; the primitive catalog above is your toolbox. No scaffolding template, no "minimal dashboard route" stub to copy.

### Schema migration (from PM doc)

vibe coding's biggest failure mode is destructive migrations. Rules:

| Change | Behaviour |
|---|---|
| Add field | Additive, safe, default value auto-filled, agent does it directly |
| Rename field | Safe, auto-migrate |
| Delete field | Destructive — require user confirm + auto-backup the old data |
| Change field type | Destructive — same |
| Change state alphabet | Existing-state mapping must be explicit; agent proposes, user confirms |

Each schema change is a version; the user must be able to roll back.

### UI anti-patterns — two are enforced at register time

`workspace_apps_register` runs two structural lints over `src/client/` for dashboard apps. Both reject the call with file/line context; nothing ships u
