Build, Install & Iterate
Move an app from source to running: build, pack, install, and update.
Agent-readable version: Markdown · llms.txt
A Rome app lives in a permanent source tree (tracked in git) and is run by the daemon from a packed artifact. Two separate tools move an app through its life:
- The
romeCLI (shipped by@rome-os/app-web-sdk) builds the app on your machine, runs the web bundle with live reload, bumps the manifest version, and publishes to the Rome App Store. - The
system:app_managementaction drives the daemon: it builds, packs, and installs in one call, uninstalls, and enables/disables apps.
You can't swap one for the other. rome build/rome dev are for local
feedback; the daemon does its own build during system:app_management { op: "install" }.
Don't mix up either of these with the Rome monorepo's own pnpm scripts — those
are not part of app development.
The rome CLI
The CLI has a small set of commands. Running rome, rome -h, rome --help,
or rome help prints usage.
rome dev Start the dev server with HMR
rome build Build the app web bundle for production
rome upgrade Bump app.yaml version by major, minor, or patch
rome login Log in to the Rome App Store and store credentials
rome whoami Show the currently logged-in account
rome publish Package a Rome app directory and upload itdev and build read app.yaml (web.displayName, web.navLabel,
web.entry) and write dist/ with a manifest.json plus assets/. Run
rome <command> --help for the options of each command.
rome build
rome build makes the production build from the current folder. It sets
NODE_ENV=production (if it isn't set), clears the backend output folders under
dist/, runs the rslib build that compiles the backend (TypeScript) and bundles
the web UI, then copies backend assets and the app icon into the output.
# from <app-root>
rome buildThe result is the dist/ tree the daemon would otherwise make — backend output
plus the web bundle under dist/web/. You normally don't need to run this by
hand: the daemon runs the same build during install. Run it on your machine only
when you want to see build errors before installing.
rome dev (HMR)
rome dev runs the web bundle in watch mode so you can work on it locally with
hot reload. It sets NODE_ENV=development (if it isn't set), clears backend
output, copies backend assets and the app icon, then starts the rslib build with
watch: true.
# from <app-root>
rome devUse this when working on the React UI and you want instant feedback in the
browser. Backend, action, and DB changes still ship through a system:app_management
install.
Customizing the build (rslib.config.ts)
The build is rslib under the hood, and the SDK generates
the whole config — most apps never touch it. When you do need to (an extra
bundler plugin, a custom define, an asset rule), drop an rslib.config.ts
(or .js / .mjs) at the app root. rome build, rome dev, and the daemon's
install build all load it and merge it over the generated base config, so
you only declare the delta:
// <app-root>/rslib.config.ts
import { defineConfig } from "@rome-os/app-web-sdk/config";
export default defineConfig({
source: {
define: { "import.meta.env.MY_FLAG": JSON.stringify(true) },
},
});The @rome-os/app-web-sdk/config subpath re-exports defineConfig,
mergeRslibConfig, loadConfig, and the RslibConfig type from
@rslib/core, so the app doesn't need its own rslib dependency.
Watch out: the merged config still has to produce the layout the daemon expects (backend output plus the web bundle under
dist/web/). Add to the config; don't redefinelibentries or output paths.
rome upgrade (version bump)
rome upgrade bumps the SemVer version field in app.yaml.
rome upgrade <major|minor|patch> [directory]- The release part is required and must be one of
major,minor, orpatch. - The optional
[directory]can be the app folder or any folder inside it; the command walks up to findapp.yaml. It defaults to the current folder.
What each bump does:
| Release | 1.4.2 becomes |
|---|---|
major | 2.0.0 |
minor | 1.5.0 |
patch | 1.4.3 |
The current version must be valid SemVer (e.g. 1.2.3); the edit leaves the
rest of the manifest as-is. On success it prints the manifest path and the
previous -> new change.
$ rome upgrade patch
Updated /path/to/app.yaml: 1.4.2 -> 1.4.3App Store: login, whoami, publish
These commands talk to the Rome App Store over HTTP. Only rome login takes a
--host <url> flag (falling back to the ROME_STORE_HOST environment
variable, then a built-in default). rome whoami and rome publish resolve
the host themselves: with ROME_TOKEN set they use ROME_STORE_HOST (or the
default), otherwise they use the host saved when you logged in.
rome login signs you in and saves a session token.
rome login --host <url> [--email <email>] [--password <pw>] [--password-stdin]If you leave out email and password, you'll be asked for them. --password
works but is not safe; use the prompt or --password-stdin instead. On success
the session token is written to ~/.rome/store-token.json with mode 0600.
$ rome login
Email: me@example.com
Password:
Logged in as me@example.com. Saved to /home/me/.rome/store-token.jsonrome whoami prints the account tied to the current credentials: host,
email, slug, where the auth came from (ROME_TOKEN env var or a 30-day session,
with the time left), and the publishing handles you have. If no credentials are
found, it tells you to set ROME_TOKEN (made in Settings → CLI / API token) or
run rome login.
$ rome whoami
host: https://store.example.com
email: me@example.com
slug: me
auth: session (expires in 29d)
handles: @merome publish packages an app folder and uploads it.
rome publish <directory> [--dry-run] [--out <file>]
[--exclude <pattern>] [--no-default-exclude]The folder must contain app.yaml; the manifest's id and version are
required. The app is packaged as a single-rooted gzip tarball that comes out the
same every time (mtime/uid/gid are stripped, so the same source gives the same
content hash). By default it leaves out node_modules, .git, and .DS_Store;
add more with --exclude (repeatable), or drop the defaults with
--no-default-exclude.
--dry-runpackages and computes the SHA-256 hash without uploading.--out <file>also writes the tarball to disk.
To upload, you must be logged in (or have ROME_TOKEN set).
$ rome publish ./my-app --dry-run
Packaged my-app@1.4.3 — 81234 bytes, sha256 9f86d0...
Dry run — skipping upload.system:app_management lifecycle
The system:app_management action is how the daemon creates, installs,
uninstalls, and turns apps on and off. It is what you use while iterating — not
the rome CLI build commands.
Source-mode install (build, pack, install in one step)
A source-mode install hands the daemon the app's source repo. In one call the
daemon runs the workspace's own pnpm install + pnpm build (the same as
rome build — backend TypeScript to dist/, web bundle to dist/web/), packs
the result into <app-root>/.rome/artifact, and installs the packed artifact,
hot-swapping in the new code.
{
"op": "install",
"source": { "mode": "source", "path": "<absolute app-root>" }
}Key rules:
sourceis required on every install. The daemon does not guess it from the lockfile or remember it between calls; pass the same{ mode: "source", path: "<absolute app-root>" }on the first install and every one after.spec.sourcepoints at the source repo, not the artifact. Pointingmode: "source"at<app-root>/.rome/artifact(ormode: "bundle"at the repo root) is the wrong shape and fails withARTIFACT_INVALID, which names the exact source to pass — follow it exactly.- Every install rebuilds and repacks, so edits to YAML alone, migrations alone, or the backend alone ship through the same single call — nothing extra is needed.
- If the daemon's build fails ("App build failed in …"), the version already
installed keeps running. Run
pnpm --dir "<app-root>" install && pnpm --dir "<app-root>" run buildto see it, fix it, and re-install.
So the loop is: edit under <app-root>/src/ → git add -A && git commit →
system:app_management { op: "install", source: … } → check it against the running
app → refresh /apps or /apps/<appId>. Never install uncommitted code — the
commit is what makes the change easy to review and undo.
The daemon-managed artifact
<app-root>/.rome/artifact is the packed build output the daemon can load. The
daemon makes it during install, and you never edit or commit it by hand. The
template's .gitignore leaves out .rome/ and dist/, so build and pack
output stays out of git.
Uninstall and purge
{ "op": "uninstall", "appId": "<appId>", "purge": false }- Default (
purge: false) removes the app's deployment folder (deployment.yaml, the per-version install folders, and theactivesymlink) but keeps the app's DB tables and data, so a later reinstall brings the state back. purge: truewipes everything the app owns, including its data.
Enable / disable
To stop or restart an app without reinstalling, toggle spec.enabled:
{ "op": "set_enabled", "appId": "<appId>", "enabled": false }The reconciler settles by loading or unloading the app's artifacts via hot-swap. First-party apps that ship with Rome support only enable/disable, not install/uninstall.
Finding what already exists
Before writing new code, look up what's already there. The harness gives you messaging, scheduling, agent calls, and other apps' actions; reusing them beats writing your own clients. Your session has a live catalog.
search_actions / read_action
search_actions { query }finds registered actions by name, description, or argument text.read_action { action_name }returns a per-argument summary (name, type, required, enums, description) plus metadata (sideEffects,requiresApproval).
The catalog is live: actions from an app you just installed show up right away.
read_action does not expand nested argument shapes — for complex inputs,
check the exact shape against the owning app's docs or source instead of
guessing from memory.
The catalog and appContext.runAction(canonicalId, args) are served by the same action
registry, so read_action is the right place to start for any runAction call:
base the canonical <app-id>:<local-name> ID and arguments on its summary before
you make the call.
search_skills
search_skills { query }andread_skill { skill_name }bring up the how-to docs shipped by installed apps. Use them to find a set workflow before making one up.
(For event-driven features, the system:search_event_catalog action finds event types
that can be emitted and their payload schemas — the fields a routine
trigger.filter can match.)
Prefer existing actions over your own clients
If an existing action does the job — sending a message, scheduling a routine,
summoning an agent, a Composio tool call — call it via appContext.runAction.
Don't build your own HTTP client, notifier, or scheduler inside the app. The
catalog covers actions, skills, and events; the rest of the
@rome-os/app-runtime SDK (db, log, runAction, settings) is the
in-process API documented with the SDK.