Building a Rome App
From an idea to a git-versioned Rome App the daemon is running — scaffold, install, iterate.
Agent-readable version: Markdown · llms.txt
This page takes you from "I want to build X" to a working app the daemon is running, tracked in git. It covers what a Rome App is, how to decide whether your idea should be an app at all, and the scaffold → install → iterate loop.
What Rome Apps are
Rome is a personal AI agent platform for one user: each installed copy serves exactly one person (the guardian). New features come as Rome Apps — plugins you install that stand on their own. Each app lives in its own source tree (tracked in git), and the daemon loads it from a packed artifact.
A single app can ship any mix of:
| Artifact | What it is |
|---|---|
| Actions | Typed operations you can call — the main thing other code and agents run |
| App-private agents | LLM agents that belong to the app, run via agentRunner or the platform system:summon action |
| Skills | Small plain-language guides an agent loads when it needs them |
| Hooks | Callbacks that run on the daemon event loop (e.g. channel-message) |
| HTTP API | Routes mounted for the app UI at /api/apps/<appId>/... and at /api/app-api/<appId>/... for callbacks/webhooks (session-gated unless api.noAuth or public access opens it) |
| Database | Drizzle tables in the shared system SQLite, kept apart by tablePrefix |
| Web UI | A React bundle mounted at /apps/<appId> inside a shadow DOM |
Everything an app ships is listed in one app.yaml file. You build on the two
public SDKs, and only those — never by importing Rome Core source:
@rome-os/app-runtime— the backend SDK (actions, hooks, API handlers,appContext,agentRunner, types).@rome-os/app-web-sdk— the frontend SDK plus theromebuild CLI (web mounting, host design tokens,fetchAppApi, routing helpers).
The default template that ships with Rome sets up one action, one API route,
and one default-export React UI, with the db: and agents: blocks commented
out in app.yaml for you to enable when needed. (A second workflow template
scaffolds a workflow app instead — see the next section.) Treat it as a starting
point — delete the parts you don't need instead of keeping unused code around.
App vs workflow: a quick test
Most "build me something that does X" requests are workflows, not apps. Run this test before you set anything up.
A workflow takes inputs, runs a series of steps (transform data, call
actions, use system:summon for an LLM step), and gives back a result — a summary,
digest, draft, a triage decision, watch-and-notify, a mix of several sources, or
"do X when Y". This is the usual shape, even when you want to run it again and
again and look back at past runs (the platform saves run history for free; you
never build that).
Build a full app only if at least one of these is true:
- It owns data the user edits — records they create, update, delete, and come back to (a tracker, CRM, library, kanban board). Wanting to see past runs does not count; that is free platform history, not your own data model.
- It has more than one thing to do — several actions the user picks between (add, browse, set up, approve…), not just "run it". A single trigger, even with inputs, is still a workflow.
- It has an agent the user talks to, or one that sticks around — something
that holds memory and its own tools across turns. A one-shot "make X from Y"
is a
system:summonstep, not an agent.
If none of those are true, stop and build a workflow instead. Two signs you
have wrongly turned a pipeline into an app: adding a db: section just to store
past outputs, or setting up a custom agent for a one-shot job.
A workflow is a verb (do this, give me the result). An app is a noun (a place that holds my stuff).
Scaffold and install
The whole flow goes through the system:app_management action. The daemon does build →
pack → install in one step; don't create files by hand, and don't let the daemon
pick paths.
1. Pick an absolute repo path under the authoring root
Pick the path first. It must sit under the app authoring directory from your
runtime context — ~/.rome/<profile>/projects/apps — so the app shows up in the
dashboard. The appId must match /^[a-z][a-z0-9-]{0,63}$/: short, lowercase,
with hyphens. core and self are reserved.
REPO="${ROME_APP_AUTHORING_ROOT:-$HOME/.rome/${ROME_PROFILE:-default}/projects/apps}/<appId>"
mkdir -p "$REPO" && cd "$REPO"Don't use $HOME/projects — that is outside the projects tree, and the app
wouldn't show in the dashboard.
2. Scaffold the template
rootPath is required and must be an absolute path — the daemon picks no
default. It won't write into a folder that already has files, so run this
before git init (a .git/ folder counts as "not empty" and would make the
step fail).
template is also required — there is no default. Choose workflow for
pipeline-shaped requests (take inputs, run steps, return a result) and
default only for genuinely app-shaped software, per the "App vs workflow"
test above.
// system:app_management
{ "op": "create", "appId": "<appId>", "rootPath": "<absolute $REPO>", "template": "default" }3. git init and commit the baseline
Start git after setting up, so commit #1 is "the template as shipped" and
every later change shows only your own work. The template ships a .gitignore
that leaves out .rome/ and dist/, so build and pack output never ends up in
git.
git init
git add -A && git commit -m "Initial scaffold of <appId>"4. Source-mode install
One call builds, packs, and installs. The daemon runs the workspace's
pnpm install + pnpm build, packs the result into $REPO/.rome/artifact, and
installs that packed artifact. spec.source points at the source repo, not
the artifact. Don't pass appId — the daemon reads it from the manifest and
gives it back to you.
// system:app_management
{ "op": "install", "source": { "mode": "source", "path": "<absolute $REPO>" } }The app is now installed. $REPO is its permanent source home;
$REPO/.rome/artifact is the packed artifact the daemon manages — never edit or
commit it.
Iterate loop
Each piece of agent work is one git commit. Every change to an app goes through the same loop:
- Edit files under
$REPO/src/. - Commit before installing — uncommitted source is not valid to release:
git add -A && git commit -m "<conventional commit message>" - Re-install with the same
sourceevery time. The daemon can't guess it from the lockfile, so repeat the first install's source on every round:// system:app_management { "op": "install", "source": { "mode": "source", "path": "<absolute $REPO>" } } - Refresh
/appsor/apps/<appId>in the dashboard to pick up the changes.
Every install rebuilds and repacks, so edits to YAML alone or migrations alone need nothing extra — the same single call ships them.
Catch build errors locally first
system:app_management runs the build inside the daemon, but you can run the same build
on your own machine. The template's package.json exposes the rome CLI
through normal scripts:
// package.json scripts
{
"dev": "rome dev", // web bundle with HMR
"build": "rome build", // backend tsc → dist/, web bundle → dist/web/
"typecheck": "tsc --noEmit",
"db:generate": "drizzle-kit generate"
}Before you call a change done, run these from $REPO:
pnpm install # if you added a dependency or just created the app
tsc --noEmit # full type check (backend + web)
pnpm build # catch build errors locally before installing
git status # working tree must be clean — every change is committedRun rome dev when you want live reload on the web UI while you work on it. Run
pnpm build when you want build errors shown before installing, instead of
finding them inside the daemon.
Common failures
| Error | Cause and fix |
|---|---|
apps.create: invalid params — rootPath: … | rootPath omitted from op: "create" — schema validation rejects the call. Supply the absolute path from step 1. |
apps.create: invalid params — template: … | template omitted from op: "create" — it is required with no default. Pass "default" or "workflow". |
scaffoldDevApp: rootPath must be an absolute path | The path was relative, or had a ~ that wasn't expanded. Turn it into a full path first. |
App directory <path> already exists and is non-empty | Usually means you ran git init before op: "create". Scaffold into the empty folder first, then git init. Don't delete files that are already there to "fix" this. |
op: "install" fails because source is missing | Every install needs source. Pass { mode: "source", path: "<absolute $REPO>" }. |
op: "install" fails with ARTIFACT_INVALID | The source.mode you gave doesn't match what's on disk. The error names the exact source to pass — follow it exactly. |
op: "install" fails with "App build failed in …" | The workspace's own build failed inside the daemon; the version already installed keeps running. Run pnpm --dir "$REPO" install && pnpm --dir "$REPO" run build to see it, fix it, then re-install. |
Next steps
Once you have a working tree and the daemon is reading from it, move on to the
rest of the build: product-design rules, the frontend design system, the data
model, agents, and the file-by-file API reference (app.yaml, action.yaml,
agent YAML, and the two SDKs). For brand-new apps, finish by checking the app
works in a fresh, clean context before you call it done.