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:

ArtifactWhat it is
ActionsTyped operations you can call — the main thing other code and agents run
App-private agentsLLM agents that belong to the app, run via agentRunner or the platform system:summon action
SkillsSmall plain-language guides an agent loads when it needs them
HooksCallbacks that run on the daemon event loop (e.g. channel-message)
HTTP APIRoutes 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)
DatabaseDrizzle tables in the shared system SQLite, kept apart by tablePrefix
Web UIA 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 the rome build 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:

  1. 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.
  2. 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.
  3. 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:summon step, 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:

  1. Edit files under $REPO/src/.
  2. Commit before installing — uncommitted source is not valid to release:
    git add -A && git commit -m "<conventional commit message>"
  3. Re-install with the same source every 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>" } }
  4. Refresh /apps or /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 committed

Run 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

ErrorCause 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 pathThe 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-emptyUsually 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 missingEvery install needs source. Pass { mode: "source", path: "<absolute $REPO>" }.
op: "install" fails with ARTIFACT_INVALIDThe 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.

On this page