Build, install, and run your first Crew app in about five minutes. We'll create an app with a manifest, one agent, one skill, and a simple dashboard UI page.
kirocrew gateway)my-dashboard/ ├── app.json ← manifest (required) ├── agents/ │ └── sample-agent.json ← agent definition ├── skills/ │ └── sample-skill/ │ └── SKILL.md ← skill knowledge file ├── ui/ ← frontend (if app has UI) │ ├── package.json │ ├── vite.config.ts │ ├── src/App.tsx │ └── .gitignore └── README.md
Every app needs an app.json at the repo root. This is the single source of truth for identity, resources, and store listing.
{ "name": "my-dashboard", "version": "0.1.0", "displayName": "My Dashboard", "description": "A Crew app: My Dashboard", "author": "yourname", "agents": ["agents/sample-agent.json"], "skills": ["skills/sample-skill"], "ui": { "entry": "dist/index.mjs", "pages": [ { "route": "/apps/my-dashboard", "label": "My Dashboard", "icon": "Package" } ] }, "permissions": { "api": ["/api/crons", "/api/status"], "events": ["notification"] } }
See Manifest reference for every field.
ui/src/App.tsx is your app's React entry point. It's a standard React component using @kirocrew/app-sdk hooks and shared UI components.
import { useAppApi, useAppEvents } from '@kirocrew/app-sdk' import { Card, CardTitle, PageHeader, StatCard } from '@kirocrew/app-sdk/ui' import { useState, useEffect } from 'react' export default function MyDashboard() { const api = useAppApi() const [data, setData] = useState(null) useEffect(() => { api.get('/api/status').then(setData) }, []) useAppEvents('notification', (event) => { console.log('New notification:', event) }) return ( <> <PageHeader title="My Dashboard" subtitle="Custom app page" /> <div className="px-6 pb-8 overflow-y-auto flex-1 min-h-0"> <div className="grid gap-3.5 grid-cols-[repeat(auto-fit,minmax(150px,1fr))] mb-6"> <StatCard label="Status" value={data ? 'Online' : '...'} accent /> </div> <Card> <CardTitle>Content</CardTitle> <p className="text-sm text-muted">Your app content here.</p> </Card> </div> </> ) }
agents/sample-agent.json:
{ "name": "my-agent", "model": "auto", "description": "Analyzes data and generates reports", "prompt": "You are a data analyst assistant.", "tools": ["@kirocrew-core"] }
The @kirocrew-core tool reference tells Crew to include the core MCP server (spawn, learn, task, wait, register hook, send message).
skills/sample-skill/SKILL.md:
--- name: sample-skill description: A sample skill that teaches the agent about my domain. triggers: [sample, domain] always: false --- # Sample skill When the user asks about the sample domain: 1. Do the domain-specific first thing 2. Then check for the domain-specific second thing 3. Return the result in the standard format
The triggers list matches user message words. When any trigger appears in a message, the skill body is loaded for that turn.
cd my-dashboard/ui npm install npm run build
This produces dist/index.mjs — the ESM bundle loaded by the dashboard.
Your vite.config.ts should mark React, ReactDOM, lucide-react, and @kirocrew/app-sdk as externals:
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], build: { lib: { entry: 'src/App.tsx', formats: ['es'], fileName: 'index', }, rollupOptions: { external: ['react', 'react-dom', 'react/jsx-runtime', 'lucide-react', /^@kirocrew\/app-sdk/], }, }, })
# Install via REST API curl -X POST http://localhost:5476/api/apps/install \ -H 'Content-Type: application/json' \ -d '{"source": "./my-dashboard"}' # Enable curl -X POST http://localhost:5476/api/apps/my-dashboard/enable
Or use the App Store UI in the dashboard → Install from local path.
Your app appears in the Crew dashboard sidebar.
Turn on dev mode:
kirocrew app dev my-dashboard
Now the workflow is:
ui/src/App.tsxcd ui && npm run buildui/ and broadcasts an app_reload WebSocket event on any change)Agent and skill changes take effect on the next agent invocation — no rebuild needed.
For faster iteration, symlink your source tree so file changes appear directly:
ln -sfn /path/to/my-dashboard/ui ~/.kiro/crew/apps/my-dashboard/ui
If your app needs its own HTTP server:
{ "backend": { "entryPoint": "backend/server.py", "port": "auto", "healthCheck": "/health", "routes": "/api/apps/my-dashboard", "type": "python" } }
The gateway launches your backend as a subprocess, reverse-proxies /apps/my-dashboard/api/* to it, and signs each proxied request with an HMAC header. Your backend verifies the signature using kirocrew-client:
from kirocrew_client import verify_proxy_request if not verify_proxy_request(request, 'my-dashboard'): return Response(status=401)
The signature is over timestamp:method:/api/path[?query]:sha256(body) with the app secret as the key. Timestamps must be within ±60s of now.
Declare it in the manifest:
{ "crons": [ { "name": "daily-check", "cron_expr": "0 9 * * 1-5", "message": "Run the daily check for my-dashboard", "agent": "my-agent" } ] }
Crew registers the cron on enable and deregisters on disable. Owned via created_by='app:my-dashboard' in the ledger.
For install-time build steps:
{ "setup": { "onInstall": "cd ui && npm install && npm run build", "onUpdate": "cd ui && npm install && npm run build" } }
Scripts run with set -euo pipefail, NONINTERACTIVE=1, and a minimal environment. They must exit 0 on success.
Available in @kirocrew/app-sdk:
| Hook | Purpose |
|---|---|
useAppApi() | Permission-scoped HTTP client (GET/POST/PUT/PATCH/DELETE) |
useAppEvents(event, cb) | Subscribe to real-time WebSocket events |
useTheme() | Reactive theme (mode, accent, colorTheme) |
useAppInfo() | App metadata (name, version, permissions) |
useNavigate() | Navigate to Crew routes |
useNotify() | Show toast notifications |
useNavBadge() | Update sidebar badge count |
useChatLauncher() | Navigate to chat with optional agent and message |
Available in @kirocrew/app-sdk/ui:
Card, CardTitle, Btn, SendBtn, Input, SearchInput, Badge, AimBadge, StatCard, Skeleton, ContentSkeleton, EmptyState, PageHeader, Toggle, InfoTip, SegmentedControl, MarkdownRenderer.
Build your first app