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 |
{ "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