# Create Holaos App

> Create a holaOS App

- Skill: `holaboss-ai/create-holaos-app` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add holaboss-ai/create-holaos-app`
- Raw SKILL.md: https://api.skillmd.com/api/skills/holaboss-ai/create-holaos-app/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: holaboss-ai (https://skillmd.com/u/holaboss-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/holaboss-ai/create-holaos-app

---


# Create a holaOS App

A holaOS App is a self-contained TanStack Start application that exposes a Composio-backed third-party service over MCP. Each app: own SQLite, own MCP server (SSE on `/mcp/sse`), own audit log, own tiny web UI. **No shared packages** — copy-paste over abstraction.

## When to use

User asks for a new holaOS App wrapping a Composio toolkit (e.g. "create a shopify holaos app", "帮我做一个 figma holaos app", "build a holaos app for linear"). Existing examples in repo: `stripe`, `figma`, `calendly`, `discordbot`, `linear`, `gcalendar`, `gdrive`, `slack`, `youtube`, `mailchimp`, `notion`, `instagram`.

For **publishing-style** apps (drafts → queue → publish, e.g. twitter/linkedin/reddit), this skill does NOT apply — those follow a different pattern with a SQLite job queue and `publisher.ts`.

## The 7-step flow

### 1. Verify the Composio toolkit exists FIRST

Don't assume — Threads / TikTok / Pinterest / Buffer are NOT in Composio. Curl the catalog before making promises.

```bash
COMPOSIO_API_KEY=<key> curl -s "https://backend.composio.dev/api/v3/toolkits?limit=500" \
  -H "x-api-key: $COMPOSIO_API_KEY" -o /tmp/toolkits.json
python3 -c 'import json; d=json.load(open("/tmp/toolkits.json"))
for t in d["items"]:
  if "<keyword>" in t["slug"].lower(): print(t["slug"], t.get("composio_managed_auth_schemes"))'
```

Note `composio_managed_auth_schemes`: if it's `["OAUTH2"]`, the toolkit is **managed** (zero-config user OAuth). If empty, the user must pass `--api-key`/credentials at connect time. Both work; managed is preferred.

API key location: `frontend/apps/server/.env` → `COMPOSIO_API_KEY`.

### 2. Scaffold from gcalendar

```bash
cd /Users/joshua/holaboss-ai/holaboss/hola-boss-apps
bash scripts/scaffold-from-gcal.sh <slug> <provider> "<Display>" "<Pascal>"
# e.g.: bash scripts/scaffold-from-gcal.sh stripe stripe "Stripe" Stripe
```

The script clones `gcalendar/`, sed-renames prefix tokens (`gcal_*` → `<slug>_*`, `GCalError` → `<Pascal>Error`, `GCAL_BASE` → `<UPPER>_BASE`, etc.) and clears the eight files you must rewrite per-provider.

Then update `package.json` name:
```bash
sed -i '' "s/\"name\": \"gcalendar\"/\"name\": \"<slug>\"/" <slug>/package.json
```

### 3. Write the provider-specific files

Eight files per app — see `references/file-templates.md` for working snippets:

| File | Content |
|---|---|
| `app.runtime.yaml` | mcp.tools list (+ tool prefix), data_schema (audit/usage/settings tables), `integration.destination` = Composio slug |
| `src/lib/types.ts` | Error code enum, `ToolSuccessMeta`, `<UPPER>_CONFIG` |
| `src/server/<slug>-client.ts` | `createIntegrationClient(<slug>)` proxy + status mapping (200→ok, 404→not_found, 429→rate_limited, 401/403→not_connected, 4xx→validation_failed, 5xx→upstream_error) |
| `src/server/connection.ts` | Cheap identity probe (e.g. `GET /me` or `GET /account`) |
| `src/server/mcp.ts` | `name: "<Display> App"` |
| `src/server/tools.ts` | 8–13 MCP tools: registerTools + impl functions exported for tests |
| `src/components/connection-status-bar.tsx` | Polls `/api/connection-status` |
| `src/routes/__root.tsx` + `src/routes/index.tsx` | Page title + heading |

**Tool naming**: snake_case, prefixed with the brand (`stripe_list_customers`, `discord_send_channel_message` even when slug is `discordbot`). Tool descriptions follow `hola-boss-apps/docs/MCP_TOOL_DESCRIPTION_CONVENTION.md` — agent only sees name+description+inputSchema+annotations.

**Mandatory tool**: `<prefix>_get_connection_status`. Always first in `app.runtime.yaml`'s `mcp.tools`. Wraps `getConnectionStatus()` from connection.ts.

**GraphQL APIs** (Linear): write a `gql<T>(query, vars)` helper instead of `apiGet/apiPost`. Demote GraphQL `errors[]` to canonical Holaboss codes by `extensions.type` (`AUTHENTICATION_ERROR` → `not_connected`, `INVALID_INPUT` → `validation_failed`, etc.). See `linear/src/server/linear-client.ts`.

### 4. Write tests

`test/e2e.test.ts` — uses `MockBridge` (already cloned in fixtures/), 5–6 cases:
- MCP health endpoint live
- get_connection_status with bridge succeeding
- One write tool writes audit row with correct `<slug>_record_id` + deep_link
- not_connected short-circuits when bridge throws
- validation_failed maps a 400
- (provider-specific) rate_limited maps 429 with retry_after

`test/live.test.ts` — `describe.skipIf(!process.env.LIVE)`, shape-only assertions, write tests gated behind `LIVE_WRITE=1` plus an env-var-named test resource id (so a test runner can never delete real customer data by accident).

See `references/file-templates.md` for skeletons.

### 5. Verify locally

```bash
cd <slug>
pnpm rebuild better-sqlite3   # required first time on macOS
pnpm run typecheck            # vite.config.ts will warn — that's a pre-existing gcalendar issue, ignore
pnpm run test:e2e             # all green
```

### 6. Register the app

Three files in the repo root — see `references/registration.md` for full schemas:

- `marketplace.json` — append entry with `name`, `description`, `icon`, `category`, `tags`, `path`, `provider_id`, `credential_source: "platform"`
- `pnpm-workspace.yaml` — add `- "<slug>"` (alphabetical order)
- `.github/workflows/build-apps.yml` — append `<slug>` to `LEGACY_MODULES` (env-var name is historical; it's the build allowlist)

Then from repo root: `pnpm install` to register the new workspace package.

### 7. Live test (the user runs this; we just commit)

User starts the broker, connects each provider once via OAuth, then `LIVE=1 pnpm --filter <slug> run test:live`. Connection metadata persists in `.composio-connections.json` (gitignored). Full instructions in `references/live-testing.md`.

## Conventions you must follow

- **Audit log**: every tool wrapped via `wrapTool("<prefix>_<name>", impl)` — writes a row to `<slug>_agent_actions` with `<slug>_record_id`, `<slug>_deep_link`, outcome, duration. Don't skip this.
- **Deep links**: when the provider has a web UI, set `<slug>_deep_link` to the canonical URL for that resource (e.g. `https://dashboard.stripe.com/customers/cus_xyz`). Agents surface these to users.
- **Error envelope**: only the seven canonical codes (`not_found`, `invalid_state`, `validation_failed`, `not_connected`, `rate_limited`, `upstream_error`, `internal`). Don't invent new ones.
- **No raw credentials**: ALL provider calls go through `createIntegrationClient(<slug>).proxy(...)`. The bridge prepends auth. Never read tokens from env or `.env`.
- **Live tests are shape-only**: `expect(Array.isArray(r.data.x)).toBe(true)` — never assert on values, since user data drifts.
- **One container, two processes**: web app on `:3000`, MCP server on `:3099` (overridden by sandbox runtime via `PORT`/`MCP_PORT` env).

## Pitfalls (from prior incidents)

- **Don't skip the catalog probe** (step 1). Building an app for a slug that doesn't exist on Composio is wasted work — `threads` was discovered missing only after the app shipped.
- **Don't add a local app→provider table in desktop**. The `provider_id` field in `app.runtime.yaml` flows through `marketplace.json` → backend → desktop catalog. Adding apps requires zero desktop edits.
- **`pnpm rebuild better-sqlite3` is required** the first time you run e2e tests on macOS; the prebuilt native binding doesn't always resolve correctly across new pnpm projects.
- **MockBridge matches via `endsWith`** — query strings break suffix matchers. Test impls without query params, or refactor the impl to the bare endpoint, or extend mock-bridge if you really need it.

## References (load on demand)

- `references/file-templates.md` — copy-pasteable skeletons for client.ts, connection.ts, tools.ts, e2e.test.ts, live.test.ts, app.runtime.yaml
- `references/registration.md` — exact format of marketplace.json entries, pnpm-workspace.yaml, build-apps.yml diffs
- `references/live-testing.md` — composio:broker setup, per-provider connect commands, env vars for write-path tests

