- name
- kirocrew-app-dev
- description
- Build, package, and publish Kiro Crew external apps. Covers app.json manifests, UI components, crons, skills, lifecycle hooks, development mode, registry publishing, and common pitfalls.
# Kiro Crew App Development
Guide for building external Kiro Crew apps against the current App Kit manifest, lifecycle, UI SDK, and registry contracts. The canonical field and API references live in [App Kit](../../docs/app-kit/README.md).
## When to Use
- User says "create a kirocrew app", "build an app", "make an app for kirocrew"
- User says "package this as an app", "publish this to the app store"
- User is building UI pages, crons, or skills that should be distributable
## App Structure
```
my-app/
├── app.json # Manifest (required)
├── ui/
│ ├── index.mjs # ESM React component (default export)
│ └── icon.svg # Sidebar icon (SVG or PNG)
├── skills/
│ └── my-skill/
│ └── SKILL.md # Skill spec
├── scripts/
│ ├── install.sh # Optional registry-install setup hook
│ └── uninstall.sh # Cleanup script
└── README.md # Optional docs
```
## app.json Manifest
```json
{
"name": "my-app",
"displayName": "My App",
"version": "1.0.0",
"description": "What it does in one sentence.",
"author": "login",
"tags": ["relevant", "tags"],
"skills": [
"skills/my-skill"
],
"crons": [
{
"name": "my-cron",
"message": "Run the scheduled app workflow.",
"every": 900,
"silent": true,
"persistent_session": false
},
{
"name": "market-open",
"message": "Summarise the overnight tape.",
"cron_expr": "30 9 * * 1-5",
"timezone": "America/New_York",
"skip_dates": ["2026-12-25"]
}
],
"permissions": {
"api": ["/api/apps/my-app/config", "/api/chat", "/api/chat/*"],
"mcpTools": ["local_knowledge_search", "send_message"],
"network": true,
"cron": true
},
"ui": {
"entry": "index.mjs",
"pages": [
{
"route": "/apps/my-app",
"label": "My App",
"iconUrl": "icon.svg"
}
]
},
"setup": {
"onInstall": "bash scripts/install.sh",
"onUninstall": "bash scripts/uninstall.sh"
}
}
```
### Critical Rules
| Field | Rule | Why |
|-------|------|-----|
| `skills` | Prefer canonical string paths `["skills/my-skill"]` | Legacy object entries with `path` or `name` are normalized by `AppManifest.from_dict`, but the public manifest schema is `string[]` |
| `permissions` | Must be an **object**; supported grants are `api`, `events`, `mcpTools`, `storage`, `network`, `memory`, `cron`, `sessionApproval`, `spawn`, `jobs`, and `exposeToApps` | A non-object parses to empty permissions, and malformed capability grants fail closed |
| `resources` | Omit from `app.json`; declare app resources with `agents`, `skills`, `sops`, and `mcpServers` | `resources` is install/registry ownership metadata (`"gateway"` or `"app"`), not an app-manifest resource array |
| `ui.entry` | Use an ESM `.mjs` bundle; `.js` is also served | HTML is not in the app UI static-file allowlist |
| `ui.pages[].iconUrl` | Use an app-relative image path for a custom icon | `ui.pages[].icon` also works for supported host Lucide names and falls back to `Package` |
| `displayName` | Required | Gateway uses it for UI display |
| `version` | Semver string | Used by the App Store registry to compute `updateAvailable` |
| `crons[].timezone` | IANA zone name; omit it only when the hour is zone-agnostic | An empty timezone falls back to the gateway config's zone and then to **UTC**, so `"cron_expr": "0 6 * * *"` without it fires at 06:00 UTC — the wrong calendar day for most users. A per-USER zone is not manifest data: pass `timezone=` to `ctx.cron.add_job` instead |
| `crons[].skip_dates` | Zero-padded `YYYY-MM-DD`, evaluated in `timezone` | `"2026-1-1"` parses but never matches the padded fire-time rendering, so the skip silently does nothing. Both fields are rejected at manifest validation, not at fire time |
| `crons[].timeout` / `crons[].timeout_secs` | Set both on a `script` cron that runs longer than 30s, e.g. `"timeout": 60, "timeout_secs": 65` | Omitted, a script is killed at 30s and a command at 300s. `timeout_secs` must cover `timeout` plus 5s of cleanup; a pair the cron SDK would refuse is rejected at manifest validation |
## Lifecycle and Resource Registration (CRITICAL)
Do not make a cron mutate the installed app tree or create skill links. When an
app is enabled, the gateway registers its declared agents, skills, SOPs, MCP
servers, and crons; gateway startup reconciles enabled app resources again.
Lifecycle scripts have explicit scope:
- A local-path `kirocrew app install <dir>` copies the app but intentionally does
not run `setup.onInstall`; build the local source before installing it.
- A registry install runs `setup.onInstall` in the cloned source before copying
it into the data home. Keep that hook idempotent.
- `setup.onEnable` / `onDisable` run around enablement, and `onUninstall` is only
for external state Kiro Crew cannot remove itself.
Keep runtime data under the app's `data/` directory. The gateway owns registered
resource links and scheduler entries; app code must not recreate them manually.
## UI Development
### Design System
Kiro Crew apps should default to the host visual language. Colors come
from the theme tokens (`var(--accent)` and friends) — see "Don't Reinvent the
Dashboard" below, which is the normative rule. The hex values here are the legacy
palette, kept for apps that predate the tokens and as the fallback inside
`var(--accent, #7c3aed)`; a hardcoded hex breaks every custom palette, so reach for
one only when a deliberate custom style is the point.
| Element | Style |
|---------|-------|
| **Styling method** | Prefer `@kirocrew/app-sdk/ui` components and theme tokens; CSS, host utility classes, or inline styles are all valid. |
| **Primary accent** | `var(--accent)` (legacy: `#7c3aed`) |
| **Light accent bg** | legacy `#e8d5f5` |
| **Success color** | `#047857` (green) |
| **Warning color** | `#b45309` (amber) |
| **Danger color** | `#b91c1c` (red) |
| **Buttons** | `borderRadius: '9999px'` (full pill), font 11px weight 500 |
| **Badges** | `borderRadius: '9999px'`, 10px bold, colored bg+text |
| **Cards** | `background: 'var(--bg)', border: '1px solid var(--border)', borderRadius: '6px', padding: '14px'` |
| **Max width** | `maxWidth: '1200px'` |
| **Font sizes** | 10px (version/badges), 11px (body/buttons), 12px (table), 13px (section headers), 18px (title) |
| **Theme vars** | `var(--bg)`, `var(--text)`, `var(--muted)`, `var(--border)`, `var(--card)` |
**Example badge:**
```javascript
_jsx('span', {
style: { background: '#e8d5f5', color: '#7c3aed', padding: '2px 7px', borderRadius: '9999px', fontSize: '10px', fontWeight: 600, letterSpacing: '0.02em' },
children: 'LABEL'
})
```
**Example primary button:**
```javascript
_jsx('button', {
style: { background: '#7c3aed', color: '#fff', border: 'none', padding: '5px 14px', borderRadius: '9999px', fontSize: '11px', fontWeight: 500, cursor: 'pointer' },
children: 'Action'
})
```
**Example secondary/ghost button:**
```javascript
_jsx('button', {
style: { background: 'transparent', color: '#7c3aed', border: '1px solid #e8d5f5', padding: '5px 14px', borderRadius: '9999px', fontSize: '11px', fontWeight: 500, cursor: 'pointer', whiteSpace: 'nowrap' },
children: '↻ Refresh'
})
```
### Reference Implementation
For a working example, use `docs/app-kit/examples/`, which contains installable
external-app patterns. Builtins under `src/kiro_crew/apps/builtins/` are useful
for UI idioms, but their backend and lifecycle contracts differ from external
apps.
Visual mock of the target style:
```html
<!--
Legacy geometry reference for a Kiro Crew app header and card.
Use host components and theme tokens instead of copying the hardcoded colors.
-->
<div style="max-width:1200px; margin:0 auto; padding:16px; font-family:system-ui; color:#e2e8f0; background:#1a1b26">
<!-- Header -->
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px">
<div style="display:flex; align-items:center; gap:10px">
<span style="font-size:18px; font-weight:600">Review Tender</span>
<span style="background:#e8d5f5; color:#7c3aed; padding:2px 8px; border-radius:9999px; font-size:10px; font-weight:600">Every 15 min</span>
</div>
<div style="display:flex; align-items:center; gap:10px">
<span style="font-size:11px; color:#6b7280">Last scan: 3m ago</span>
<button style="background:transparent; color:#7c3aed; border:1px solid #e8d5f5; padding:5px 14px; border-radius:9999px; font-size:11px; font-weight:500">↻ Refresh</button>
<span style="font-size:10px; color:#6b7280">v1.7.0</span>
</div>
</div>
<!-- Card -->
<div style="background:#1a1b26; border:1px solid #2d2f3d; border-radius:6px; padding:14px; margin-bottom:12px">
<div style="font-size:13px; font-weight:600; color:#7c3aed; margin-bottom:8px">Open Reviews Being Tended (2)</div>
<!-- Table row example -->
<div style="display:flex; align-items:center; gap:8px; padding:6px 0; border-bottom:1px solid #2d2f3d; font-size:12px">
<a style="color:#7c3aed; text-decoration:none">#1234</a>
<span style="font-size:11px; max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap">Fix manifest dict format</span>
<span style="background:#fef3c7; color:#b45309; padding:2px 7px; border-radius:9999px; font-size:10px; font-weight:600">iterating</span>
<span style="margin-left:auto">
<button style="background:transparent; color:#7c3aed; border:1px solid #e8d5f5; padding:5px 14px; border-radius:9999px; font-size:11px; font-weight:500">💬 Chat</button>
</span>
</div>
</div>
<!-- Update banner -->
<div style="background:#e8d5f5; color:#7c3aed; padding:8px 14px; border-radius:9999px; font-size:12px; display:flex; justify-content:space-between; align-items:center">
<span>Update available: v1.6.0 → v1.7.0</span>
<button style="background:#7c3aed; color:#fff; border:none; padding:5px 14px; border-radius:9999px; font-size:11px; font-weight:500">Update Now</button>
</div>
</div>
```
Key visual rules from the mock:
- Dark theme uses `var(--bg)` / `var(--border)` / `var(--text)` — don't hardcode dark hex
- Light purple badge for metadata (`#e8d5f5` bg, `#7c3aed` text)
- Amber status pills (`#fef3c7` bg, `#b45309` text)
- Ghost buttons with purple text on light purple border
- Version as plain muted text (smallest element, 10px)
- Cards have 14px padding, 6px radius, 12px bottom margin
### Entry Module Pattern
```javascript
import { useState, useEffect } from 'react'
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from 'react/jsx-runtime'
import { useAppApi, useAppInfo, useNavigate } from '@kirocrew/app-sdk'
export default function MyApp() {
const [state, setState] = useState({})
const [loading, setLoading] = useState(true)
const api = useAppApi()
const { version: appVersion } = useAppInfo()
const navigate = useNavigate()
useEffect(() => {
async function load() {
try {
setState(await api.get('/api/apps/MY-APP/config'))
} finally {
setLoading(false)
}
}
load()
const interval = setInterval(load, 30000) // 30s polling
return () => clearInterval(interval)
}, [api])
// ... render UI
}
```
GitHub에서 보기