# Building a Rome App
URL: /docs/building-apps/quickstart
Markdown: /docs/building-apps/quickstart.md
Description: From an idea to a git-versioned Rome App the daemon is running — scaffold, install, iterate.

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 [#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 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 [#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 &#x2A;*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, &#x2A;*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 [#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 [#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.

```bash
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 [#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.

```jsonc
// system:app_management
{ "op": "create", "appId": "<appId>", "rootPath": "<absolute $REPO>", "template": "default" }
```

### 3. git init and commit the baseline [#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.

```bash
git init
git add -A && git commit -m "Initial scaffold of <appId>"
```

### 4. Source-mode install [#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.

```jsonc
// 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 [#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:
   ```bash
   git add -A && git commit -m "<conventional commit message>"
   ```
3. Re-install with the &#x2A;*same `source`** every time. The daemon can't guess it
   from the lockfile, so repeat the first install's source on every round:
   ```jsonc
   // 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 [#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:

```jsonc
// 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`:

```sh
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 [#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 [#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.