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 rome CLI (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_management action 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 it

dev 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 build

The 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 dev

Use 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 redefine lib entries 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, or patch.
  • The optional [directory] can be the app folder or any folder inside it; the command walks up to find app.yaml. It defaults to the current folder.

What each bump does:

Release1.4.2 becomes
major2.0.0
minor1.5.0
patch1.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.3

App 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.json

rome 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: @me

rome 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-run packages 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:

  • source is 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.source points at the source repo, not the artifact. Pointing mode: "source" at <app-root>/.rome/artifact (or mode: "bundle" at the repo root) is the wrong shape and fails with ARTIFACT_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 build to see it, fix it, and re-install.

So the loop is: edit under <app-root>/src/git add -A && git commitsystem: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 the active symlink) but keeps the app's DB tables and data, so a later reinstall brings the state back.
  • purge: true wipes 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 } and read_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.

On this page