API Changelog
The bossanova.v1 OrchestratorService (see the API Reference) uses Stripe-style, date-based versioning. Each dated version pins a stable response shape: when the server starts serving a new value or behavior, it also registers a down-convert transform so callers pinned to an older version keep receiving the shape they were built against.
The Bossanova-Version header
Clients select an API version by sending a Bossanova-Version request header
containing a YYYY-MM-DD identifier (for example Bossanova-Version: 2026-07-11).
The resolved version is echoed back on the response.
- A request that negotiates the Current version runs zero transforms.
- A request pinned to an older version has the ordered chain of down-convert transforms applied so it observes the behavior that shipped with its version.
- A header-less caller pins to the Baseline (the oldest supported version), so it never silently shifts onto newer behavior. This matches the Stripe intent of "pin to the version you started on."
The current version is 2026-09-14. The engineering procedure for adding a new
version and its transform lives in the repository at docs/api-versioning.md
(view on GitHub).
Versions
Listed oldest to newest. Every version below is a member of the production registry served by the OrchestratorService.
2026-06-29 — Baseline
The launch baseline and the oldest supported version. It is the version assumed for header-less callers. No transforms run against a Baseline-shaped response beyond those introduced by later versions.
2026-07-04 — Orphaned session state
The OrchestratorService began serving SessionState value
SESSION_STATE_ORPHANED on Session.state, a terminal state for a headless run
that was killed by a daemon restart. Clients pinned to an older version, which
were built before this value existed, are down-converted to
SESSION_STATE_IMPLEMENTING_PLAN, the prior observable behavior.
2026-07-05 — Agent auth-failed attention reason
The OrchestratorService began serving AttentionReason value
ATTENTION_REASON_AGENT_AUTH_FAILED on Session.attention_status.reason, a new
attention reason surfaced when an agent's pane shows the login-required terminal
shape (for example "Not logged in" / "Please run /login"). Older clients, which
never saw this reason, are down-converted back to no attention reason.
2026-07-06 — Unmanaged account label and limited chat status
Two transforms shipped in this version:
- Unmanaged account label — restores the prior "System default" account label for older clients where the server now serves "Unmanaged local credentials" for the unbound rotation account.
- Limited chat status — maps
CHAT_STATUS_LIMITEDand the derived "usage-limited…" session display shape back to the prior idle-style behavior for older clients.
2026-07-11 — No-eligible-account rotation outcome
The OrchestratorService began serving RotationOutcome value
ROTATION_OUTCOME_STATUS_ONLY_NO_ELIGIBLE_ACCOUNT on
Session.rotation_events[].outcome, distinguishing "no active account to rotate
to" from the prior "agent cannot rotate". Clients pinned to an older version are
down-converted back to ROTATION_OUTCOME_STATUS_ONLY_NO_CAPABILITY, the prior
observable value.
2026-07-18 — Errored session display recolor
The OrchestratorService began serving the errored-recolor display shape for
orphaned/blocked sessions on Session.display_label / display_intent /
display_spinner. An errored session now keeps its real underlying status label
and spinner (a live "working" spinner, a pending "? question") but has its intent
recolored to DISPLAY_INTENT_DANGER so the error stays visible; a legitimate
terminal muted PR ("✓ merged" / "closed") is exempt and stays muted. Clients
pinned to an older version were built against the prior shapes (a fixed
"orphaned"/DANGER/no-spinner tuple for orphaned sessions and an un-recolored
base cascade for blocked sessions), so they are down-converted back to those.
2026-07-23 — Respawn-in-place rotation outcomes
The OrchestratorService began serving two RotationOutcome values on
Session.rotation_events[].outcome:
ROTATION_OUTCOME_RESPAWNED_SAME_ACCOUNT, audited when a pane that failed
authentication is stopped and respawned in place under the same account (the
account itself probes healthy), and ROTATION_OUTCOME_RESPAWN_CAP_EXHAUSTED,
audited when the per-chat respawn-in-place budget for the window is spent.
Clients pinned to an older version are down-converted back to
ROTATION_OUTCOME_STATUS_ONLY_NO_CAPABILITY, the prior observable value.
2026-08-03 — Agent-stalled attention reason
The OrchestratorService began serving AttentionReason value
ATTENTION_REASON_AGENT_STALLED on Session.attention_status.reason, a new
attention reason raised when a chat reports CHAT_STATUS_WORKING while its agent
has made no semantic progress for longer than its phase's threshold, a silently
dead turn behind a still-animating spinner. Like the auth-failed reason it is
only raised where the session had no other attention, so clients pinned to an
older version are down-converted back to no attention reason, the prior
"keeps spinning" behavior.
2026-08-04 — Waiting chat status
The OrchestratorService began serving ChatStatus value CHAT_STATUS_WAITING
on ChatStatusEntry.status, ChatStatusDelta.status and
SessionStatusEntry.status, alongside a new waiting_reason string on each of
those messages. A chat is waiting when it is blocked on an external event (a
registered GitHub callback, a background poll tick) rather than computing, and
waiting_reason carries a short human-readable explanation such as
awaiting checks_passed_ready on owner/repo#123 (it is empty for every other
status). Before this version such a chat reported CHAT_STATUS_WORKING, so
clients pinned to an older version are down-converted back to
CHAT_STATUS_WORKING with an empty waiting_reason.
2026-08-12 — Draft-PR failure ranks below live activity
The OrchestratorService stopped letting a session-level draft-PR-creation failure
claim the session's primary display composite while a chat is live. The
? PR failed label used to outrank every branch except a pending question and a
usage-limit banner, so a row could read ? PR failed while the chat underneath it
read working, two contradictory presents on one row. That branch now sits
immediately below the working branch, so Session.display_label,
Session.display_intent and Session.display_spinner carry the live label
(working, waiting, initializing, merging, archiving) whenever one
applies, and still report ? PR failed when nothing is live. Clients pinned to an
older version were built against the previous precedence, so they are
down-converted back to ? PR failed.
The failure itself is not hidden from a current client: it is surfaced as a session warning hint, and that hint is deliberately exempt from the accompanying recessive treatment because it is the row's only carrier of the failure.
2026-08-16 — A cron gate that could not run is no longer reported as gated
The OrchestratorService began distinguishing a cron gate that could not be
evaluated from one that ran and decided there was no work. Previously every
blocked fire — a gate that timed out, could not be launched, or that the shell
reported missing (exit 127) or not executable (exit 126) — was recorded as
gated and derived CRON_JOB_STATUS_GATED, a warning-styled "waiting, healthy"
value. A broken deployment therefore looked exactly like a quiet backlog.
A gate that could not run now serves CronJob.last_run_outcome gate_failed and
derives CronJob.last_run_status CRON_JOB_STATUS_FAILED, and RunCronJobNow
returns the matching gate_failed skip reason. A gate that ran and exited
non-zero for any other code keeps gated / CRON_JOB_STATUS_GATED, and still
blocks the fire. No new CronJobStatus enum value was added; the new outcome
reuses the existing CRON_JOB_STATUS_FAILED, so this is a change in the value
served rather than in the schema.
Clients pinned to an older version were built against the previous values, so they
are down-converted back to outcome gated, status CRON_JOB_STATUS_GATED, and
skip reason gated.
2026-08-20 — A session account switch stopped by its own deadline is no longer reported as ABORTED
The OrchestratorService began distinguishing a session account switch that ran
out of time from one that failed for an unknown reason. Previously the
daemon had no wire value for "a deadline stopped this", so a switch ended by its
own daemon-side respawn budget came back with no typed error code and
ProxySwitchSessionAccount answered ABORTED, the historical fallback for an
unmodelled failure.
ABORTED was not merely vaguer; it was actively misleading. It invites a retry,
and a request killed by its own deadline is exactly the request a caller must not
retry: re-sending restarts a budget that has already proven too short, and on an
at-least-once command stream it can stack a second switch on top of work the
first one may have partially completed.
A switch ended by its daemon-side budget now answers DEADLINE_EXCEEDED. A new
CommandResult.ErrorCode value, ERROR_CODE_DEADLINE_EXCEEDED, carries the
distinction over the daemon stream. No procedure or message field was added;
this is a change in the status code served on an existing procedure rather than
in the schema.
The change is scoped to the account switch alone. Other commands that fail with
an expired context are unaffected and still answer ABORTED. ProxySwitchSessionAccount's
own relay timeout (the orchestrator giving up waiting for the daemon) has
always answered DEADLINE_EXCEEDED and is unchanged at every version, including
for older clients.
Clients pinned to an older version were built when this case read as ABORTED,
so they are down-converted back to ABORTED with the failure message preserved.
2026-08-21 — Switch cancellation and result-ceiling behavior
The OrchestratorService began distinguishing a session account switch that the
caller canceled from one that failed for an unknown reason. Previously the
daemon had no wire value for "the caller canceled this", so a canceled switch
came back with no typed error code and ProxySwitchSessionAccount answered
ABORTED, the historical fallback for an unmodelled failure.
ABORTED invites a retry, which is the wrong signal for a request the caller
abandoned. Retrying can stack duplicate switch work on top of the partially
completed first attempt.
A switch canceled by its caller now answers CANCELED. A new
CommandResult.ErrorCode value, ERROR_CODE_CANCELED, carries the distinction
over the daemon stream. No procedure or message field was added; this is a
change in the status code served on an existing procedure rather than in the
schema.
The change is scoped to the account switch alone. Other daemon-scoped commands
that return the same typed code still answer ABORTED, and
ProxySwitchSessionAccount's own orchestrator-side cancellation path is
unchanged for every version.
Clients pinned to an older version were built when this case read as ABORTED,
so they are down-converted back to ABORTED with the failure message preserved.
ProxySwitchSessionAccount began using self-describing timeout text when bosso's
own 120-second result ceiling expires before a daemon verdict arrives. The status
code remains DEADLINE_EXCEEDED, but current clients now see that the account
switch may still be running, and that the request did not cancel or tear down the
switch. The caller should re-check the chat's account to see where it landed.
Previously this handler-owned result ceiling used the generic relay timeout text:
command timed out after 2m0s. Clients pinned to an older version were built
against that message, so they are down-converted back to the legacy text while
keeping DEADLINE_EXCEEDED.
2026-08-25 — Head-current check state
Session.last_check_state now serves only a CI verdict demonstrated at the
current PR head SHA. Stale, missing, or non-demonstrated observations serve
CHECKS_OVERALL_UNSPECIFIED, while the raw persisted latch moves to
Session.last_check_state_observed with last_check_state_head_sha and
last_check_state_at provenance.
Clients pinned to an older version are down-converted so last_check_state
again equals last_check_state_observed, matching the prior behavior that could
surface a stale verdict as if it still described the current head.
2026-09-02 — Organization-scoped visibility
The API surface now includes organization management messages and nine
organization RPCs on OrchestratorService: list, get, create, and rename
organizations; list, invite, remove, and role-edit members; and switch the
active organization.
The organization visibility cutover is intentionally handler-gated rather than
registered as a down-convert transform. Session, chat, repo, cron, notes,
webhook, GitHub App, attach-token, daemon, and stream procedures whose
caller-visible resource set changes must branch on the resolved API version:
clients pinned before 2026-09-02 keep the legacy user-scoped view, while
current clients observe organization-scoped visibility. The transform layer
cannot express this because it sees only the procedure and payload, cannot
inspect the caller, cannot turn a success back into NOT_FOUND, and cannot add
or remove streaming frames.
2026-09-03 — SwitchActiveOrganization retired
SwitchActiveOrganization is retired in favor of AuthKit
switchToOrganization. The RPC and request/response messages remain in
bossanova.v1 only for wire compatibility: buf breaking: use: FILE blocks
deleting them from v1, and true removal requires bossanova.v2.
Current clients receive UNIMPLEMENTED with retirement guidance naming AuthKit
switchToOrganization. Clients pinned to an older version are down-converted to
the legacy organization management is not implemented message while keeping
UNIMPLEMENTED.
2026-09-04 — Abandoned checkout no longer reports an activating subscription
An account that opened a Stripe Checkout session and never completed it was
previously reported as CLOUD_ACCESS_STATE_PENDING_ENTITLEMENT_REFRESH with the
message "Your subscription is being activated." and can_create_checkout=false,
which left clients polling forever for an activation that would never arrive.
Current clients receive CLOUD_ACCESS_STATE_NEEDS_SUBSCRIPTION with
checkout_started=true, can_create_checkout=true,
denial_reason="subscription_required" and a message inviting the user to
resume checkout. Only an account that has actually paid (one awaiting its
entitlement refresh) still reports the activating shape.
No field or enum member was added: the change is in the values served on
CloudAccessStatus, carried on GetCloudAccessStatus,
CreateCheckoutSession, and RefreshCloudEntitlements. Clients pinned to an
older version are down-converted back to the activating shape.
One behavior change on this version is not down-converted, deliberately.
CreateCheckoutSession against an account that had started a checkout but never
recorded an idempotency key used to be refused; it now records a key and returns
a hosted URL. A down-convert transform can only rewrite a response, never turn a
success back into an error, and re-introducing the refusal for pinned clients
would restore the dead end this version exists to remove. Pinned clients see the
old status shape but the new checkout behavior.
2026-09-05 — Repository and daemon listings span every organization you belong to
ProxyListReposAggregated and ListDaemons used to answer for a single
organization, resolved from the caller's own session claim. A repository or
daemon registered under another organization the caller belongs to was simply
absent from the reply, and nothing in the response said so.
Current clients receive the union across every organization they belong to, while daemon-authenticated callers retain the single-organization inventory. Repository rows remain deduplicated by origin URL exactly as before, and daemon inventory remains acting-user scoped: organization membership never exposes another member's machine or hostname.
Two additions carry the change. The request gains an optional organization_id:
set it to narrow the read to one organization (the previous behavior, now
stated in the request rather than inherited from the claim), and leave it unset
for the union. Each response gains failed_organizations, naming any
organization whose read failed, so a partial answer is reported as partial
instead of returned as a shorter list. An organization with no live machines is
not a failure and is not listed. A narrowed read never reports partial results:
one unavailable organization is the whole request failing. DaemonInfo also
gains organization_id, allowing clients to attribute each inventory row.
Clients pinned to an older version keep the single-organization read and always
see failed_organizations empty, because the path that fills it does not run for
them. This is one of the changes that cannot be expressed as a down-convert
transform (restoring "the one organization this particular caller's claim named"
requires the request, which a response transform never sees), so the older
behavior is served by the handler itself rather than by the transform chain.
2026-09-06 — Cloud access resolves across every organization the caller belongs to
Cloud access used to be decided from the organization the caller claimed on the request alone. A user who belongs to several WorkOS organizations, whose subscription lives on one of them, was refused whenever they arrived claiming a different one: the same person, the same paid seat, a different claim.
Current clients have their access resolved across every organization they
belong to: the claimed organization is judged first, and if it cannot grant
access on its own the server folds over the caller's other member organizations
and grants CLOUD_ACCESS_STATE_ACTIVE when one of them is entitled. Clients
pinned to an older version keep the claimed-organization-only verdict.
No field or enum member was added: the change is in the values served on
CloudAccessStatus, carried on GetCloudAccessStatus, CreateCheckoutSession,
and RefreshCloudEntitlements.
This version registers no down-convert transform, deliberately. The verdict
is caller-relative (the same CloudAccessStatus message is correct for one
caller and wrong for another, depending on which organization earned it), and a
TransformResponse receives only the method name and the response message,
never the request. It therefore cannot tell an ACTIVE earned by the caller's
own organization from one earned by a sibling. The compatibility seam is instead
the handler-level version gate that docs/api-versioning.md names as the
sanctioned escape hatch for changes the transform layer cannot express: the
handlers serving these three RPCs restrict the caller to their claimed
organization when the negotiated version resolves older than 2026-09-06.
2026-09-07 — Cloud access status names your organization
CloudAccessStatus.workos_org_id is now populated with the WorkOS organization
the caller is acting as. The field has existed since cloud subscription gating
shipped, but nothing ever filled it, so every client that read it saw an empty
string on every response.
Current clients receive the organization resolved for the request: the caller's
verified organization scope, falling back to the org_id claim on the session.
A daemon caller, which has no organization of its own, still receives an empty
value. The field is populated on GetCloudAccessStatus, CreateCheckoutSession
and RefreshCloudEntitlements alike, and it describes who asked rather than what
the answer was, so it is present regardless of the access state.
This is what lets a command-line client send a refused cloud login to the organization it was actually refused for: it matches the id against the caller's organization list and opens the organization-scoped subscribe page instead of the unscoped one.
No field or message was added; only the value served changed, so clients pinned
to an older version, which were built when this field could only be empty, are
down-converted back to an empty workos_org_id.
2026-09-08 — Session commands route across every organization you belong to
Session listings already span every organization the caller belongs to, but
commands used to resolve their target only in the active organization. A caller
could therefore see and open a session filed in another member organization,
then receive NOT_FOUND when trying to stop or otherwise act on it.
Current clients retry a missed session-owner lookup across the caller's member
organizations. The caller must already belong to every organization considered,
and each candidate is rechecked against the authorized membership scope.
Sessions held by non-member organizations remain indistinguishable from unknown
session ids: both return NOT_FOUND, and neither dispatches a command.
Clients pinned to an older version keep the active-organization-only command
routing behavior. This version registers no down-convert transform because the
change turns an error into a success: an error transform never sees the success
path, and a response transform cannot recreate the prior NOT_FOUND. The
compatibility branch therefore lives in the handler.
2026-09-09 — Owner-resolution outages no longer look like empty results
ProxyListSessions now distinguishes an unavailable session-ownership store
from a session that is legitimately outside the caller's organization. A
missing owner remains a silent, privacy-preserving skip. Any other ownership
lookup failure returns UNAVAILABLE with a stable public message instead of a
successful but silently shortened list; the underlying dependency error remains
in server logs only.
Clients pinned to an older version retain the legacy successful short-list response. The server continues scanning after the failed lookup, so that legacy response contains every session whose ownership could still be resolved rather than stopping at the first outage.
2026-09-10 — Fleet reads span every organization you belong to
An unset ProxyListCronJobsRequest.organization_id now returns the union across
every organization the caller belongs to. Each row carries its owning
organization, and failed_organizations makes a partial fleet read explicit.
Setting organization_id still narrows the request to one authorized organization.
Clients pinned to 2026-09-09 or older keep the claimed-organization-only list. This compatibility behavior lives in the handler because reconstructing the organization named by a caller's claim requires request context unavailable to a response transform.
Seven additional fleet-wide reads now query the caller's own Ready daemons
across every organization they belong to. Each request accepts an
organization_id filter to narrow the read after a membership check, and each
response reports organizations whose fleet could not be read. ProxyEmptyTrash
additionally reports its deletion count per organization. Clients pinned to
older versions retain the former single-organization behavior.
Repository lists now also fail explicitly when organization-holder enrichment is unavailable, instead of returning blank holder values that look like unclaimed repositories. Clients pinned to 2026-09-09 or older retain the legacy successful repository list without organization stamps for that failure. The response-aware compatibility transform recovers only the typed holder-store failure; other repository-list errors remain errors at every version.
2026-09-11 — Invitation lifecycle completion
RemoveOrganizationMember now accepts invitation_id to revoke an
organization-owned pending invitation. The operation uses an
organization-scoped lookup before revoking, so another organization's invitation
remains indistinguishable from an unknown id.
Clients pinned to 2026-09-10 or older keep the prior INVALID_ARGUMENT
behavior. This compatibility boundary lives in the handler because a successful
empty response cannot be transformed back into an error after the revoke side
effect has occurred.
ListOrganizationMembers now returns an invitation-only row while WorkOS has
accepted an invitation but its user has not yet landed as a local member. The
row sets is_invite_accepted and invitation_id; local user and membership ids
remain empty. Clients pinned to 2026-09-10 or older do not receive this row.
Membership reconciliation also corrects Stripe seat quantity after an insert or removal changes local membership cardinality. This best-effort billing repair is not version-gated: response transforms cannot undo an external Stripe write, and preserving a stale quantity would preserve the billing defect.
2026-09-12 — Session listings span every organization you belong to
ProxyListSessions now returns the union across every organization the caller
belongs to when organization_id is unset. Set organization_id to narrow the
read to one organization after a membership check. Sessions remain attributed
with organization_id, and failed_organizations reports any organization
whose slice could not be read without discarding the rest of the union.
Clients pinned to 2026-09-11 or older keep the former single-organization read, resolved from their active organization claim. Header-less callers also keep that legacy behavior because an absent version header resolves to the Baseline. This compatibility boundary lives in the handler: a response transform cannot inspect the request or reconstruct the caller-relative claimed organization.
The deprecated ProxyListSessionsAcrossOrganizations RPC remains served for
older clients, but new callers should use ProxyListSessions.
2026-09-13 — A healthy account check reports a superseded credential
AuthCheck.failure_class now carries credential_superseded alongside
outcome healthy. It means an ambient codex login for the same provider
account holds a different refresh token, so the refresh chain behind the stored
credential is dead even though the provider still accepts the stored access
token. The account stays eligible, which is why the outcome remains healthy —
the class is a warning about the future, not a failure.
Before this version a healthy check always carried an empty failure_class.
Clients pinned to 2026-09-12 or older are down-converted back to that empty
value on every response that carries an Account: ProxyListAccounts,
ProxyManageListAccounts, ProxyAddAccount and ProxyRefreshAccount. A class
on a non-healthy outcome is untouched — that shape predates this version.
2026-09-14 — Unproven credential refresh chain (Current)
Account.auth_check.outcome began serving refresh_chain_unproven (with
failure_class refresh_not_observed) for a credential check that completed
with no provider error, on a credential whose own access token says a token
refresh should already have happened, and whose run observed no credential
write. That is a live access token sitting above a refresh chain nothing has
proven still works: the check keeps passing until the access token expires, then
fails hard with no prior warning.
It is a warning, not an accusation. auth_invalid remains the only outcome
that removes an account from selection, and an account reporting
refresh_chain_unproven stays selectable.
Before this version the identical clean run served healthy with an empty
failure_class. auth_check.outcome is a plain string that clients switch on
to choose a severity, so clients pinned to 2026-09-13 or older are
down-converted back to that pair rather than being shown a token they were never
built to read.
See also
- API Reference — the generated OpenAPI reference for the OrchestratorService.
docs/api-versioning.mdin the repository — the engineering procedure for adding a new API version and down-convert transform.