Skip to content

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.

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.

  1. 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.
  2. 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.
  3. 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:

Terminal window
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.

scope is either private (the default — visible only to you) or organization to share it; organization needs a higher role.

Terminal window
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.

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-...).

A Workflow is created as a draft, filled with code, then published to go live.

Terminal window
# 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.

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.

Terminal window
# 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.

  • Check a Workflow before you create it. POST /api/v2/workflow/validate dry-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, then commit. 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 / allowedAgentIDs in the create body or via POST /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 dataAppID is 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 private Feature 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 in private scope becomes that Admin’s private resource — invisible to whoever else operates the token. For automation that a team shares, prefer organization scope so the resources are visible to everyone who should see them; reserve private scope 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.

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.

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 id
2. (optional) create + fill tables — see the "Send data" guide
3. 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 -> live
4. 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.