Skip to main content

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 with POST /v1/organizations/{account}/api-keys and { "label", "role", "scope": "apps" } on the account itself (a workspace answers 412 apps_key_needs_account). The secret, sk_ and 64 hex characters, is shown once. Its role (owner, admin, builder or viewer) is what it may do on the account; a script that approves plans — which creates the app's workspace — needs owner. It reaches the apps routes, GET /v1/accounts and GET /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 for scope=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:

RoutePermissionChecked on
Readsorganization.app.readthe account
Chat writes (create, send, stop, answer a card)organization.app.writethe account
Approving a plan (creates the app)creating a workspace — the app's workspace is made with the approver's credentialthe account (owner, account_owner, account_admin)
Ship, restoreorganization.app.writethe account
Revoke a connection, archiveorganization.app.managethe app's workspace

A chat is visible to the person (or key) that started it. Someone else asking for it gets 404.

Endpoints​

MethodPathDescription
POST/v1/organizations/{account}/app-chatsStart a chat (202)
GET/v1/organizations/{account}/app-chatsYour 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}/messagesSend a message (202)
GET/v1/organizations/{account}/app-chats/{chatId}/messagesThe chat's history, oldest first
GET/v1/organizations/{account}/app-chats/{chatId}/messages/{messageId}One message
POST/v1/organizations/{account}/app-chats/{chatId}/stopInterrupt 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}/progressThe progress object
GET/v1/organizations/{account}/app-chats/{chatId}/cards?state=openThe chat's cards
POST/v1/organizations/{account}/app-chats/{chatId}/cards/{cardId}Answer a card
GET/v1/organizations/{account}/apps?archived=trueThe 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}/archiveArchive an app
GET/v1/organizations/{account}/apps/{appRef}/previewThe app's preview
GET/v1/organizations/{account}/apps/{appRef}/versionsIts versions, newest first
POST/v1/organizations/{account}/apps/{appRef}/versions/{sha}/restoreRestore a version (202)
POST/v1/organizations/{account}/apps/{appRef}/shipShip what you have (202)
GET/v1/organizations/{account}/apps/{appRef}/connectionsWhere it can run, and its consents
DELETE/v1/organizations/{account}/apps/{appRef}/connections/{connectionId}Revoke a consent
Not yet available

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.

kindWhat the chat doesWhen an approved plan is built
chatDoes what it is asked: answers, plans, or gets a change madeWhen you ask for it
planPlans the work and writes the plan downWhen you ask for it
buildPlans the work and builds itAs soon as you approve it
movePlans moving something you already have onto the appWhen 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​

FieldNotes
idach_…
title
kindWhat 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
parentChatIdThe ach_… of the chat whose card started this one, else null
planChatIdThe 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
stateintake, planning, awaiting_approval, creating, building_app, building, shipping, done, needs_you, paused, failed
appRefThe app's ws_… once the app exists, else null
author{ id } — who started it
progressThe progress object
lastSeqThe stream's head — the seq of the chat's newest frame
createdAt, updatedAt, archivedAtISO 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:

typeFields
texttext (markdown)
thinkingtext
file-readpaths[]
file-editoperation (create, update, delete, rename), path, toPath?
bashcommand, output? (the last 8 KB), exitCode?
searchscope (repo, web), query
tool-callname, status (ok, error), summary
steptext, state (running, done, failed)
cardcardId
versionsha, summary, taskKey
milestonename, 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
EventWhenPayload
STATE_SNAPSHOTFirst frame of every connectionsnapshot: { chat, progress }; its seq is the chat's head
MESSAGES_SNAPSHOTA message finishedmessages: [message], append: true
RUN_STARTED / RUN_FINISHED / RUN_ERRORAround each Work Agent turn, and each turn of a build sessionrunId, messageId; RUN_ERROR adds code, message
TEXT_MESSAGE_START / _CONTENT / _ENDProse, as it is writtenmessageId, delta
TOOL_CALL_START / _ARGS / _END / _RESULTAn actiontoolCallName (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_DELTAProgress changedops: [{ "op": "replace", "path": "/progress", "value": … }]
CUSTOM name: "card"A card opened, was answered or withdrawnvalue: the card
CUSTOM name: "version"A version was pushedvalue: { sha, summary, taskKey, at }
CUSTOM name: "preview"The preview changedvalue: the preview object
CUSTOM name: "urls"A preview or live URL was first seenvalue: { preview?, live? }
CUSTOM name: "resync"from is older than the stream keepsvalue: { 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:

FieldNotes
stageunderstanding, planning, setting_up, building, shipping, done
runscode_only, deploying, running
headlineOne 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, liveAs 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.

KindPayloadActions and values
questionquestions: [{ 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
planappName?, 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
connectionprovider, candidates: [{ connectionId, name, account, shareMode }], connectUrl?use — { "connectionId" } (implied when there is one candidate): consent and go live. not_now
attachworkspace: { ref, name }, repo, keepAskingallow — { "keepAsking"? }. decline
repositoryorg, reason (app_cannot_create, user_authorization_required), fixUrl?retry (after fixing it on GitHub), use_managed
approvalsessionId, taskKey, tool, summary, requestIdapprove, deny — the build session's request, decided on your credential
noticelevel (info, warning, blocked), text, link?dismiss
handoffwhy, rows: [{ id, name, summary, chatId }] — what comes next, offered by a chat whose own work is done; chatId is null until that row is startedstart — { "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}/restore asks 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 is 412 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:

ReasonStatusMeaning
card_closed409The card was already answered, expired or withdrawn
app_has_chat409appRef names a workspace that is already an app
chat_has_app409appRef given to a chat that already has a different app
kind_not_allowed422POST …/app-chats was asked for a kind it does not start (setup)
nothing_to_ship412There is no unshipped version
credits_exhausted412The account is out of credits
consent_required412Going live needs a connection nobody has allowed
app_limit412The account's workspace cap would be exceeded
managed_repo_limit412The account's managed-repository cap is reached
not_available_yet412The plan's starting point can't make an app yet
grant_not_permitted403You can't grant that connection to the app — ask someone with integration.manage
action_not_allowed422The 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" }
}
}
TypeWhendata beyond chatId and appRef
app.chat.createdA chat started—
app.chat.updatedA chat was renamed or archivedthe change: title?, archived?
app.chat.deletedA chat was deleted—
app.message.finishedThe Work Agent finished a replymessageId, status, usage (and tokens, its total)
app.card.answeredA card was answeredcardId, kind, action
app.createdThe app existsname, runs, startingPoint, repository, agentPrincipal, runsOn?
app.autonomy.setThe app's workspace may continue without askinglevel
app.attachedThe Work Agent may work in an existing workspacename, repository, agentPrincipal, keepAsking
app.connection.consentedSomeone allowed the app to run on a connectionprovider, connectionId, connectionName, shareMode, grantCreated, actor
app.connection.revokedA consent was revokedprovider, connectionId, actor
app.preview.readyA stage deploy was observedenv, url, sha, source
app.liveA prod deploy was observedenv, url, sha, urls: { preview?, live }
app.version.createdA version was pushedsha, shortSha, taskKey, summary
app.version.restoredA restore put an older version backsha, taskKey, restoredFrom, restoredFromShort
app.shippedA milestone's (or change's) pull request mergedtaskKey, kind, name, pr?
app.updatedThe app was renamedname, previousName
app.archivedThe app was archivedname, 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 })
}
}
CallRoute
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.