Skip to navigation

Runtime API

The Runtime REST API is generated from a published DomainConfig and includes app-scoped platform endpoints for user-visible channel interaction. The generated OpenAPI contract for the target app is canonical.

OpenAPI

Fetch the contract:

GET /api/rest/<tenant-id>/<app-id>/openapi

The Production OpenAPI document is public metadata. Add ?workspaceId=<workspace-id> when the contract should describe a Workspace runtime source instead; because that response can expose unpublished model metadata, the Workspace request requires normal app-scoped Runtime bearer authentication. Use the response to discover endpoint names, schemas, auth requirements, response shapes, and errors. Do not infer app-specific endpoint names from another app or mix Production and Workspace contracts.

Workspace OpenAPI and agent discovery can describe agents that exist only in the selected Workspace. The channel runtime still resolves a new DM target from the published app configuration, so a Workspace-only agent cannot receive a DM until that change is promoted.

Commands

Published commands become REST endpoints. A command request:

  1. authenticates the caller
  2. checks permissions
  3. validates input
  4. loads decision state
  5. emits events if the command succeeds
  6. returns a structured result

Keep cross-command orchestration, lookup-before-create, and cross-app coordination outside the command request itself. For delivery safety, send an optional Idempotency-Key header containing a client-owned UUID. One key names one exact command intent:

  • Repeating the same command and body with the same key returns the canonical committed receipt with idempotentReplay: true; it does not dispatch the handler or append the command events again.
  • Reusing a key with different input returns 409 COMMAND_IDEMPOTENCY_CONFLICT and effectState: rejected.
  • A typed 503 COMMAND_UNAVAILABLE with effectState: rolled_back confirms that the request did not append. Start a new intent only if the operator still wants the action.
  • A lost connection or unclassified 5xx has an unknown outcome. Retry the identical request with the same key until the canonical receipt or an authoritative terminal result is returned. Do not switch to a new key while the original outcome is unknown.

The executionId in the response equals the supplied intent UUID and, together with correlationId, locates the Production or Workspace execution trace. Idempotency covers transport retries for one intent; business-level dedupe across distinct intent keys still belongs in the domain or caller workflow.

New receipt-bound commands retain their original scope and decision-boundary evidence with the committed events. Recovery after a lost commit acknowledgement returns that recorded decision without running the handler again. Historical receipts without this context remain readable; Cessy does not invent missing decision evidence. Conflicting or malformed recorded context fails closed.

A successful command proves the canonical command execution boundary and the append of any events reported by that execution. It does not prove that every downstream projection has applied those events. Diagnose projection delivery separately; a missing projection row is not proof that the command never ran, and retrying with a new intent can create a second business operation. Reuse the same idempotency key when the original outcome is unknown. For events covered by durable runtime intake, Cessy automatically retries a missing projection phase without replaying the command or appending another source event.

Policy fan-out

A custom_code policy may return at most 100 ordinary inline commands. A larger ordinary result is rejected before any child command write begins. Use a homogeneous durable batch for more than 100 similar commands, or model explicit durable stages when command types are mixed or the workflow needs a final completion barrier.

The 10-100 range is an admission ceiling, not an automatic recommendation. Use inline fan-out in that range only when the complete downstream cascade, including triggered policies and projection callbacks, is bounded and measured below 60 seconds at p95 and below the 90-second hard segment regression budget. Prefer __batch even for 100 items or fewer when homogeneous high-volume work needs operational isolation, independent retries, rate limiting, progress, or dead-letter handling. For mixed work or final synchronization, model explicit durable stages now; a reusable durable completion barrier is tracked for future work in #3005.

Inline fan-out also bounds durable identity by UTF-8 bytes: 1,000 bytes for a command type and 2,000 bytes each for a resolved scope key or child ID. This validation covers explicit, command-data, boundary-derived, and trigger-default scopes before child dispatch. Oversized untrusted identities are represented by bounded hash-suffixed failure evidence; derived parent prefixes use deterministic hashes.

Prepared children with the same resolved decision scope execute sequentially in declaration order. Independent scope groups use a sliding window, so each free lane starts the next ready scope instead of waiting for a fixed chunk to complete.

For admitted inline fan-out, runtime persists a complete child evidence census before the first write and reserves handler time for terminalization. The census contains stable child identity, index, command type, resolved scope, and status; it does not contain command payloads or replay intent. Terminal evidence accounts for every child as succeeded, failed, unknown, or not_started. unknown means the child was issued but its append outcome was not confirmed; reconcile it against stable business or canonical event evidence before retrying. When the dispatch deadline is reached, runtime records the exhaustive policy failure and, when onError is configured, a leased compensation owner before invoking that command. The failure becomes terminal only after compensation is durably completed or explicitly terminated by an audited abort. Operator aborts use OPERATOR_ABORTED only before dispatch starts; terminating a started attempt retains DISPATCH_OUTCOME_UNKNOWN. Recovery may use WORKSPACE_EXPIRED or WORKSPACE_UNAVAILABLE when the exact Workspace source definitively no longer exists. An abort records actor, reason, and timestamp without claiming that the compensation command ran. This is an evidence contract, not a replayable child outbox.

If a configured onError command cannot be mapped, normalized, or encrypted, runtime has not created a compensation owner and does not fabricate one merely to mark it aborted. It first persists a queryable ErrorLog diagnostic with code POLICY_FANOUT_COMPENSATION_PREPARATION_FAILED under a deterministic identity; only after that write is confirmed may it terminalize the original fan-out without compensation, retaining the fan-out’s primary error code. Diagnostic failure leaves the lifecycle running (or missing before a manifest exists) and fails closed. The diagnostic contains controlled references and a phase only, never mapping expressions, command/event data, receipt errors, crypto messages, or credentials.

For criteria-based child commands, the persisted fan-out cutoff is also the absolute PostgreSQL statement cutoff. The runtime overrides a shorter pooled session default for that transaction, then uses the reserved terminal window to finish rollback and an indexed raw-tag probe while retaining half of that window for durable terminal lifecycle persistence. Exact canonical rows produce a successful receipt and are published with their stored identities; a confirmed rollback with canonical absence produces a failed receipt; unresolved, unreadable, partial, or mismatched evidence remains unknown. Reconciled receipts surface only operation class, elapsed time, and reconciliation state, never SQL, criteria, tags, command data, or event payloads. Production and Workspace probes remain isolated by their effective app IDs.

Inline fan-out requires a stable trigger event ID. Redelivery before the persisted deadline reports the execution as already in progress without reissuing children or running onError. At or after the deadline, runtime can atomically record failure on the same execution without replaying children. Because a running census is not a per-child checkpoint, interruption recovery marks every child unknown; reconcile canonical evidence before a business retry. onError is a separate durable compensation phase: redelivery waits for its live lease or claims an expired lease, then checks indexed canonical child evidence and verifies the encrypted intent’s exact materialized-runtime fingerprint before dispatch. The fingerprint covers full config, including subscriptions and downstream policies, plus a versioned executor contract. An append already present after an ambiguous failure is recorded as reconciled without fabricated execution or event-count evidence.

Before mutable worker execution, the canonical lifecycle pre-read uses the same deterministic execution ID through a bounded transient-connection retry sequence. Only a successful read with no lifecycle proves that the execution has not started. Retry exhaustion, cancellation, deadline expiry, or an unavailable canonical probe fails closed before worker code or business onError instead of risking duplicate work. Exhaustion does not schedule an automatic retry job; explicit/source redelivery of the durable event reuses the same deterministic lifecycle ID and canonical fence.

One process-wide authority admits at most ten covered runtime operations and at most seven ordinary command executions. One slot remains available for each missing protected class: lifecycle, projection callback, and durable diagnostic evidence. Owner-fair queues and bounded control priority allow control work to progress without starving commands. These are process-local admission ceilings, not cross-replica or physical database-pool reservations.

Every ordinary command entry point uses this authority, including public API, batch, onError, and compensation execution. Each physical attempt owns its command slot through handler dispatch, event persistence, and synchronous projection publication, then releases it before retry backoff or synchronous or asynchronous post-policy handlers dispatch nested commands. Inline fan-out uses the same executor-owned admission for every physical attempt; caller-authored fan-out identity metadata cannot bypass it. If a caller’s deadline expires after work starts, the caller returns promptly but the slot remains occupied until that physical attempt and synchronous projection work settle.

Async policy queue wait is measured separately and counts toward the supported end-to-end segment budget. The handler execution deadline starts only after admission; expiring a queued subscription after its source event was persisted would silently skip that delivery. Once admitted, worker execution, projection callbacks, command admission and execution, and terminal persistence share the handler deadline. Projection callbacks are capped at 30 seconds and below the remaining handler budget, with cooperative cancellation and a terminal guard. Structured telemetry separates handler queue wait, central command admission (runtime.command_admission.wait_ms), fan-out command-permit wait, lifecycle phase/retry timing, diagnostic persistence, and projection-callback timing. Protected lifecycle diagnostics include pool-acquire/query durations, the configured deadline, a before/during-query termination phase, and one correlated recovery outcome after a successful retry. Deterministic lifecycle IDs appear in that protected diagnostic, never in aggregate metric attributes.

PolicyExecutionLogged version 1 uses the optional receipt properties operationClass, elapsedMs, and reconciliationState for append diagnostics. Historical version-1 receipts without those properties remain valid. The first Production release that both reads and persists the expanded shape is commit 13ce8501d6c98a17491220d22cc207a88f53606b. That exact release is the permanent rollback floor for expanded receipts: never deploy or roll back a reader below it, because earlier strict readers cannot replay expanded version-1 events.

Recovery-loop tracking is independent of ordinary __cascadeDepth, so a deep valid saga can still invoke its first configured onError. That command and its downstream events carry a stable __onErrorLineage and __onErrorDepth: 1 (plus __isOnError for compatibility). Runtime currently allows one recovery boundary. A later failure in the same lineage does not launch another onError; durable fan-out instead records terminal fan-out errorCode: ON_ERROR_SUPPRESSED evidence. Before that terminal is committed, ErrorLog durably records reason RECOVERY_DEPTH_LIMIT_REACHED, the target command, lineage, and observed/maximum recovery depths under a stable diagnostic identity. Execution diagnostics expose the terminal marker separately from compensation.

The complete immutable onError intent is persisted in a bounded, versioned AES-256-GCM envelope. The global cron tick durably rotates through a bounded window of authoritative Production and Workspace runtime scopes, then scans the canonical latest pending lifecycle under each effective app ID. Runtime loading starts only after an authoritative candidate page contains due compensation; empty or incomplete pages do not require a published model. Failures loading a runtime for due work remain visible and leave the compensation pending. It re-reads the stream and claims expired leases without waiting for the original worker, and it never replays fan-out children or substitutes Production config for a Workspace.

If exact Workspace materialization reports typed not-found, an expired or missing unstarted compensation is terminalized with WORKSPACE_EXPIRED or WORKSPACE_UNAVAILABLE. A started attempt is never dispatched again and uses DISPATCH_OUTCOME_UNKNOWN. Transient storage failures, invalid Workspace config, and missing or incorrect CREDENTIAL_ENCRYPTION_KEY material fail closed and leave compensation pending. Drain or explicitly resolve pending compensations before rotating that key; pending intent does not currently support a multi-key or key-ID fallback.

Batch commands

Batch command execution is useful for controlled automation. A batch declaration is homogeneous: it contains one commandType and many item payloads. It does not encode a mixed command sequence or guarantee that a final item acts as a completion barrier. Segment those workflows explicitly and advance the modeled stage only after the preceding batch reaches a terminal outcome whose progress and dead-letter evidence satisfies the business completion rule. Use this operational isolation for homogeneous high-volume work even when it fits below the 100-command inline ceiling. Continue using explicit staged completion for mixed/final synchronization; the reusable durable barrier is future work tracked in #3005.

Keep batches bounded and give target commands stable business identities when retries must be deduplicated. Batch tracking does not itself make commands idempotent, promise that the triggering event plus all job/item records share one atomic transaction, or make successive progress reads an isolated snapshot. Each processing claim is ownership-fenced. Crash recovery retries only claims that provably never began execution; a started claim without durable outcome proof becomes an explicit dead letter and must be reconciled before deliberate operator replay. A thrown/ambiguous command result is never retried blindly. Before a replacement processor publishes a running lease, it takes a durable yielding lease and settles every stale claim from the expired owner. This same admission boundary applies to an operator resume, so resumed work cannot advance alongside claims left behind by its previous processor.

Native batch item diagnostics use five payload-free effect states: not_started, rejected, rolled_back, committed, and outcome_unknown. Each new attempt carries a deterministic execution ID and canonical request hash that activates EventStore idempotency. After a lost response, only a complete canonical receipt matching the execution ID, request hash, and command type can settle success. An absent or unavailable receipt remains unknown, including a zero-event command’s lost response. Confirmed rollback from stream, tag, and criteria appends is retryable.

get_batch_job and list_batch_jobs expose stored cancelled jobs as legacy_cancelled_unsettled. This means cancellation control is recorded but there is no immutable terminal census. Pending items and unknown attempts still require reconciliation; the cancelled flag is not no-effect evidence.

The Design MCP list_batch_job_items tool pages stable item IDs/indexes, technical identity hashes, attempt/category/error fields, execution and decision receipts, reconciliation references, and replay eligibility without returning command data, scope values, raw errors, or business event data. Inspection is fenced to the tenant, effective app, and selected Production or Workspace source.

For an outcome_unknown dead letter, reconcile the execution reference against canonical command/event/projection evidence. Record the conclusion with record_batch_item_effect_verification, echoing the item hash, replay generation, attempt, and command execution ID when present: effect_absent makes that exact attempt eligible for replay, while effect_committed permanently blocks it. Then call replay_dead_letter_items with exact item IDs and technical identity hashes. Bulk replay preflights the complete selection and makes no changes if any item is ambiguous or committed.

For a trusted custom_code policy that always returns exactly one homogeneous batch, set typeConfig.durableRecovery to native_batch_v1 and declare a finite, non-empty targetCommands set. This opt-in binds the completed batch manifest to the durable source-policy work ID and registers its Production or Workspace runtime in a dedicated recovery catalog. In the isolated production topology, a minutely deduplicated runtime-worker producer assigns the manifest to a long-lived, lease-fenced recovery worker. That owner is independent of Cron lane deadlines and can honor the full per-item attempt budget; inline-only deployments retain a fair alternating Cron fallback. The custom handler remains an opaque, at-most-once boundary: uncertainty before a complete manifest requires reconciliation, while post-manifest recovery uses the Batch claim fences described above. Configured onError remains supported.

Projections

Projection endpoints read current state. They support filtering, sorting, pagination, and output shaping according to the projection contract.

Projection delivery for durable runtime events is recoverable per source occurrence. Each projection handler atomically commits its document changes and an exact settlement receipt. If a transient PostgreSQL failure interrupts a phase, the matching runtime revision reloads the canonical event, skips handlers that already settled, and retries only the missing projection handlers. It does not rerun the command or publish post-event policies. Production and each Workspace use separate recovery and receipt scopes.

Recovery is bounded and fail-closed. Deterministic handler failures, exhausted retries, an unavailable source row, or an ended Workspace become poisoned operational state for diagnosis; Cessy does not infer projection success from elapsed time. A read immediately after command success can therefore still lag while recovery is pending.

Policy projection callbacks read the exact active Production or Workspace source and never substitute Production for a Workspace. Their bounded, value-free diagnostics record both successful and failed callback outcome, phase, timing, remaining budget, response reach/status, and query shape. Callbacks also carry the worker’s exact runtime-config revision. Invalid app ownership, unknown projection, stale worker snapshot, and transient app/runtime registry failures therefore produce distinct retry and reconciliation guidance.

See Projection Queries.

Agent discovery

Runtime clients can discover the safe agent catalog before opening a chat:

GET /api/rest/<tenant-id>/<app-id>/agents
Authorization: Bearer <runtime-api-key-or-oauth-token>

The response contains only stable public metadata:

{
"agents": [
{
"id": "agent-support",
"name": "support",
"label": "Support",
"kind": "standard"
}
]
}

Prompts, models, MCP configuration, credentials, container settings, provider secrets, and private scope configuration are not part of this response. Use the returned id as agentId in POST /channels/dm only when that agent is already published. A Workspace-selected catalog can also include preview-only agents; promote those agents before opening a DM. The catalog uses normal Runtime REST authentication; the channel call itself has the stricter user-principal requirement described below.

Channels

Runtime REST exposes channel endpoints for user-delegated chat experiences, including partner BFFs and custom frontends. Every channel and user-channel-view operation requires an OAuth/JWT bearer token that resolves to the authenticated human user. API keys and agent service tokens are rejected. The endpoints apply app access checks and only expose channels tied to agents from the target app.

Common channel calls:

POST /api/rest/<tenant-id>/<app-id>/channels/dm
GET /api/rest/<tenant-id>/<app-id>/channels
GET /api/rest/<tenant-id>/<app-id>/channels/<channel-id>
GET /api/rest/<tenant-id>/<app-id>/channels/<channel-id>/messages
POST /api/rest/<tenant-id>/<app-id>/channels/<channel-id>/messages
POST /api/rest/<tenant-id>/<app-id>/channels/<channel-id>/close
POST /api/rest/<tenant-id>/<app-id>/channels/<channel-id>/read
GET /api/rest/<tenant-id>/<app-id>/user-channel-views

Every successful POST /channels/dm creates a fresh human-to-agent conversation channel and linked session and returns 201 with created: true. Repeating the call creates another conversation; it does not deduplicate by user and agent. The supplied agentId must exist in the published app configuration even if the request selects a Workspace runtime source. To continue a prior conversation, retain its returned channel id or select it from the user’s channel views and post through that channel’s messages endpoint. Use /channels/{channelId}/read and /user-channel-views for inbox state.

Channel endpoints are not a replacement for modeled commands. Business mutations should still execute commands; channel messages should explain or coordinate the user-visible work.

Private turn context

POST /channels/dm and POST /channels/{channelId}/messages accept an optional agentTurnContext object. Use it when the visible user message should remain natural, but the agent needs private orientation for this turn, such as the current product surface, focused document id, draft/published status, projection pointers, write/proposal constraints, or small namespaced extensions for builder-specific hints. agentSessionContext is accepted as a compatibility alias; send only one field.

The context is not returned by channel message APIs and is not copied into the public channel log. It is injected privately for the associated agent wake and monitoring evaluation, with scope: "next_message_only". For POST /channels/dm, provide it only when the request also posts content; createOnly DM opens do not have an associated turn.

permissions inside the context are orientation only. Runtime authorization, delegation, domain commands, and explicit user action requirements still control reads and writes.

Event import

Event import is for migrations, repairs, and controlled operational workflows. Normal user behavior should execute commands. Ordinary cascade depth (__cascadeDepth), recovery markers (__isOnError, __onErrorDepth, __onErrorLineage), and the internal compatibility marker __legacyRecoveryOriginUnknown are platform-owned metadata and are rejected at the public import boundary.

Projection rebuild

Projection rebuild reprocesses event history through a replacement read model for projection definition changes, migrations, and deliberate bulk repair. Transient delivery gaps for unchanged durable projections use automatic occurrence recovery instead. Verify output after a rebuild and communicate production impact before running it on large event histories.

Adapter call route

The adapter call route exists for controlled platform workflows. Treat it as platform-managed support, not as the normal public app API. External consumers should use commands, projections, webhooks, subscriptions, or a BFF.