The app manifest (app.json) declares your app's identity, resources, and requirements. It lives at the repo root — or under a subdirectory if the registry entry specifies one.
| Field | Type | Description |
|---|---|---|
name | string | Unique identifier, kebab-case (e.g. "oncall-watchtower") |
version | string | Semver version (e.g. "1.0.0") |
displayName | string | Human-readable name shown in the App Store |
description | string | Short description of what the app does |
| Field | Type | Description |
|---|---|---|
author | string | Author name or team |
license | string | License identifier |
minCrewVersion | string | Minimum Gateway version required |
tags | string[] | Discovery tags (e.g. ["oncall", "monitoring"]) |
jobFamilies | string[] | Job families this app is relevant to |
| Field | Type | Description |
|---|---|---|
agents | string[] | Paths to agent JSON files (relative to app root) |
skills | string[] | Paths to skill directories |
sops | string[] | Paths to SOP (Standard Operating Procedure) files |
mcpServers | object | MCP server definitions (same format as mcp.json) |
| Field | Type | Description |
|---|---|---|
repo | string | Git repository name. Used by the blob proxy to serve images |
iconPath | string | Path to app icon (PNG, square, min 256×256) |
screenshots | string[] | Paths to screenshots (PNG/JPG, max 5) |
highlights | string[] | Feature bullet points shown on the detail page (max 10) |
Hero art is rendered on Discover list rows, featured spotlight, feature cards, and the detail-page banner.
| Field | Purpose | Recommended size |
|---|---|---|
heroImage | Light-theme hero | 1200×675 (16:9) |
heroImageDark | Dark-theme variant | 1200×675 (16:9) |
heroImageDetail / heroImageDetailDark | Detail-page banner only (preferred over heroImage there) | 1200×288 (25:6) |
Resolution order on every surface: current theme's art → opposite theme's → first screenshot → deterministic gradient with app icon. A missing hero degrades cleanly.
Path form depends on distribution:
/apps/{name}/ui/ui/hero-light.svg); the registry rewrites it to a blob-proxy URL{ "crons": [ { "name": "ticket-refresh", "every": 300, "message": "Check for new high-severity tickets" }, { "name": "daily-digest", "cron_expr": "0 9 * * 1-5", "message": "Generate daily digest", "agent": "digest-agent" } ] }
| Field | Description |
|---|---|
name | Job identifier |
every | Interval in seconds (mutually exclusive with cron_expr) |
cron_expr | Cron expression (mutually exclusive with every) |
message | Prompt sent to the agent on each run |
agent | Agent to run (optional, uses default if omitted) |
enabled | Default true. Must be a JSON boolean. false registers the cron paused (visible, resumable) — for jobs that need user configuration first |
{ "ui": { "entry": "dist/index.mjs", "pages": [ { "route": "/apps/my-app", "label": "My App", "icon": "Shield", "entryPoint": "dist/page.mjs", "mountFunction": "mount" } ], "sidebar": { "section": "Apps", "order": 10 } } }
| Field | Default | Description |
|---|---|---|
ui.entry | Path to ESM bundle (relative to app root) | |
ui.pages[].route | URL path for the page | |
ui.pages[].label | Sidebar label | |
ui.pages[].icon | Lucide icon name (e.g. "Shield", "Package") | |
ui.pages[].iconUrl | Custom icon image path (relative to ui/) | |
ui.pages[].entryPoint | Per-page ESM bundle (overrides ui.entry) | |
ui.pages[].mountFunction | "mount" | Exported function name in the ESM bundle |
ui.sidebar.section | "Apps" | Sidebar section name |
ui.sidebar.order | 10 | Sort order within section |
{ "backend": { "entryPoint": "backend/server.py", "port": "auto", "healthCheck": "/health", "routes": "/api/apps/oncall-watchtower" } }
| Field | Default | Description |
|---|---|---|
backend.entryPoint | Script to run (relative to app root), or a dotted Python module path launched via python -m | |
backend.port | "auto" | Port number or "auto" for auto-assignment |
backend.healthCheck | "/health" | Health check endpoint path |
backend.routes | Base route path for the backend | |
backend.type | "" | Runtime: "python", "asgi", "node", "exec", or "" (auto-detect from entryPoint) |
App backends are accessible through the gateway's reverse proxy at /apps/{name}/api/{path}, which avoids CORS issues for dashboard UI pages.
Instead of (or alongside) a standalone backend process, register Python entry points that run inside the gateway process:
{ "backend": { "hooks": { "routes": "backend.routes:register_routes", "on_startup": "backend.hooks:on_startup", "on_shutdown": "backend.hooks:on_shutdown" } } }
| Field | Description |
|---|---|
backend.hooks.routes | Registers handlers into the gateway's in-process RouteRegistry |
backend.hooks.on_startup | Invoked when the app's hooks are wired up |
backend.hooks.on_shutdown | Invoked when the app is disabled |
Hooks are wired up on app enable (via on_app_enable, also re-run on gateway startup) — no gateway restart needed.
{ "permissions": { "api": ["/api/crons", "/api/status", "/api/agents"], "events": ["notification", "slots"], "mcpTools": ["cron_add", "cron_list"], "storage": true, "cron": true, "memory": "app-scoped", "network": false } }
| Field | Type | Description |
|---|---|---|
permissions.api | string[] | Allowed API path prefixes (enforced today) |
permissions.events | string[] | Allowed WebSocket event types |
permissions.mcpTools | string[] | Allowed MCP tool names |
permissions.storage | boolean | Can use app-scoped storage |
permissions.cron | boolean | Can create cron jobs |
permissions.memory | string | Memory access: "", "app-scoped", or "shared" |
permissions.network | boolean | Can make external network requests |
permissions.jobs | boolean | Can run durable server-side jobs through the host Job SDK; the job continues after the reader leaves the app page |
Use contributes when your app adds a row or compact control to a Crew-owned surface rather than a page of its own.
Each entry creates a row in the Cmd+K Command Bar. Activating it opens a new chat seeded with prompt; set autoSend only when the command also declares an argument that the prompt interpolates with {argument}.
{ "contributes": { "commands": [ { "id": "summarize-url", "title": "Summarize URL", "subtitle": "Start a research chat", "icon": "FileSearch", "keywords": ["research", "summary"], "prompt": "Summarize {argument} for this project.", "autoSend": true, "argument": { "kind": "url", "placeholder": "https://example.com", "hint": "Enter a public URL", "hosts": ["example.com"] } } ] } }
Required command fields are id, title, and prompt. subtitle, icon, keywords, and autoSend are optional. An optional argument has kind ("text" or "url"), plus optional placeholder, hint, hosts, and patternError. hosts is valid only for a URL argument. The host validates the declaration; app code does not run inside the Command Bar.
An app can put up to two live controls beside the agent and project chips in the composer. Each is a lazily loaded ESM module and receives the active session identity from the dashboard.
{ "contributes": { "sessionControls": [ { "id": "environment", "entryPoint": "dist/session-control.mjs", "label": "Environment", "icon": "Server", "statusPath": "session-status" } ] } }
id and entryPoint are required. label, icon, and statusPath are optional. When present, statusPath is an app-local backend route; the dashboard calls it with the current session identity and expects { "state": "ok" | "warn" | "none", "tooltip": "..." }.
{ "setup": { "onInstall": "cd ui && npm install && npm run build", "onUninstall": "echo cleanup done", "onUpdate": "cd ui && npm install && npm run build", "onEnable": "echo enabled", "onDisable": "echo disabled", "configSchema": {} } }
| Field | Default | Description |
|---|---|---|
setup.onInstall | "" | Shell command run after install |
setup.onUninstall | "" | Shell command run before uninstall |
setup.onUpdate | "" | Shell command run after update |
setup.onEnable | "" | Shell command run when app is enabled |
setup.onDisable | "" | Shell command run when app is disabled |
setup.onEnableTimeout | 30 | Timeout in seconds for onEnable |
setup.onDisableTimeout | 30 | Timeout in seconds for onDisable |
setup.configSchema | {} | JSON Schema for app configuration |
Scripts run with set -euo pipefail enforced by Crew. Unset variables and pipe failures cause immediate exit — no silent errors.
Timeout limits: onInstall / onUpdate = 300s, onUninstall = 120s, onEnable / onDisable = configurable (default 30s).
If onEnable fails, the enable is rolled back — the app stays disabled and any registered resources are deregistered. onDisable failures are logged as warnings but do not block the disable.
onUninstall receives KEEP_DATA=1 or KEEP_DATA=0 in the environment — if the user chose "Keep app data", the script should skip deleting user data directories.
{ "dependencies": { "managedBy": "gateway", "capabilities": { "mcp": [ { "id": "some-mcp-server", "source": "registry" } ], "skills": [ { "id": "some-skill", "source": "registry" } ] }, "commands": ["jq", "node", "python3"] } }
| Field | Default | Description |
|---|---|---|
dependencies.managedBy | "gateway" | Who manages dependency lifecycle: "gateway" or "app" |
dependencies.capabilities.mcp | [] | Required MCP server dependencies |
dependencies.capabilities.skills | [] | Required skill dependencies |
dependencies.capabilities.agents | [] | Deprecated for managedBy: "gateway" — always reported unresolved |
dependencies.commands | [] | System commands that must be on PATH (checked via which) |
Dependencies are tracked in a reference-counting ledger. On uninstall, Crew shows which deps can be safely removed vs. which are shared with other apps.
| Field | Default | Description |
|---|---|---|
lifecycle | "gateway" | "gateway" (managed), "app" (self-managed), or "locked" (cannot uninstall) |
resources | "gateway" | "gateway" (Crew registers agents/skills/MCP) or "app" (app handles its own) |
{ "platform": { "os": ["macos", "linux"], "arch": [], "installMode": "server", "clientInstall": { "shell": "curl -fsSL https://example.com/install.sh | bash", "postInstall": "open ~/Applications/MyApp.app" } } }
| Field | Default | Description |
|---|---|---|
platform.os | ["macos", "linux"] | Supported platforms |
platform.arch | [] (any) | Supported architectures |
platform.installMode | "server" | "server" or "client" |
platform.clientInstall.shell | One-liner for local install | |
platform.clientInstall.postInstall | Command to run after install |
When installMode: "client", the App Store shows copy-paste terminal instructions instead of running the install on the server. Use for apps that must run on the user's local machine (e.g. Electron apps when Crew runs on a remote host).
{ "openCommand": "open ~/Applications/MyApp.app" }
For apps that run outside the dashboard, the openCommand declares how to launch them. POST /api/apps/{name}/open runs it in the background. On a headless remote environment, the endpoint returns the command for you to run locally instead.
Enforced at install time:
name must match /^[a-z0-9]+(?:-[a-z0-9]+)*$/ (kebab-case)version must match semver (X.Y.Z)agents, skills, sops, ui.entry, ui.pages[].entryPoint, backend.entryPoint must be relative and stay inside the app root (absolute paths and .. traversal are rejected via canonical resolve + containment)backend.hooks.* are format-checked (module.path:callable, no traversal expressible) and containment-checked at load timemcpServers entries use command / args / url / env — not app-relative file paths — and are not path-checkedevery or cron_exprroute and labelUnknown fields in app.json are preserved during parsing and round-tripped through to_dict() / to_json(). Newer manifest features can coexist with older Crew versions without breaking validation.
{ "name": "oncall-watchtower", "version": "1.0.0", "displayName": "Oncall Watchtower", "description": "Monitor tickets, pipelines, and alarms for your on-call rotation", "author": "kirocrew", "tags": ["oncall", "monitoring"], "agents": ["agents/ticket-analyst.json"], "skills": ["skills/oncall-runbook"], "crons": [ { "name": "ticket-refresh", "every": 300, "message": "Check for new high-severity tickets" } ], "ui": { "entry": "dist/index.mjs", "pages": [ { "route": "/apps/oncall-watchtower", "label": "Oncall", "icon": "Shield" } ] }, "permissions": { "api": ["/api/crons", "/api/status"], "events": ["notification"] }, "platform": { "os": ["macos", "linux"] } }
Manifest reference