Apps
The apps API is the API orun.work speaks: a person describes an app in a chat, the Work Agent asks what it needs to know with cards, proposes a plan, and — once the plan is approved — creates the app, builds it milestone by milestone and puts it live. Everything orun.work does, a script can do with the same calls.
Two families of routes, both under the account:
/v1/organizations/{account}/app-chats…— chats, their messages, cards and live stream./v1/organizations/{account}/apps…— the apps those chats made.
{account} is the account (the root organization) you work in, as org_…, ws_… or its slug. An app ref is the app workspace's ws_… — every app is a workspace of its own in the account. Chat ids are ach_…, message ids amsg_…, card ids acd_….
Responses use the platform envelope { data, meta: { requestId, cursor } }; lists are cursor-paginated (limit defaults to 50, max 100 — see Pagination); every POST honours Idempotency-Key.
Authentication
Send a bearer, any one of:
- An account API key with scope
apps(sk_…) — for scripts and servers. Create one in the console under the account's settings, orun.work keys (/accounts/{account}/api-keys), or withPOST /v1/organizations/{account}/api-keysand{ "label", "role", "scope": "apps" }on the account itself (a workspace answers412 apps_key_needs_account). The secret,sk_and 64 hex characters, is shown once. Its role (owner,admin,builderorviewer) is what it may do on the account; a script that approves plans — which creates the app's workspace — needsowner. It reaches the apps routes,GET /v1/accountsandGET /v1/identity/me, and nothing else: every other route answers 404, webhooks included. - An
apps-scoped OAuth token — what an app gets from Orunbase's sign-in when it asks forscope=apps. Same reach. orun.work itself is the console on its own host and signs in as the console does. - A session or CLI token (
orun-cloud login) — reaches these routes like any other.
Every route re-authorizes deny-by-default:
| Route | Permission | Checked on |
|---|---|---|
| Reads | organization.app.read | the account |
| Chat writes (create, send, stop, answer a card) | organization.app.write | the account |
| Approving a plan (creates the app) | creating a workspace — the app's workspace is made with the approver's credential | the account (owner, account_owner, account_admin) |
| Ship, restore | organization.app.write | the account |
| Revoke a connection, archive | organization.app.manage | the app's workspace |
A chat is visible to the person (or key) that started it. Someone else asking for it gets 404.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /v1/organizations/{account}/app-chats | Start a chat (202) |
GET | /v1/organizations/{account}/app-chats | Your chats, newest first |
GET | /v1/organizations/{account}/app-chats/{chatId} | A chat |
PATCH | /v1/organizations/{account}/app-chats/{chatId} | Rename, archive or unarchive a chat |
DELETE | /v1/organizations/{account}/app-chats/{chatId} | Delete a chat and its messages (never its app) |
POST | /v1/organizations/{account}/app-chats/{chatId}/messages | Send a message (202) |
GET | /v1/organizations/{account}/app-chats/{chatId}/messages | The chat's history, oldest first |
GET | /v1/organizations/{account}/app-chats/{chatId}/messages/{messageId} | One message |
POST | /v1/organizations/{account}/app-chats/{chatId}/stop | Interrupt the Work Agent's current turn |
GET | /v1/organizations/{account}/app-chats/{chatId}/events?from={seq} | The live stream (text/event-stream) |
GET | /v1/organizations/{account}/app-chats/{chatId}/progress | The progress object |
GET | /v1/organizations/{account}/app-chats/{chatId}/cards?state=open | The chat's cards |
POST | /v1/organizations/{account}/app-chats/{chatId}/cards/{cardId} | Answer a card |
GET | /v1/organizations/{account}/apps?archived=true | The account's apps |
GET | /v1/organizations/{account}/apps/{appRef} | An app |
PATCH | /v1/organizations/{account}/apps/{appRef} | Rename an app |
POST | /v1/organizations/{account}/apps/{appRef}/archive | Archive an app |
GET | /v1/organizations/{account}/apps/{appRef}/preview | The app's preview |
GET | /v1/organizations/{account}/apps/{appRef}/versions | Its versions, newest first |
POST | /v1/organizations/{account}/apps/{appRef}/versions/{sha}/restore | Restore a version (202) |
POST | /v1/organizations/{account}/apps/{appRef}/ship | Ship what you have (202) |
GET | /v1/organizations/{account}/apps/{appRef}/connections | Where it can run, and its consents |
DELETE | /v1/organizations/{account}/apps/{appRef}/connections/{connectionId} | Revoke a consent |
Browsing an app's files (…/files, …/files/raw) and downloading it as a ZIP (…/download) are planned but not served yet. Its code is in its GitHub repository (repository.url on the app). The chat list's appRef and state filters are not served yet either.
Chats
Start a chat
curl -X POST "https://api.orunbase.com/v1/organizations/$ACCOUNT/app-chats" \
-H "Authorization: Bearer $ORUN_APPS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "message": "A booking page for my yoga studio, with a waitlist" }'
{
"data": {
"chat": {
"id": "ach_5b1f0c9e2d8a4f6b9c3e7a1d0f2b4c6e",
"title": "A booking page for my yoga studio, with a waitlist",
"state": "intake",
"appRef": null,
"author": { "id": "sp_…" },
"progress": { "stage": "understanding", "headline": "Understanding your idea", "…": "…" },
"lastSeq": 1,
"createdAt": "2026-10-31T09:00:00.000Z",
"updatedAt": "2026-10-31T09:00:00.000Z",
"archivedAt": null
},
"messageId": "amsg_9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b"
},
"meta": { "requestId": "req_…", "cursor": null }
}
The body takes message (1–8,000 characters), an optional title (the Work Agent names the chat otherwise) and an optional appRef: a ws_… workspace of this account to work in. The Work Agent asks before touching an existing workspace (an attach card); a workspace that is already an app answers 409 conflict, details.reason: app_has_chat.
A Build chat
To plan and build one more piece of work on an app that already exists, pass kind: "build" with the app's appRef:
{ "message": "Add a waitlist when a class is full", "kind": "build", "appRef": "ws_ABCD1234", "parentChatId": "ach_…" }
The Work Agent asks nothing about the app (it already works there) and sets nothing up. It proposes a plan for this piece of work alone; its plan card has no starting point and no place to run to choose, and Approve writes the plan into the app's workspace as an epic of its own, which the chat then carries as epic. parentChatId is optional: one of your own chats about the same app, recorded as where this one came from.
An app builds one thing at a time. If another of its chats is building, a Build chat's work waits its turn — its progress headline says which chat it waits for — and starts by itself.
The app must be one of this account's that you can write to (404, 403), and not archived (409, app_archived).
kind says what a chat about an existing app is for. Each needs appRef.
kind | What the chat does | When an approved plan is built |
|---|---|---|
chat | Does what it is asked: answers, plans, or gets a change made | When you ask for it |
plan | Plans the work and writes the plan down | When you ask for it |
build | Plans the work and builds it | As soon as you approve it |
move | Plans moving something you already have onto the app | When you ask for it |
To have the chat work in a repository other than the app's own, pass repoLinkId: a repository linked in the app's workspace that agents may work in. Anything else answers 422.
With no kind, a chat with no appRef is app. setup is not started by this call and answers 422, details.reason: kind_not_allowed.
The Chat object
| Field | Notes |
|---|---|
id | ach_… |
title | |
kind | What the chat is for: app, setup, plan, build, move or chat. Fixed when the chat is created. app takes an app from the idea through its build and its follow-ups; the others are described under creating a chat |
parentChatId | The ach_… of the chat whose card started this one, else null |
planChatId | The ach_… of the chat that holds the plan this chat's work belongs to, else null. A list of chats nests under it |
epic | { ref, slug } — the epic this chat builds in the app's workspace, once its plan is written; else null |
state | intake, planning, awaiting_approval, creating, building_app, building, shipping, done, needs_you, paused, failed |
appRef | The app's ws_… once the app exists, else null |
author | { id } — who started it |
progress | The progress object |
lastSeq | The stream's head — the seq of the chat's newest frame |
createdAt, updatedAt, archivedAt | ISO timestamps; archivedAt is null unless archived |
Rename, archive, delete
PATCH …/app-chats/{chatId} takes { "title"?, "archived"? } and answers the chat. DELETE …/app-chats/{chatId} deletes the chat and its messages and answers { "id", "deleted": true }; it never deletes the app — apps are archived through /apps.
Messages
POST …/messages with { "message": "Add a due date to each todo" } answers 202 { "messageId" }. While the Work Agent is mid-turn the message queues — it becomes the next turn, or a steer into the build session working on the app. It never fails because a turn is running. POST …/stop interrupts the Work Agent's current turn ({ "stopping": true }, or false when nothing was running); work already pushed stays.
GET …/messages is the chat's history, oldest first. A message:
{
"id": "amsg_…",
"chatId": "ach_…",
"role": "agent",
"content": "Here's the plan.",
"parts": [
{ "type": "text", "text": "Here's the plan." },
{ "type": "card", "cardId": "acd_…" }
],
"status": "complete",
"usage": { "model": "…", "tokens": { "input": 2400, "output": 380, "cacheRead": 0, "cacheWrite": 0, "total": 2780 }, "credits": 0 },
"createdAt": "…",
"completedAt": "…"
}
role is user or agent; status is streaming, complete, stopped or failed. parts is the ordered transcript of the turn — the Work Agent's and the build session's:
type | Fields |
|---|---|
text | text (markdown) |
thinking | text |
file-read | paths[] |
file-edit | operation (create, update, delete, rename), path, toPath? |
bash | command, output? (the last 8 KB), exitCode? |
search | scope (repo, web), query |
tool-call | name, status (ok, error), summary |
step | text, state (running, done, failed) |
card | cardId |
version | sha, summary, taskKey |
milestone | name, state |
Every part may carry startedAt, finishedAt and by: { kind: "agent" | "session", sessionId?, taskKey? }.
usage.credits on a message is always 0: the Work Agent's turns are metered as apps.agent_tokens and debited from the account's credits, and the account's credit ledger (in the console, Billing, labelled orun.work agent) is where the amount shows. With no credits left, a turn opens a notice card titled Out of credits (payload { level: "blocked", text, link, reason: "credits_exhausted" }, link being the account's Billing page in the console), the stream emits RUN_ERROR with code: "credits_exhausted", and no model call is made; Approve, Use, Ship and Restore answer 412 with details.reason: "credits_exhausted".
The stream
GET …/app-chats/{chatId}/events?from={seq} answers text/event-stream. Each frame is one data: line holding an event in the platform's AG-UI dialect (v1), and every frame carries seq, the chat's cursor:
data: {"v":1,"type":"STATE_SNAPSHOT","seq":42,"snapshot":{"chat":{…},"progress":{…}}}
data: {"v":1,"type":"TEXT_MESSAGE_CONTENT","seq":43,"messageId":"amsg_…","delta":"Here's"}
: keepalive
| Event | When | Payload |
|---|---|---|
STATE_SNAPSHOT | First frame of every connection | snapshot: { chat, progress }; its seq is the chat's head |
MESSAGES_SNAPSHOT | A message finished | messages: [message], append: true |
RUN_STARTED / RUN_FINISHED / RUN_ERROR | Around each Work Agent turn, and each turn of a build session | runId, messageId; RUN_ERROR adds code, message |
TEXT_MESSAGE_START / _CONTENT / _END | Prose, as it is written | messageId, delta |
TOOL_CALL_START / _ARGS / _END / _RESULT | An action | toolCallName (a part type for a session's actions, step for the setup's steps, the tool's name for the Work Agent's own); _ARGS' delta and _RESULT's content are one-line summaries |
STATE_DELTA | Progress changed | ops: [{ "op": "replace", "path": "/progress", "value": … }] |
CUSTOM name: "card" | A card opened, was answered or withdrawn | value: the card |
CUSTOM name: "version" | A version was pushed | value: { sha, summary, taskKey, at } |
CUSTOM name: "preview" | The preview changed | value: the preview object |
CUSTOM name: "urls" | A preview or live URL was first seen | value: { preview?, live? } |
CUSTOM name: "resync" | from is older than the stream keeps | value: { oldestSeq } |
A build session's turn is framed by its own RUN_STARTED / RUN_FINISHED with a runId of the form as_…:xxxxxxxx (the session id and a short suffix); the Work Agent's turns have plain UUID run ids.
Resume. Reconnect with from set to the last seq you processed — the snapshot's seq is the head, not a position, so don't use it as a cursor. The stream replays every retained frame after from, then goes live. It keeps the last 2,000 frames.
Resync. When from is older than that, the stream opens with STATE_SNAPSHOT and CUSTOM name=resync, then replays from the oldest frame it still has. Frames were lost: re-read the chat (GET …/app-chats/{chatId}) and its history (GET …/messages), and keep reading the stream.
Keepalive. A : keepalive comment every 25 seconds.
Query-string auth. Clients that cannot set a header may pass the bearer as access_token=… on this route only. Prefer the header.
Progress
GET …/progress — and the progress in the snapshot and every STATE_DELTA — is the chat's folded state:
| Field | Notes |
|---|---|
stage | understanding, planning, setting_up, building, shipping, done |
runs | code_only, deploying, running |
headline | One line: "Building: 2 of 4 — Bookings" |
milestones[] | { name, state, versionCount, pr? }; state is queued, working, reviewing, done, live |
activity[] | { at, text } — the latest progress lines |
preview, live | As on the app |
needsYou[] | { cardId, kind, title } — the cards waiting for an answer |
Cards
Cards are how the Work Agent asks. A client answers a card; it never makes one.
GET …/cards?state=open lists the chat's cards (state is optional: open, answered, expired, withdrawn). The card:
{
"id": "acd_…",
"chatId": "ach_…",
"messageId": "amsg_…",
"kind": "question",
"state": "open",
"title": "A couple of questions",
"payload": { "questions": [ … ] },
"actions": ["answer"],
"answer": null,
"answeredBy": null,
"answeredAt": null,
"createdAt": "…"
}
actions lists what you may post while the card is open (empty once it is closed). Answer with POST …/cards/{cardId} and { "action", "values"? }; the answer is { "card", "messageId" } — the message the answer started, or null when it started none.
| Kind | Payload | Actions and values |
|---|---|---|
question | questions: [{ id, text, multiSelect, options: [{ id, label, description? }] }] (1–3 questions) | answer — { "answers": [{ "questionId", "optionIds": [], "text"? }] }, one entry per question; one option unless multiSelect; text is a free answer |
plan | appName?, startingPoint: { id, name, why }, milestones: [{ name, summary, doneWhen[] }], repository: { choices, default }, autonomy, runsOn: { provider, candidates: [{ connectionId, name, account, shareMode }], default, connectUrl? } | approve — { "repository"?, "runsOn"? }: a repository choice (managed, or account:<login>) and a connectionId or not_yet (each defaults to the card's default). Approving creates the app; picking a connection is the consent to run it there. change — { "text" }: back to planning |
connection | provider, candidates: [{ connectionId, name, account, shareMode }], connectUrl? | use — { "connectionId" } (implied when there is one candidate): consent and go live. not_now |
attach | workspace: { ref, name }, repo, keepAsking | allow — { "keepAsking"? }. decline |
repository | org, reason (app_cannot_create, user_authorization_required), fixUrl? | retry (after fixing it on GitHub), use_managed |
approval | sessionId, taskKey, tool, summary, requestId | approve, deny — the build session's request, decided on your credential |
notice | level (info, warning, blocked), text, link? | dismiss |
handoff | why, rows: [{ id, name, summary, chatId }] — what comes next, offered by a chat whose own work is done; chatId is null until that row is started | start — { "rowId" }: opens that piece of work in a new Build chat on the same app and records its ach_… on the row. The card stays open for its other rows and closes with its last; starting a row again answers the same chat. dismiss closes it |
Answer a question card:
curl -X POST "https://api.orunbase.com/v1/organizations/$ACCOUNT/app-chats/$CHAT/cards/$CARD" \
-H "Authorization: Bearer $ORUN_APPS_KEY" \
-H "Content-Type: application/json" \
-d '{ "action": "answer", "values": { "answers": [
{ "questionId": "q1", "optionIds": ["o2"] },
{ "questionId": "q2", "optionIds": [], "text": "Monthly passes too" } ] } }'
Approve a plan, keeping the code in orun.work's GitHub and running it on a Cloudflare connection:
curl -X POST "https://api.orunbase.com/v1/organizations/$ACCOUNT/app-chats/$CHAT/cards/$CARD" \
-H "Authorization: Bearer $ORUN_APPS_KEY" \
-H "Content-Type: application/json" \
-d '{ "action": "approve", "values": { "repository": "managed", "runsOn": "int_7c1e0b4a9d3e48f0a6b5c4d3e2f1a0b9" } }'
An answered card is final. Answering a closed card — answered, expired or withdrawn — is 409 conflict, details.reason: card_closed; an action the card doesn't take, or values it can't use, is 422 validation_failed.
Apps
The App object
{
"ref": "ws_ABCD1234",
"name": "Yoga bookings",
"origin": "created",
"runs": "running",
"chatId": "ach_…",
"startingPoint": { "id": "tanstack-start", "tag": "baseline-v3" },
"repository": { "fullName": "orunwork-apps/yoga-bookings-k3x9q2", "managed": true, "url": "https://github.com/orunwork-apps/yoga-bookings-k3x9q2" },
"preview": { "source": "env", "env": "stage", "state": "ready", "url": "https://…", "sha": "7c1e0b4", "deployedAt": "…" },
"live": { "env": "prod", "url": "https://…", "sha": "7c1e0b4", "deployedAt": "…" },
"plan": { "milestones": [{ "name": "Bookings", "summary": "…" }] },
"createdAt": "…",
"updatedAt": "…",
"archivedAt": null
}
origin is created (a chat made it) or attached (an existing workspace the Work Agent was allowed into). runs is code_only until a connection is consented, then deploying, then running. plan is the plan its chat approved.
GET …/apps lists the account's apps, newest first; archived apps are left out unless you pass archived=true.
Archive and rename
PATCH …/apps/{appRef} with { "name": "Yoga studio" } (1–60 characters) renames the app and answers it (organization.app.write on the account; emits app.updated). It renames the app only; its workspace keeps its own name and slug.
POST …/apps/{appRef}/archive archives it and answers the app with archivedAt set (organization.app.manage on the app's workspace; emits app.archived once). Archiving stops the Work Agent: a live session is ended, open cards are withdrawn, Ship and Restore answer 409 with details.reason: "app_archived", a new message gets a note rather than a model turn, and the app's Work Agent principal is revoked. It does not delete the workspace, the repository or anything deployed. Archiving an archived app answers the same archivedAt; if the principal could not be revoked yet, it answers 503 with details.reason: "principal_revoke_failed" — the archive stands, and archiving again finishes it.
Preview
GET …/apps/{appRef}/preview is the app's stage environment:
{ "source": "env", "env": "stage", "state": "ready", "url": "https://…", "sha": "7c1e0b4", "deployedAt": "…" }
state is none — code only: nothing runs yet, because no connection has been consented — deploying (keeps the last good url and sha while a newer version deploys) or ready. sha names the version on stage. Render whatever url you are given; a later release may add other sources.
Versions, restore, ship
GET …/apps/{appRef}/versions lists the pushes to the app's task branches, newest first: { sha, summary, taskKey, at, shipped, restoredFrom }. shipped turns true once the task's pull request merged; restoredFrom names the version a restore put back.
POST …/versions/{sha}/restoreasks the Work Agent to put that version's tree back as a new version —202 { "messageId" }.POST …/ship— Ship what you have — asks the Work Agent to land the current task's work now:202 { "messageId" }. With no unshipped version it is412 precondition_failed,details.reason: nothing_to_ship. On a running app the preview refreshes once the stage deploy that follows succeeds.
Connections
GET …/apps/{appRef}/connections answers where the app can run and what it may use:
{
"providers": [
{ "provider": "cloudflare", "candidates": [{ "connectionId": "int_…", "name": "acme-prod", "account": "acme", "shareMode": "auto" }] }
],
"consents": [
{ "provider": "cloudflare", "connectionId": "int_…", "state": "granted", "consentedBy": "usr_…", "consentedAt": "…", "revokedBy": null, "revokedAt": null }
]
}
A provider with no candidate carries connectUrl, the console page to connect one. DELETE …/connections/{connectionId} revokes a consent and answers it (state: "revoked"). It undeploys nothing; the next deploy needs a new consent, offered with a connection card.
Errors
Errors use the platform envelope (Errors). The apps plane names its own reasons in details.reason:
| Reason | Status | Meaning |
|---|---|---|
card_closed | 409 | The card was already answered, expired or withdrawn |
app_has_chat | 409 | appRef names a workspace that is already an app |
chat_has_app | 409 | appRef given to a chat that already has a different app |
kind_not_allowed | 422 | POST …/app-chats was asked for a kind it does not start (setup) |
nothing_to_ship | 412 | There is no unshipped version |
credits_exhausted | 412 | The account is out of credits |
consent_required | 412 | Going live needs a connection nobody has allowed |
app_limit | 412 | The account's workspace cap would be exceeded |
managed_repo_limit | 412 | The account's managed-repository cap is reached |
not_available_yet | 412 | The plan's starting point can't make an app yet |
grant_not_permitted | 403 | You can't grant that connection to the app — ask someone with integration.manage |
action_not_allowed | 422 | The card doesn't take that action, or the values don't fit it |
Some refusals happen during a turn rather than on a request; they arrive on the stream as RUN_ERROR with the reason as code (consent_required, for instance), or as a notice card (out of credits, a limit reached).
Webhooks
Subscribe to app.* (or a single type) with the webhooks API. Deliveries carry the platform body:
{
"id": "5f0c9c2e-8a51-4d5f-b0a9-1f6d2f6f9a11",
"type": "app.live",
"occurred_at": "2026-10-31T09:41:07.000Z",
"data": {
"chatId": "ach_…",
"appRef": "ws_ABCD1234",
"env": "prod",
"url": "https://yoga-bookings.example.workers.dev",
"sha": "7c1e0b4",
"urls": { "preview": "https://yoga-bookings-stage.example.workers.dev", "live": "https://yoga-bookings.example.workers.dev" }
}
}
| Type | When | data beyond chatId and appRef |
|---|---|---|
app.chat.created | A chat started | — |
app.chat.updated | A chat was renamed or archived | the change: title?, archived? |
app.chat.deleted | A chat was deleted | — |
app.message.finished | The Work Agent finished a reply | messageId, status, usage (and tokens, its total) |
app.card.answered | A card was answered | cardId, kind, action |
app.created | The app exists | name, runs, startingPoint, repository, agentPrincipal, runsOn? |
app.autonomy.set | The app's workspace may continue without asking | level |
app.attached | The Work Agent may work in an existing workspace | name, repository, agentPrincipal, keepAsking |
app.connection.consented | Someone allowed the app to run on a connection | provider, connectionId, connectionName, shareMode, grantCreated, actor |
app.connection.revoked | A consent was revoked | provider, connectionId, actor |
app.preview.ready | A stage deploy was observed | env, url, sha, source |
app.live | A prod deploy was observed | env, url, sha, urls: { preview?, live } |
app.version.created | A version was pushed | sha, shortSha, taskKey, summary |
app.version.restored | A restore put an older version back | sha, taskKey, restoredFrom, restoredFromShort |
app.shipped | A milestone's (or change's) pull request merged | taskKey, kind, name, pr? |
app.updated | The app was renamed | name, previousName |
app.archived | The app was archived | name, archivedAt |
Every app.* event is delivered to the account's endpoints — subscribe on the account, before any app exists. data.chatId is null for an app with no chat (an attached workspace), and data.appRef is null until the chat has an app. id is stable across retries — deduplicate on it. Verify every delivery's signature (Verify deliveries).
SDK
@saas/sdk has the whole surface on client.apps. Every method takes the account first.
import { OrunCloud, isAppStreamCard, isAppStreamResync } from "@saas/sdk";
const client = new OrunCloud({
baseUrl: "https://api.orunbase.com",
auth: { kind: "bearer", token: process.env["ORUN_APPS_KEY"]! },
});
const account = process.env["ORUN_ACCOUNT"]!;
const { chat } = await client.apps.chats.create(account, { message: "A todo app for my team" });
for await (const event of client.apps.chats.events(account, chat.id)) {
if (event.type === "TEXT_MESSAGE_CONTENT") process.stdout.write(event.delta ?? "");
if (isAppStreamResync(event)) {
// Frames were lost: re-read what you show, then keep reading.
const history = await client.apps.chats.messages.list(account, chat.id);
}
if (isAppStreamCard(event) && event.value.state === "open") {
// decide, then: client.apps.chats.cards.answer(account, chat.id, event.value.id, { action, values })
}
}
| Call | Route |
|---|---|
apps.chats.create(account, { message, appRef?, title? }) | POST …/app-chats |
apps.chats.list(account, { limit?, cursor? }), apps.chats.iter(…) | GET …/app-chats |
apps.chats.get / update / delete(account, chatId, …) | GET / PATCH / DELETE …/app-chats/{chatId} |
apps.chats.send(account, chatId, message), stop, progress | …/messages, …/stop, …/progress |
apps.chats.events(account, chatId, { from?, signal?, reconnect? }) | GET …/events as an async iterator |
apps.chats.messages.list / iter / get | …/messages |
apps.chats.cards.list(account, chatId, { state? }), apps.chats.cards.answer(account, chatId, cardId, { action, values }) (also apps.cards) | …/cards |
apps.list(account, { archived?, limit?, cursor? }), apps.iter(…), apps.get, apps.update(account, appRef, { name }), apps.archive | …/apps, …/apps/{appRef}, …/archive |
apps.preview, apps.versions, apps.restore(account, appRef, sha), apps.ship | …/preview, …/versions, …/restore, …/ship |
apps.connections, apps.revokeConnection(account, appRef, connectionId) | …/connections |
Lists answer { items, cursor }. The events iterator reconnects on its own when the connection drops, resuming from the last seq it yielded (backing off between failed connects; a 4xx other than 408/429 is final and throws). It yields CUSTOM name=resync like any other frame — isAppStreamResync(event) tells you to re-read. Abort the signal or break to stop. Errors are the SDK's typed errors; the apps plane's reason is error.reason ("card_closed", "nothing_to_ship", …).
CLI
orun-cloud apps new "a todo app for my team" # chat with the Work Agent here, answering its cards
orun-cloud apps list [--archived] [--all]
orun-cloud apps open <ws_…|ach_…> # the chat in orun.work
orun-cloud apps preview <ws_…|ach_…> # the preview URL, or "code only"
See Build an app and the CLI reference.