Skip to content

Use the Ronja CLI

The ronja CLI signs your terminal in to Ronja. It exists mainly so scripts and AI coding agents can work against the API without anyone pasting credentials around by hand.

It is deliberately not a wrapper around the API. It does the one thing a script cannot do for itself — an interactive browser sign-in — and then hands over everything needed to call the API directly over HTTP. Two commands:

Terminal window
ronja login # sign in through the browser
ronja context # print everything needed to call the API

Two more make those HTTP calls easier without wrapping anything: ronja api sends a request to any path with your credentials attached, and ronja query runs read-only SQL and gives you CSV. Both are further down this page.

There are three more exceptions, and they are the same idea three times: ronja wf keeps a workflow’s Python in a folder on your machine, ronja app keeps an app’s source (TSX) in one, and ronja pipeline keeps a feature’s derived-table SQL in one. Either way you write it in your own editor and keep it in your own version control, then validate, push and publish from the terminal.

The CLI is a single binary with nothing to configure. On a Mac:

Terminal window
brew install ronjatech/tap/ronja

On Linux, or anywhere you already have Go:

Terminal window
go install github.com/ronjatech/ronja-cli/cmd/ronja@latest

brew is macOS-only — Homebrew does not install this kind of package on Linux. If you would rather not use either, download a binary for your platform from the releases page, unpack it, and put ronja somewhere on your PATH.

If the command is not found after go install, your Go bin directory is not on your PATH:

Terminal window
export PATH="$PATH:$(go env GOPATH)/bin"

Add that line to your shell profile (~/.zshrc or ~/.bashrc) to make it stick. Check it worked:

Terminal window
ronja --help
ronja --version

To upgrade later, brew upgrade ronja or re-run the go install command.

Terminal window
ronja login

The CLI prints a short code and opens your browser. Check that the code on screen matches the one in your terminal, confirm you recognise the machine listed as Requested by, then choose Authorize. Your terminal picks up the result within a few seconds.

If the machine has no browser — a server over SSH, a container — use:

Terminal window
ronja login --no-browser

and open the printed URL somewhere you are signed in. The code stays valid for 10 minutes and works only once.

With no --url, the CLI signs you in to https://cloud.ronja.tech. Pass --url only when your Ronja lives somewhere else — a self-hosted deployment, or a development server:

Terminal window
ronja login
ronja login --url http://localhost:8080

Each login is stored as a named profile: one instance, one organization, and the token that reaches it. Several can be signed in at the same time, and the most recent login becomes the current profile. To see them all, switch, or target one for a single command:

Terminal window
ronja profile list
ronja profile use acme-retail
ronja query --profile local "SELECT 1"

Run ronja profile use with no name to pick from a list instead — arrow keys to move, Enter to choose, q or Esc to cancel. It starts on the profile you are already using, so Enter alone changes nothing.

Any command also takes --url to pick by instance instead of by name.

If you belong to more than one organization

Section titled “If you belong to more than one organization”

An access token belongs to one organization — the one you are signed in to in the browser at the moment you approve. The approval screen names it, so check it there.

To add a second: switch organization in the web app, then run ronja login again. The new token is stored as its own profile beside the first, never on top of it, and each is named after its organization.

Because two profiles can then share one instance, --url alone is no longer enough to tell them apart. When it is ambiguous the CLI names both and asks you to pick with --profile rather than guessing — the difference between them is which organization you are about to write to.

Profiles on your own machine are named by port — local-8082 — so several development servers running at once stay tellable apart.

Rename anything you dislike; the name is only a local handle:

Terminal window
ronja profile rename app-2 northwind
Terminal window
ronja whoami

This asks the server rather than reading the file on disk, so a revoked token is reported as revoked. It prints the user, organization and role the CLI is acting as. Add --json for a machine-readable answer — every command supports it.

Terminal window
ronja context

This prints which instance you are on and as whom, how to authenticate, and then the instance’s own API index — the guides, what you can build, and where the endpoint reference, schemas, and built-in skills live. It reads that index live from the server, so it always matches the API in front of you.

If you run it inside a workflow, app or pipeline folder — or in a folder holding Python — it ends by saying so and listing the commands for that kind of folder.

From there you work over plain HTTP. Load your stored login into the environment:

Terminal window
eval "$(ronja env)"

That sets RONJA_URL and RONJA_TOKEN. Because eval consumes the command’s output, the token goes straight into your environment without ever being displayed — so it stays out of your terminal history, your logs, and, if you are working with an AI agent, its transcript.

Terminal window
curl -H "Authorization: Bearer $RONJA_TOKEN" "$RONJA_URL/api/v2/authentication/me"

Environment variables do not survive between separate commands. If something runs each command in a fresh shell — most AI agent tools do — combine the two steps:

Terminal window
eval "$(ronja env)" && curl -H "Authorization: Bearer $RONJA_TOKEN" "$RONJA_URL/api/v2/feature/query"

If RONJA_TOKEN is already set, that value wins and there is nothing to load.

Everything ronja context points at is served without authentication, so any HTTP client can read it:

Path What it is
/llms.txt The index: task guides, what you can build, and links to everything below
/docs/api/endpoints.md Every endpoint with the token scope it requires
/docs/api/skills.md Ronja’s own agent expertise, as an index
/docs/api/openapi.json Request and response schemas

/docs/api/skills.md is worth knowing about: it lists the same built-in skills Ronja’s own agent uses — how to design a feature, author a workflow, define a metric, build an app. It is an index, one line per skill; fetch the full text of the one you need from /docs/api/skills/<slug>.md. Read the relevant skill before building that kind of resource and you will get the shape right the first time.

ronja api makes a request to your instance with your credentials already attached. You give it a path; it adds the base URL and the authorization header.

Terminal window
ronja api /api/v2/authentication/me

This is the safer form of the eval "$(ronja env)" recipe above: your token is loaded inside the command and never reaches a shell variable, a command line, or an agent’s transcript.

It knows nothing about individual endpoints — it is a way to send a request, not a menu of things to send. Find out what to call from ronja context and the reference it points at.

To send a body, use -d:

Terminal window
ronja api -X POST /api/v2/feature -d '{"name":"Sales","scope":"private"}'
Flag What it does
-X, --method The HTTP method. Defaults to GET, or POST when you pass a body.
-d, --data The request body. -d @file.json reads it from a file, -d @- reads it from a pipe — which is easier than escaping JSON on a command line. Either way the body is capped at 16 MiB.
-F, --form Send a file. -F file=@report.pdf attaches a file from your machine, -F note=draft adds a plain field. Repeat the flag for more than one. Use this for upload endpoints, which do not accept -d.
-H, --header An extra header, written as "Name: value". Repeat the flag for more than one. A body is sent as application/json unless you override it here.
-q, --jq Pull one part out of the response with a jq expression, so you do not need a second tool to read it.
-r, --raw With --jq, print text results without quotes — the form you want when the result goes straight into another command.
-o, --out Write the response to a file instead of printing it, readable only by your own user account.
--timeout How long to wait for one request. Two minutes by default; 0 waits for as long as the request takes.
--retry Retry this many times if the instance is busy or briefly unavailable. Off by default.
--fail-on-error Treat a successful response that carries an error message as a failure. See the caution under Query your data.

The path must start with / — the instance address is added for you, so pass /api/v2/... rather than a full URL. It is sent exactly as you write it, query string included, so escape anything that needs escaping: a space in a search term has to be written %20, or the request will be rejected.

The response is printed exactly as the server sent it, so you can pipe it straight into a file. That includes error responses: if a request fails, the body still prints, a single line naming the status and path goes to the error stream, and the command exits non-zero — so a script stops rather than continuing on a failure.

If you ask for a path that does not exist, the reply suggests the closest real ones — most usefully when the path is right but the method is not.

Terminal window
ronja api -X POST /api/v2/file/upload/quarterly.pdf -F file=@./quarterly.pdf

The reply carries both an id and a key for the uploaded file. Keep both: different parts of the API ask for one or the other, and they are not interchangeable.

Terminal window
ronja api /api/v2/feature/query --jq '.items[].name'
featureID=$(ronja api -X POST /api/v2/feature -d @feature.json --jq '.id' -r)

Put the expression straight after --jq and any other flags after it. --jq takes a value, so --jq -r '.id' reads -r as the expression and the command refuses it, telling you the working form.

Some work — a workflow run, a table build, a data sync — starts immediately and finishes later. --wait-until keeps asking until a condition is true, so you do not have to write a loop:

Terminal window
ronja api "/api/v2/workflow/run/$runID" \
--wait-until '.status == "done" or .status == "error"' --jq '.status' -r

Wait for the statuses that mean finished, rather than for “not running any more” — a Durable workflow can pause partway through at Waiting, and a condition like .status != "running" would treat that pause as the end and hand you a run with no result.

It asks every three seconds and gives up after five minutes; --wait-interval and --wait-timeout change both. It only works on requests that read, never on ones that create something — otherwise waiting would make one thing per attempt.

ronja query runs read-only SQL against your Tables and gives you CSV back.

Terminal window
ronja query "SELECT * FROM {{ ref('tableID') }} LIMIT 10"

Tables are referenced by ID with {{ ref('tableID') }}, not by name — the ID appears in the table’s URL in the app.

Real SQL is rarely one line, so it can come from a file or a pipe instead:

Terminal window
ronja query --file monthly-report.sql
cat monthly-report.sql | ronja query

Give the SQL in exactly one of those three ways. If you give none, the command tells you so rather than waiting.

Flag What it does
--out Write the CSV to a file instead of printing it. Nothing goes to the output stream, so this is the one to use for a result too big to read on screen — except with --json, where the file gets the CSV and the JSON object still prints. The file is readable only by your own user account.
--json Print the whole response — the CSV, the row count, whether it was cut short, and the reporting timezone — as a single JSON object.
--jq Pull one value out of that response — the row count, whether it was cut short, or the reporting timezone. Not the rows themselves: those are CSV, which jq does not read. Cannot be combined with --json.
-r, --raw With --jq, print text results without quotes.
--max-rows Ask for at most this many rows. Without it you get the server’s own limit.
--timeout How long to wait. Two minutes by default; 0 waits for as long as the query takes.

A query over a lot of data is sent to larger compute automatically, and that can take minutes. If a heavy query stops with a timeout, it was not rejected — raise --timeout and run it again.

If your result hits the row limit, the command says so on the error stream and still exits successfully — the rows you got are real, there are simply more of them. Raise --max-rows, add --out to write them to a file, or aggregate in SQL.

Every successful query also prints Reporting timezone on the error stream, and carries it as zoneUsed in the --json response. It is there because the CLI sends no timezone of its own, so your query reads in UTC, while the same query in the app reads in your own timezone. Anything grouped by day, week or month therefore falls on different boundaries in the two places — the numbers differ and both are right. When a figure from the terminal disagrees with the same figure in the app, check this first.

ronja db works with a Managed database — the Ronja-hosted Postgres you build a system on, as opposed to the Tables ronja query reads. Everything here needs the Admin role.

Terminal window
ronja db sql <database ID> "SELECT * FROM leads LIMIT 10"

The SQL comes from an argument, a file, or a pipe — exactly as it does for ronja query — and the result is CSV, with the same --out, --json, --max-rows and --timeout flags.

Pass values into a statement rather than building them into it:

Terminal window
ronja db sql <database ID> "INSERT INTO leads (email, score) VALUES (\$1, \$2)" \
--params '["a@b.c", 42]'

Two things differ from ronja query, and both are worth knowing:

  • It cannot change the schema. This runs as the database’s write role, so CREATE, ALTER and DROP are refused. Schema changes go through ronja db migrate, which records them in the database’s own history.
  • A statement that fails is a plain failure. Unlike ronja query, there is no successful-response-carrying-an-error to watch for: the message prints, the command exits non-zero, and so does ronja api or curl if you call the endpoint yourself.

A new database has no connection roles at all, so your first statement fails until one exists. Create one:

Terminal window
ronja api -X POST /api/v2/database/<database ID>/user \
-d '{"access":"write","featureID":"<feature ID>"}'

Keep your schema changes as .sql files in a migrations/ folder. Each file is one migration, named after the file, and they apply in filename order — so zero-pad the numbers, because 10_x.sql sorts before 9_x.sql.

Terminal window
ronja db migrate status --database <database ID>
ronja db migrate push --database <database ID>

status reports what is applied, pending or drifted and changes nothing. It exits non-zero when anything has drifted, so it works as a check in an automated job without anyone parsing its output. push applies everything not yet applied, all in one transaction: if the last one fails, none of them applied.

Both send the whole folder every time and let the database work out what is new, so running push twice is safe — the second run reports everything as already applied. After the first successful push the database is remembered, and --database is no longer needed in that folder.

Creating, listing and deleting databases stay on ronja api:

Terminal window
ronja api -X POST /api/v2/database -d '{"name":"crm"}'
ronja api /api/v2/database/query

ronja wf — short for ronja workflow — keeps one Workflow’s Python in a folder on your machine. You edit it in your own editor, keep it in your own version control, and push it to Ronja when it is ready. Everything you push goes to your own draft of the workflow, never straight to the live one.

Use this rather than ronja api whenever the code is in files. It sends the whole folder in one command, keeps the workflow’s parameters in the folder alongside the code, and runs and waits for a test run for you — none of which you get from sending files one at a time.

From a workflow that already exists:

Terminal window
ronja wf clone <workflow ID>

The workflow ID appears in the workflow’s URL in the app. If you already have a draft of it — from the app, or from an earlier push — those are the files you get, because they are your newest version.

Or from a Python script you already have:

Terminal window
ronja wf init --from scripts/monthly_report.py --feature <feature ID>

The feature ID appears in the feature’s URL. Unlike clone, which copies the workflow down into a new folder, init sets up the directory you are already in — so you can run it inside an existing repository. Nothing is created in Ronja yet: the workflow comes into existence on your first push, so you can get the script working first and only then have it appear in the app.

Terminal window
ronja wf status # what changed locally, and in Ronja
ronja wf validate # check the folder, save nothing
ronja wf push # send it to your draft
ronja wf test --param month=2026-07 # run the draft and wait for it
ronja wf publish # take it live, or ask for review
Command What it does
status Files you changed since your last push, whether you have a draft, and anything that changed in Ronja since — the app edits the same draft, so this is worth checking before you push.
validate Sends the folder to Ronja and reports what a save would reject: a table, agent or Codex reference that does not resolve, a missing main file, a file name Ronja cannot store. A secret it cannot reach is reported as a warning instead — the save succeeds and that credential is simply not bound. Parameters you declared are checked the same way: a parameter with no name is an error, while a questionable declaration — an option list on a parameter that is not a dropdown, a default value that does not match the parameter’s type — is a warning you can push past. Nothing is saved, and the workflow does not have to exist yet.
push Validates first, then copies the folder into your draft — creating the draft, or the workflow itself on a first push. Add --no-validate to skip the check, --force to overwrite a draft that changed in the app since your last push. If a single file changed in Ronja since you last synced, push stops on that file and names it rather than overwriting it.
test Runs your draft and waits for it, then prints the status, log and outputs. Pass each value as --param name=value. Add --stale-ok to run the draft as it stands when the folder has changes you have not pushed. If the draft is Durable and pauses, test stops waiting there and reports the status as waiting with what ran so far — it does not sit out the pause. It exits zero, because nothing failed: the run resumes on its own, so check it in Ronja for the rest.
publish Publishes a workflow that has never been live, commits your draft onto one that has, or — on a shared workflow only an admin can commit — submits it for review and tells you so. If somebody else published a change while you were working, publish refuses and tells you what moved; add --overwrite-remote to commit yours anyway and replace theirs.
discard Throws your draft away. The live workflow and your local files are untouched. Asks first; pass --yes when there is no terminal to ask on. If the workflow has never been published, discard refuses — that draft is the workflow, so throwing it away would delete it; delete it from the app instead.

status, push and publish also print a URL: line — a link to the workflow in Ronja, so you can open it — which is simply left out, and never reported as a failure, when the command didn’t reach Ronja or your instance has no web address configured.

Every command takes --json, and each one exits non-zero when it fails, so an automated job stops rather than continuing on a broken workflow.

A workflow that requires approval before each run cannot be run from the terminal at all — the approval can only be given in a chat, so test that kind from a chat in the app.

File Keep it in version control?
Your Python, including any files it imports Yes
ronja.json — the title, the file Ronja runs first (main.py unless you change it), the workflow’s parameters, its reporting timezone, and which workflow this folder maps to in each instance and organization Yes
.ronja/ — the record of what you last pushed No; it is ignored for you automatically
Anything else beginning with a dot — .env, .git/, .venv/ Your choice; Ronja never syncs them

Hidden files are never sent. Anything whose name starts with a dot, at any level of the folder, stays local — so a .env beside your code is not uploaded, and a .git/ directory is not walked. status, validate and push each say which files they left out and why, so nothing disappears quietly. A workflow that already has a file with a hidden name cannot be cloned into a folder at all: the file would be written once and then invisible to every later command. Rename it in the app first.

The mapping in ronja.json records one entry per instance and organization, so the same folder can target a test organization and your production one — even on the same instance — without either overwriting the other. Use --profile or --url to choose which one a command talks to.

A workflow’s parameters — the values you fill in when you run it, and the ones you pass as --param name=value — are declared in ronja.json, so they live in version control next to the code that reads them. Your Python reads each one with tools.getVariable("name").

"parameters": [
{ "name": "month", "label": "Month", "type": "date", "required": true },
{ "name": "upto", "label": "Up to", "type": "number", "defaultValue": 10 }
]

type is string, number, date or select. Add description to explain the value, defaultValue to prefill it, required to insist on it, and for select, either options (a fixed list) or optionsQuery (SQL that produces the list).

ronja wf push makes the workflow match what you declared — adding, changing and removing parameters. ronja wf status shows what you have declared and tells you when it differs from the workflow. ronja wf test --param accepts only the names you declared, so a typo is caught before anything runs.

If ronja.json has no parameters at all — which is how folders created before this worked look — the CLI leaves the workflow’s parameters alone, and you can keep editing them in the app. Add the key (use [] if there are none yet) to take ownership of them from the folder.

ronja.json can also declare the workflow’s reporting timezone — the calendar its runs group days, weeks and months in. It is how you change that calendar from the terminal, and it keeps the setting in the same commit as the code it applies to.

"reportingTimezone": "Europe/Stockholm"

The key works like parameters, in the same three states:

In ronja.json What push does
Not there at all Leaves the workflow’s timezone alone
"" Sets the workflow to UTC
"Europe/Stockholm" Makes the workflow match

Leaving it out is what a folder made before this existed looks like, so it has to mean leave it alone — otherwise the first push after upgrading would quietly move every one of those workflows to UTC. ronja wf clone writes the key when the workflow has a timezone; ronja wf init leaves it out, so a brand-new workflow inherits your organization’s default.

"" means reset to UTC, not “no timezone”. There is no way to put a workflow back to having none: a workflow with no timezone is one created before workflows had the setting, or one created while your organization has no default, and it follows whoever started the run instead. ronja wf status reports a difference between the folder and the workflow, and ronja wf push refuses to overwrite a timezone somebody changed in the app since you last synced.

ronja app is the same loop for an app. Everything about the folder works the way it does for a workflow — the same ronja.json, the same record of what you last pushed, the same one-entry-per-organization mapping, the same warning when someone else has changed things since you last synced.

Terminal window
ronja app status # what changed locally, and in Ronja
ronja app clone <data-app-id> # or: ronja app init --feature <feature-id>
$EDITOR App.tsx
ronja app push # sends your files, then checks that they build
ronja app validate # run the build check on its own
ronja app test # open the app in a browser for you and report what it saw
ronja app publish # publish it, or ask an admin to
ronja app discard # throw your draft away
Command What it does
init Sets up the directory you are already in, optionally copying in a component you already have with --from. --feature is required, --title names the app. Nothing is created in Ronja yet.
clone Copies an existing app’s files down into a new folder, permissions included. Prefers your own draft over the published version, because that is your newest work. Nothing is created in Ronja.
status Files you changed since your last push, whether you have a draft, whether it builds, which permissions differ from ronja.json, and anything that changed in Ronja since. Read-only, and the local half works signed out.
push Creates the app if this is the first push, then sends the permissions, then the files, then checks the build. Add --no-validate to skip the check, --force to overwrite a draft that changed in the app since your last push.
validate Rebuilds your draft on its own and reports what failed. Warns when the folder holds changes you have not pushed — it checks what is on the server, not what is on your disk.
test Opens your draft in a real browser somewhere in Ronja, waits for it to finish loading, and reports what it saw: a screenshot, anything that went wrong while it ran, and what it fetched. Writes a report.json and the screenshots into the current folder (--out-dir puts them elsewhere). --route '#/orders' opens a particular view; --viewport mobile renders it phone-sized.
publish Publishes the app, or — on a shared app only an admin can commit — submits it for review and tells you so. Refuses a draft that does not build. --no-request-review turns the review route into a failure instead of submitting, which is what you want in an automated job that expects the change to go live directly.
discard Throws your draft away; the published app and your local files are untouched. Asks first; pass --yes when there is no terminal to ask on. An app that has never been published is its draft, so discarding it would delete the app — that needs --delete-app. It is a soft delete: the app goes to the trash for 30 days, your local files are kept, and the folder is unbound so your next push creates a fresh app.

status, push and publish also print a URL: line — a link to the app in Ronja, so you can open it — which is simply left out, and never reported as a failure, when the command didn’t reach Ronja or your instance has no web address configured.

Every command takes --json, and each one exits non-zero when it fails.

ronja.json says which kind of folder it is, so running a workflow command inside a data-app folder is refused rather than doing something surprising.

Like a workflow, a new app is a draft until you publish it. Your first push creates it unpublished and visible only to you — nobody else sees it in the feature, and an app you start and abandon leaves nothing behind. ronja app publish publishes that same app: the ID never changes, so a link you shared while building keeps working.

Four things work differently from a workflow, and each one is visible the first time you use it.

The first file is always App.tsx. An app builds from that file and the name cannot be changed, so the folder simply reports it.

What the app is allowed to read lives in ronja.json. A workflow’s tables and secrets are worked out from its code. An app’s are not — you say what it may reach, under access:

"access": {
"allowedTableIDs": ["table-abc"],
"allowedSecretIDs": [],
"allowedAgentIDs": [],
"allowedWorkflowIDs": [],
"allowedCodexIDs": [],
"allowedMetricIDs": [],
"capabilities": []
}

An app pushed without this builds and displays, but has nothing to show — so fill it in before your first push. Nothing later reminds you: an app with an empty list compiles, validates and publishes exactly like a finished one, and the gap only shows when somebody opens it. Because these are permissions rather than settings, ronja app push and ronja app status list every single thing being granted or taken away, one per line, instead of just saying that something changed. You can only grant what you can already reach yourself, and for a shared feature an admin still has to approve the change.

Leave capabilities empty for an app that reads Ronja tables — listing the table is all it needs. It is only for apps that do something beyond reading your data: ask the AI for a completion, query or write to an outside database, or take a file upload.

If ronja.json has no access at all — which is how folders created before this worked look — the CLI leaves the app’s permissions alone and you keep editing them in the app. Add the key to take ownership of them from the folder.

Pushing takes a few seconds per file. Ronja rebuilds the whole app on every file it receives, so a push says which file it is working on. It also means the app will not build for a moment in the middle of a push — a file that mentions another one you have not sent yet. That is expected and is not a failure; your files are saved either way. The check at the end is the one that counts, and it is reported as Compiles: yes or Compiles: NO with the errors.

An app that does not build cannot be published. ronja app publish refuses it and shows you the errors, and the live app keeps serving its last published version until you fix them. ronja app validate runs the same check on its own.

ronja app test looks at the app for you. Ronja opens your draft in a real browser, waits for it to finish loading, and hands back what it saw:

Terminal window
ronja app test
ronja app test --route '#/orders' --viewport mobile

It writes report.json and one screenshot-1.png, screenshot-2.png, … per picture taken, plus screenshot.png for the one that best represents the finished page, when the render produced one. Some renders take no pictures at all — a bundle that did not build has nothing to photograph — and running it again clears the previous run’s pictures first, so what you are looking at is always this run. On screen you get a short summary: whether the page settled, how many errors it hit, and what it fetched.

It is a report, not a verdict. There is no pass or fail, and it never blocks anything — ronja app test finishes successfully even for an app full of errors, because the errors are the point of the report. Read it in this order: look at the screenshot; check whether the page settled — either the app said it had finished everything it started, or it simply went quiet (if it did not settle, it was still working when time ran out — that is slow, not broken, and --timeout gives it longer than the 20 seconds it waits by default); look at what it fetched and how many rows came back, because a blank panel above a query that returned nothing is usually the data, not the code; and only then read the errors, which name the line in App.tsx. Pass --fail-on-errors when you want an automated job to stop on them — it stops on an app that did not build, too, which is the case that reports no errors at all because there was nothing to run.

You can also drive the app while it renders. --steps names a file holding a JSON array of interaction steps — [{"action":"click","text":"Refresh"},{"action":"screenshot"}] — which run against the app once it has loaded, and a {"action":"screenshot"} step is how you capture what the page looked like after them. This needs the Admin role, because the steps really happen: a click runs the app’s own code with the same authority anyone opening the app has, so it can start workflows and Saved Agents, upload files and call outside systems. Nothing is mocked and nothing is rolled back — treat a step file the way you would treat clicking through the live app yourself.

Now and then it reports that Ronja’s browsers were busy. It waits and tries once more, and if they are still busy the command fails rather than reporting a clean app it never saw. That says nothing about your app — wait a moment and run it again; do not change anything in between.

node_modules, dist and build are never sent, and are listed among the files each command left out. An app holds at most 100 files, of at most 5 MiB each and 50 MiB in total — all three checked on your machine, so an oversized folder is refused before anything is sent rather than halfway through. See Limits and retention.

Work on a feature’s tables from your editor

Section titled “Work on a feature’s tables from your editor”

ronja pipeline — aliased pl — keeps a Feature’s Derived tables as .sql files in a folder on your machine. One file is one table. You edit the SQL in your own editor, keep it in your own version control, and push it to Ronja when it is ready.

Everything you push goes to your own draft of each table, and pushing builds that draft rather than the live table — so a query that does not work changes nothing anyone else can see.

From a feature that already has derived tables:

Terminal window
ronja pipeline clone <feature ID>

The feature ID appears in the feature’s URL in the app. You get one .sql file per derived table, named after the table. If you already have a draft of one — from the app, from a chat, or from an earlier push — that is the SQL you get, because it is your newest version. Only Derived tables are copied: Foundation, Integration and Dynamic tables have no SQL to hold, and metrics are not handled by this command yet. The CLI says which ones it left out.

Or start an empty folder in the directory you are already in:

Terminal window
ronja pipeline init --feature <feature ID>

Nothing is created in Ronja yet. Each table comes into existence on the first push of the file that describes it — and unlike a new workflow or app, a new table is visible in the feature straight away, holding no data until you publish it.

Terminal window
ronja pipeline status # what changed locally, and in Ronja
ronja pipeline push [file.sql ...] # send each changed file to your draft and build it
ronja pipeline publish [file.sql ...] # commit your drafts, or ask an admin to
ronja pipeline discard [file.sql ...] # throw your drafts away

push, publish and discard all take file names, and act on the whole folder when you give none.

Command What it does
status Files you changed since your last push, files that have no table yet, each table’s build state, any draft you have open, and anything that changed in Ronja since — chats and the app edit the same draft, so this is worth checking before you push. Read-only: it does not even open a draft.
push For each changed file: creates the table if it is new, opens or reuses your draft, writes the SQL, builds it, and reports the result — which columns the change adds or drops, which input tables it starts or stops reading (Inputs: +/-), how many rows it produced against the live table, and, for a draft under a million rows, a few sample rows. Above that the sample is skipped and the command says so — reading rows out of a table that large costs real compute, so query it directly if you want a look. Files build in dependency order, so a table is built after everything in the folder it reads from. Name files to push only those. --force overwrites a table whose SQL changed in Ronja since your last push.
publish Commits each draft onto its table — or, in a shared feature only an admin can commit, submits it for review and tells you so. Refuses a draft whose build failed, one that is still building, and one that has never been built. Publishes in dependency order, so a table lands after everything in the folder it reads from. Name files to publish only those. --no-request-review turns the review route into a failure instead of submitting, which is what you want in an automated job that expects the change to go live directly.
discard Throws your drafts away. The live tables and your local files are untouched, so status will show the folder as changed against them; push again to start fresh drafts. Name files to discard only those. Asks first; pass --yes when there is no terminal to ask on.

Every command takes --json, and each one exits non-zero when it fails.

status is a check you can gate on. The exit code answers one question: has anything moved in Ronja under me? It works as a check in an automated job without anyone parsing the output — the same contract ronja db migrate status has — and --json still prints the full report either way, so you can read the detail when you want it.

It exits non-zero when:

  • a table’s SQL moved in Ronja since your last sync — either the live table or your own open draft, because a chat or the app edits the same draft you do;
  • it could not check at all: no working sign-in, an instance it could not reach, a table listing that failed;
  • the binding is ambiguous or broken — the folder names a table your record was not taken from, or an instance entry it cannot resolve;
  • a table it is bound to could not be read, so drift cannot be ruled out for it.

“Not checked” is a different answer from “everything is fine”, and a job that treated the two alike would report a clean folder it never looked at.

It exits zero when nothing on the server has moved, as far as this folder can tell — which includes three cases that are easy to misread as problems:

  • a file you changed only on your machine. Local edits are not the gate; the gate is what changed in Ronja. Pushing is how you resolve them, and status lists them either way.
  • a file with no table in Ronja yet — a fresh init with nothing pushed. There is nothing on the server to have moved.
  • a fresh clone, with no local record of what you last pushed. .ronja/ is never committed, so a colleague who has just cloned the folder has no record to compare against — that is the normal way somebody joins a pipeline, not a failure. status says there is nothing to check yet and points you at ronja pipeline push to establish one. push treats the same state the same way: its overwrite guard stands down when there is nothing to compare.

--json is where the local picture lives: the files you changed and the ones with no table yet are in the payload even though they do not move the exit code.

File Keep it in version control?
Your .sql files, one per table Yes
ronja.json — the title, and which table each file maps to in each instance and organization Yes
.ronja/ — the record of what you last pushed No; it is ignored for you automatically
Anything else — a README, notes, test fixtures Your choice; Ronja never syncs them

Only .sql files are sent, and everything else is ignored without comment, so a pipeline folder can sit inside an ordinary repository. Deleting a .sql file does not delete its table: the CLI says the file is gone and leaves the table alone. Delete tables in the app.

Write each input as {{ ref('table ID') }}, the same form the app and the API use. The CLI works out each table’s inputs from the references in its file, so adding an input is editing the SQL and nothing else. A numbered reference like {{ ref('0') }} is refused — it means “the first of this table’s declared inputs”, which is not something a file on your machine can know, and pushing it would silently point the query at a different table.

Publishing rebuilds everything downstream. When a draft is committed, every table that reads the published one is rebuilt automatically — so there is nothing to run afterwards, and no run command to look for.

Starting a new table needs the Admin role, so a first push of a new file is refused otherwise. Editing an existing table is not: anyone with the User role who can read the table can push and build their own draft, and then submit it for review. A User Read-Only account can clone the folder and check its status, and nothing more.

Approving in the browser creates a personal access token bound to you. It appears under Account → Access tokens, named after the machine that requested it.

Two things follow from it being yours:

  • It carries your live role. If your role changes, the token’s permissions change with it. If you leave the organization, it stops working. You cannot use it to grant someone else more access than they already have.
  • Anything it creates belongs to you. A feature it creates in private scope is your private feature.

The token expires 90 days after you sign in — ronja whoami shows the date, and signing in again renews it. Revoke it sooner from Account → Access tokens when a machine no longer needs it, and immediately if a laptop is lost or a token is pasted somewhere it should not have been.

On your machine it is written to a config file readable only by your user account — ronja context prints the exact path (~/Library/Application Support/ronja/config.json on macOS, ~/.config/ronja/config.json on Linux).

To remove a stored credential:

Terminal window
ronja logout

That forgets one profile — the current one, unless --profile or --url names another. Other organizations on the same instance keep their own logins. It does not revoke the token — do that from Account → Access tokens if the token may have been exposed.

Two environment variables override everything stored on disk, so an automated job never depends on who last signed in on that machine — and never writes a credential to disk:

Terminal window
export RONJA_URL="https://cloud.ronja.tech"
export RONJA_TOKEN="<a token>"
ronja whoami --json

For CI, mint a token in the web UI (Account → Access tokens) and put it in your secret store. You can also pipe an existing token in instead of opening a browser:

Terminal window
echo "$RONJA_TOKEN" | ronja login --url "$RONJA_URL" --with-token

Commands never prompt when they are not attached to a terminal, and exit non-zero on failure, so an automated job fails loudly rather than hanging.