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 Data 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 Data 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 Data app
Section titled “4. Create and publish the Data app”A Data app is created empty and live, gets its source files, then a required validate step, then commit. 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":"export default function App() { return <h1>Weekly sales</h1> }"}'# -> 200 {"dataAppID":"dataapp_...(a DRAFT id)", "path":"App.tsx", ...}
# Validate + compile the draft. Required before commit.rapi -X POST "$RONJA_BASE_URL/api/v2/dataapp/dataapp_.../validate"
# Commit the draft to replace the live app.rapi -X POST "$RONJA_BASE_URL/api/v2/dataapp/dataapp_.../commit"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. - A Data app publishes in three steps. Push files, then
validate, thencommit. Skipping validate makes commit fail — a draft must have been validated 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 Data app uses must be declared — a Data 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. An undeclared reference returns a 400 at validate time. - You can only bind what you can read. Binding a table, secret, workflow, or agent you don’t have access to returns 403. A Data app’s viewers inherit whatever it binds — potentially reaching across Features — so this check is a real access boundary, not a formality.
- Live versus draft. The first file edit to a live Data 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. - Shared Features have a review gate. On a shared Feature, if you are not an Admin, your Workflow and Data-app changes land as a draft awaiting admin review instead of going live. On your own private Feature they commit 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 Data 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 + Data 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 + live PUT /api/v2/dataapp/:id/files/App.tsx {content} (entry file = app entry point) POST /api/v2/dataapp/:id/validate (REQUIRED before commit) POST /api/v2/dataapp/:id/commit -> live
Checklist:- Data app = push files -> validate -> commit. There is no one-shot publish.- Declare bindings first: allowedTableIDs / allowedSecretIDs / allowedWorkflowIDs / allowedAgentIDs (create body or POST :id/checkout). The API does NOT auto-scan source.- Only bind resources this token can read, else 403.- Live vs draft: the first Data-app file edit 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 = changes land as a draft awaiting review, not live.- No transaction spans the chain — a mid-chain failure leaves an orphan Feature; use idempotent names and clean up the orphan.