Build a feature with the API
Most people build a Feature and its resources by asking Ronja in a chat. You can also build them straight over HTTP with a scoped API token — deterministic, scriptable, and with no AI spend. This guide walks the ordered sequence: create a Feature, add a Workflow, then a App.
The machine-readable entry point is /llms.txt — a lean index that links to task recipes and the full endpoint reference at /docs/api/endpoints.md (every endpoint with the scope it needs) plus the OpenAPI schema at /docs/api/openapi.json. The interactive reference is served at /docs/api. This guide is the tutorial; those are the contract.
Before you start: mint a token
Section titled “Before you start: mint a token”The very first token cannot come from the API — minting tokens is an Admin-only action, and a scoped token can only ever mint an equal-or-narrower child, never a wider one. So the cold start is always the web UI.
- Follow Send data via the API to open API Tokens and create a token. Name it (e.g.
feature builder) and pick a Role that bounds what it can do. - Keep Limited to scopes and grant only what your script needs:
- Structure → Write — create the Feature.
- Automation → Write — create and publish the Workflow.
- Analytics → Write — create and commit the App.
- Data → Write — create and fill tables (see Send data via the API).
- Secrets → Write / Agents → Write — only if the Feature binds secrets or saved agents.
- Copy the secret from the Token created modal — it is shown exactly once.
Send it on every request in the Authorization header as a Bearer token:
export RONJA_BASE_URL="https://<your-org>.ronja.tech"export RONJA_API_TOKEN="<paste your token>"alias rapi='curl -sS -H "Authorization: Bearer $RONJA_API_TOKEN"'The API endpoint at POST /api/v2/authentication/token re-mints an equal-or-narrower token from an already-authorized caller — handy for a script that hands a still-narrower token to a sub-task, but it can never be your bootstrap.
The recipe
Section titled “The recipe”1. Create the Feature
Section titled “1. Create the Feature”scope is either private (the default — visible only to you) or organization to share it; organization needs a higher role.
rapi -X POST "$RONJA_BASE_URL/api/v2/feature" \ -d '{"name":"Weekly sales report","scope":"private"}'# -> 200 {"id":"<feature-id>","name":"Weekly sales report", ...}Save the returned Feature id — every resource below is created inside it.
2. Get data in (optional)
Section titled “2. Get data in (optional)”A Workflow that reads a table needs that table to exist first. Creating a table, pushing Parquet, and building it is already covered in Send data via the API — follow it, then come back with your table ids (table-...).
3. Create and publish the Workflow
Section titled “3. Create and publish the Workflow”A Workflow is created as a draft, filled with code, then published to go live.
# Create the draft — featureID is required.rapi -X POST "$RONJA_BASE_URL/api/v2/workflow" \ -d '{"featureID":"<feature-id>","title":"Roll up weekly sales"}'# -> 200 {"id":"workflow_...", "lifecycle":"draft", ...}
# Push the code. Markers in the body bind the workflow's data dependencies.rapi -X PUT "$RONJA_BASE_URL/api/v2/workflow/workflow_.../files/main.py" \ -d '{"content":"df = {{ ref(\"sales_raw\") }}\nweekly = df.resample(\"W\").sum()\n{{ write(\"weekly_sales\") }} = weekly\n"}'
# Publish the draft to make it live.rapi -X POST "$RONJA_BASE_URL/api/v2/workflow/workflow_.../publish"{{ ref('alias') }} (an input table), {{ write('alias') }} (an output table), and {{ secret('id') }} are resolved on the server from the code you send — you send code, not resolved ids. The input tables a {{ ref }} names must already exist. Publishing is required: a freshly created Workflow stays a draft until you publish it.
4. Create and publish the App
Section titled “4. Create and publish the App”An App is created empty and unpublished — a draft only you can see — gets its source files, then a required validate step, then commit to publish it. There is no one-shot publish.
# Create the app. Declare every table/secret/workflow/agent it uses up front.rapi -X POST "$RONJA_BASE_URL/api/v2/dataapp" \ -d '{"featureID":"<feature-id>","name":"Weekly sales","allowedTableIDs":["table_..."]}'# -> 200 {"id":"dataapp_...", ...}
# Push the entry file. It must match the app's entry point — App.tsx by default.rapi -X PUT "$RONJA_BASE_URL/api/v2/dataapp/dataapp_.../files/App.tsx" \ -d '{"content":"import { createRoot } from \"react-dom/client\";\nfunction App() { return <h1>Weekly sales</h1> }\ncreateRoot(document.getElementById(\"app\")).render(<App />);"}'# -> 200 {"dataAppID":"dataapp_...", "path":"App.tsx", ...}# dataAppID is the row the write landed on. On a new app that is the id you# just created — same id, still unpublished.
# Validate + compile the draft. Required before commit.rapi -X POST "$RONJA_BASE_URL/api/v2/dataapp/dataapp_.../validate"
# Commit to publish the app. The id does not change.rapi -X POST "$RONJA_BASE_URL/api/v2/dataapp/dataapp_.../commit"You can also have Ronja render the app headlessly and look at what it did — POST /api/v2/dataapp/<id>/preview hands back a screenshot, the errors it hit and the queries it ran, as an observation and never a pass/fail verdict; it sits on the Data scope because it runs the app’s real queries, and the app-development guide linked from /llms.txt covers it in full.
The restore-and-publish endpoint re-publishes an already-committed version — it is not the fresh-app path, so don’t reach for it here.
Gotchas
Section titled “Gotchas”- Check a Workflow before you create it.
POST /api/v2/workflow/validatedry-runs a whole candidate Workflow — Feature id, entry point, and file contents — and returns every problem it finds, each attributed to the file it came from: unresolved{{ ref }}/{{ write }}/{{ secret }}markers, a missing entry point, invalid parameters. It takes no Workflow id and saves nothing, so you can iterate until it comes back clean before anything is created. It is optional, and it does not check whether you are allowed to write into a shared Feature — that is still decided when you save. - An App publishes in three steps. Push files, then
validate, thencommit. Skipping validate makes commit fail — a draft must have compiled cleanly since its last file edit. - Declare bindings first. Unlike building in a chat, the API does not scan your source for referenced tables, secrets, workflows, or agents. Anything your Workflow or App uses must be declared — an App’s
allowedTableIDs/allowedSecretIDs/allowedWorkflowIDs/allowedAgentIDsin the create body or viaPOST /api/v2/dataapp/:id/checkout, a Workflow’s dependencies through its{{ ref }}/{{ secret }}markers. For an App, only secret references are checked when it compiles — everything else is not checked at all. An app whose SQL reads a table that is not inallowedTableIDscompiles clean, validates clean and publishes, and then every query it makes fails once someone opens it. Nothing warns you at any step, so check the allowlists yourself: an emptyallowedTableIDsis the single most common reason a published app draws its layout and then shows no data. - You can only bind what you personally have access to. Binding a table, secret, workflow, or agent you cannot reach returns 403. An App’s viewers inherit whatever it binds — potentially reaching across Features — so this check is a real access boundary, not a formality. Being an Admin does not lift it: an Admin can open another person’s private-Feature table by id, but binding it into an App is refused all the same (
table "…" is not accessible). Share the resource into a Feature you both reach, or move it, and bind it then. - An App’s entry file must mount itself. It has to end with
createRoot(document.getElementById("app")).render(<App />). A file that only defines and exports a component is refused — nothing calls it, so it would build and then show a blank page, and the compile says so rather than letting you publish it. That check catches source that never mounts; it is not a promise that a build that passes renders. A component that mounts and returns nothing, or fails on first render, still publishes. Open the app and look. - A new App starts as a draft.
POST /api/v2/dataappcreates it unpublished and visible only to you, the same as a Workflow. The firstcommitpublishes that same row — the id never changes — so an app you start and abandon leaves nothing behind in the Feature. - Live versus draft. The first file edit to an already-published App forks a private draft. The response’s
dataAppIDis the row your edit landed on — if it differs from the id you sent, your change is on a draft and is not live until you validate and commit. Read the field to know where you are. - In a shared Feature, creating is Admin-only — the review gate is for edits. If you are not an Admin,
POST /api/v2/dataappin a shared Feature is refused outright with a 400 (admin required to create a data app in a shared feature). There is no review lane for a brand-new app, so there is nothing to submit and nothing to retry — post it toPOST /api/v2/dataapp/proposeinstead, which creates a proposal you author files against while an Admin approves or rejects it. A brand-new Workflow is refused one step later: the draft is created, andPOST /api/v2/workflow/:id/publishis the call that fails, withPOST /api/v2/workflow/proposeas its review lane. Both propose endpoints sit on the Admin scope, so a token limited to Analytics and Automation cannot reach them. Editing an already-published Workflow or App is the case that lands as a draft: your edits fork one automatically,commitis refused, and you callPOST /api/v2/dataapp/:id/request-review(orPOST /api/v2/workflow/draft/:id/request-review) to put it in front of an Admin. On your own private Feature everything commits directly. - You author as the token’s user. A token acts on behalf of the user who minted it: resources it creates are owned by that user, so a
privateFeature it creates is that user’s private Feature. The Activity log still records the token itself as the actor, so automated actions stay distinguishable from that person’s own work — you cannot post work attributed to Ronja or to someone else. Watch the scope of a shared team token: because the token owns what it creates as its minting Admin, anything it creates inprivatescope becomes that Admin’s private resource — invisible to whoever else operates the token. For automation that a team shares, preferorganizationscope so the resources are visible to everyone who should see them; reserveprivatescope for a token you alone use. - Partial failures leave orphans. No single request spans Feature to Workflow to App, so a failure mid-chain can leave an orphan Feature with nothing in it. Use stable, idempotent names so a retry doesn’t duplicate, and clean up by listing the Feature’s resources and deleting the orphan.
Find the exact endpoints
Section titled “Find the exact endpoints”This guide covers the golden path. Start from the machine-readable index at <your-org>.ronja.tech/llms.txt (also served at /.well-known/llms.txt): it links to the full endpoint list at /docs/api/endpoints.md (every endpoint with its required scope) and the raw OpenAPI spec at /docs/api/openapi.json. The interactive reference (try calls, read every field) is served at /docs/api.
Using Claude Code
Section titled “Using Claude Code”Drop this into your project’s CLAUDE.md so an AI coding assistant builds against the API correctly. It teaches only the shape and defers the detail to the live index, so it won’t go stale:
## Ronja API
Base URL: https://<your-org>.ronja.tech — every call sends `Authorization: Bearer $RONJA_API_TOKEN`.
Discover the API from the live index first: fetch `<base>/llms.txt`. It's a lean index —it links to task recipes and the full endpoint list at `<base>/docs/api/endpoints.md`(every endpoint with its required scope).Don't guess request bodies — read the OpenAPI spec at `<base>/docs/api/openapi.json` for exact shapes.
Golden path to build a Feature with a Workflow + App:1. POST /api/v2/feature {name, scope} -> Feature id2. (optional) create + fill tables — see the "Send data" guide3. POST /api/v2/workflow/validate {featureID, entrypoint, files} (optional dry-run: no id, saves nothing, findings per file) POST /api/v2/workflow {featureID, title} -> draft PUT /api/v2/workflow/:id/files/main.py {content} (markers resolved server-side) POST /api/v2/workflow/:id/publish -> live4. POST /api/v2/dataapp {featureID, name, allowedTableIDs} -> empty + unpublished draft PUT /api/v2/dataapp/:id/files/App.tsx {content} (entry file = app entry point; must end with createRoot(...) .render(...) or it is refused) POST /api/v2/dataapp/:id/validate (REQUIRED before commit) POST /api/v2/dataapp/:id/commit -> published (same id)
Checklist:- App = push files -> validate -> commit. There is no one-shot publish.- A new App is a DRAFT visible only to you until the first commit publishes it. The id never changes, and an app you abandon leaves nothing behind.- Declare bindings first: allowedTableIDs / allowedSecretIDs / allowedWorkflowIDs / allowedAgentIDs (create body or POST :id/checkout). The API does NOT auto-scan source.- Only SECRET references are compile-checked. An undeclared table/workflow/agent compiles, validates and publishes clean, then fails at run time once someone opens the app. Nothing warns you — verify the allowlists yourself before committing.- Only bind resources this token's user personally reaches, else 403 — Admin is no exception.- Editing an already-published App forks a draft; the response `dataAppID` is where the edit landed — if it differs from the id you sent, it's a draft, not live.- Shared Feature + non-admin — three different outcomes, do not conflate them: * CREATE an App -> 400 "admin required to create a data app in a shared feature". There is NO review lane for a new app, so do not retry and do not try to submit it for review. Use POST /api/v2/dataapp/propose (needs the admin scope) and author files against the returned proposal; an Admin approves it live. * CREATE a Workflow -> the draft is created fine, but POST /api/v2/workflow/:id/publish is refused. Use POST /api/v2/workflow/propose (needs the admin scope) instead. * EDIT a published Workflow / App -> the edit forks a draft and commit is refused; POST /api/v2/dataapp/:id/request-review (or /api/v2/workflow/draft/:id/request-review) puts it in front of an Admin.- No transaction spans the chain — a mid-chain failure leaves an orphan Feature; use idempotent names and clean up the orphan.