Skip to content
GitHub
Decisions

ADR-089: Action discoverability endpoint — OpenAPI 3.x, per-`(org, domain)`, with version pinning

Context

ADR-077 D5 committed Spectral to OpenAPI-style action discoverability on the HTTP surface: callers’ hosts get a programmatic way to know which actions exist for a given (org_id, domain_id), what each action’s request-body shape is (the world-model context schema’s supplied-source subset relevant to that action), and what the response shape is. Deferred specifics: separate vs. integrated endpoint, schema export format, version negotiation, caching contract.

ADR-088 D2 settled the MCP-side discoverability surface (spectral.list_actions tool). This ADR settles the HTTP-side equivalent; the two surfaces share underlying introspection logic.

Decision

D1 — Dedicated endpoint at GET /api/orgs/{org_id}/domains/{domain_id}/actions

Action discoverability lives on a dedicated HTTP endpoint distinct from POST /api/decide. URL shape follows the ADR-086 D7 org/domain path conventions (URL itself is illustrative per the ADR-077 D-note on placeholder paths). A GET request to the endpoint returns the OpenAPI 3.x document describing the actions available to the authenticated caller’s (org_id, domain_id) scope.

Separating discoverability from invocation lets clients introspect cheaply (small response, GET, cacheable) without invoking decide; lets caching apply naturally (D4); lets the response shape be tuned to discovery use cases (full OpenAPI structure with all the standard tooling support) rather than constrained by decide’s decision-contract shape.

D2 — Schema export: OpenAPI 3.x JSON

The endpoint returns a complete OpenAPI 3.x JSON document describing the action surface for the (org, domain, world_model_version) resolved by the request. Standard OpenAPI structure:

  • openapi: "3.1.0" (or current stable version at v0 implementation).
  • info: title, version (the world-model version), description naming (org, domain).
  • servers: the API server URL.
  • paths: actions exposed as operations under POST /api/decide, with each action’s requestBody describing the action-specific context schema (the supplied-source attributes relevant to that action). The decide operation’s response schema (per ADR-077 D3) is referenced consistently across actions.
  • components.schemas: shared definitions for context attribute types, the status enum (GREEN / GREEN-SKIP / YELLOW / RED), the work-frame structure per ADR-077 D3, and the decision-metadata structure.
  • components.securitySchemes: Bearer JWT per ADR-039 + ADR-087.

The per-action requestBody schema is constructed at request time from the active world-model version’s action registry: each action’s requestBody lists the supplied context attributes relevant to that action (the attributes the action’s rules consume — the action’s persisted canonical input ontology per ADR-107, typed + documented and cross-rule-reconciled at publish so two rules naming one quantity present as one input; see the Consequences note). Computed and system-generated attributes are not part of the request body — they’re documented in info.description for context but not as input schemas.

Specific JSON structure (field naming, OpenAPI version, $ref usage) is implementation detail; the architectural commitment is that the response is a valid OpenAPI 3.x JSON document that off-the-shelf OpenAPI tooling (code generators, validators, mock servers) can consume.

D3 — Version negotiation

By default, the endpoint returns the document for the active world-model version for (org, domain). Callers can pin to a specific version via the world_model_version query parameter:

  • GET /api/orgs/{org_id}/domains/{domain_id}/actions → active version.
  • GET /api/orgs/{org_id}/domains/{domain_id}/actions?world_model_version=7 → version 7 (active or retired).

The pin model mirrors the world_model_version request body field on POST /api/decide (per ADR-077 D2). Pinned responses are stable artifacts that callers can persist and validate against historical decisions.

Requesting a non-existent or unauthorized version returns 404 + RFC 9457 envelope per ADR-006 D3.

D4 — Caching contract

Active-version responses are cacheable with a short TTL:

Cache-Control: public, max-age=60
ETag: "<sha256 of the response body>"

Clients can validate freshness via If-None-Match; servers respond 304 Not Modified when the ETag matches. The 60-second max-age trades a small staleness window for amortized lookup cost — on world-model-version activation (per ADR-085 D4 active-version invalidation), discoverability picks up the new version within the cache window.

Pinned-version responses are immutable:

Cache-Control: public, max-age=31536000, immutable
ETag: "<sha256 of the response body>"

A pinned world_model_version is a frozen artifact — the response cannot change because the version’s content cannot change (per ADR-026 + ADR-080’s audit-integrity properties). Long-lived caching is safe.

D5 — Auth

The endpoint requires the same Bearer JWT as POST /api/decide (per ADR-077 D1 + ADR-039 + ADR-087). The authenticated caller’s (org_id, domain_id) scope determines which actions appear in the response — an org’s discoverability response shows only that org’s actions; a customer cannot enumerate another customer’s action surface.

Operator delegation tokens (per ADR-087 D2) see the target customer’s action surface — the outer org_id + domain_id determines the response just as it does for decide calls.

Alternatives considered

Integrated discoverability (each action’s URL exposes its own OpenAPI fragment via OPTIONS or /{path}/openapi.json). Rejected. Doesn’t match the body-routing decision in ADR-077 D2 (actions don’t have distinct URL paths). A single discoverability endpoint per (org, domain) returning the full OpenAPI document is the natural shape.

Custom Spectral-specific JSON shape (not OpenAPI). Rejected. OpenAPI is the industry standard; off-the-shelf tooling (code generators, validators, mock servers) is the customer-visible benefit. A Spectral-specific shape would force every customer integration to handle two schema formats (Spectral’s discoverability + everyone else’s OpenAPI).

Expose all versions in a single response (callers pick). Rejected. The response would scale with the number of versions; agents that just want the active version pay for unused content. The pin query parameter handles the specific-version case cleanly.

No caching at v0. Rejected. Discoverability is a poll-on-startup pattern for many agent frameworks; without caching, every agent restart re-fetches the full OpenAPI document. Short TTL on active version + immutable on pinned versions amortizes the lookup cost with negligible staleness.

Long max-age (e.g., 1 hour) on active version. Rejected. World-model version activation should be visible to discoverability clients quickly; the 60-second TTL keeps the staleness window small while still amortizing lookup cost across typical poll patterns.

Separate JSON schema documents (not OpenAPI; standalone JSON Schema per action). Rejected. OpenAPI subsumes JSON Schema for request/response shapes and adds the operation-level metadata (auth, paths, security) that callers actually consume. Standalone JSON Schema would lose the operation-level structure.

Consequences

  • A new HTTP endpoint lands in apps/api per ADR-076 D1: GET /api/orgs/{org_id}/domains/{domain_id}/actions (illustrative URL). The endpoint constructs the OpenAPI document at request time from the world-model state (action registry + per-action context schemas + version metadata).
  • The introspection logic that builds the OpenAPI document is shared with ADR-088 D2’s spectral.list_actions MCP tool — same source data, different protocol envelope. Both surfaces derive from the platform-side projection of worlds state (per ADR-085 D2).
  • Rate limits per ADR-084 D1 apply to the discoverability endpoint per (org_id, api_key_id) like any other API route; discoverability traffic is typically low-volume + cacheable, so it rarely approaches rate-limit ceilings.
  • Audit chain entries for discoverability requests are not part of the decision audit chain (these are introspection reads, not decisions); operator observability per ADR-084 D2 surfaces discoverability call patterns for capacity tuning.
  • OpenAPI document construction logic is a Phase 4 implementation epic. Specific OpenAPI version (3.0 vs 3.1) and shape details land with the implementation; the architectural commitment is the format + the per-(org, domain, version) content.
  • Cross-IdP federation and customer-provided OpenAPI extensions (e.g., custom x- keys) are reservable post-release; the v0 document is Spectral-authored only.
  • The action discoverability endpoint design open item from ADR-077 D5 is settled by this ADR.
  • The MCP-side discoverability (ADR-088 D2) and the HTTP-side discoverability (this ADR) together close out the action-discoverability commitment from ADR-077 D5 across both protocols.
  • The action registry this endpoint reads is realized by ADR-104worlds.actions + worlds.action_rules (M:N), one content-addressed module per declared action, and the retirement of the v0 "*" wildcard. Until ADR-104, the “action registry” referenced in D2/D3 is the single wildcard module; after it, this endpoint lists the world’s real declared action set with per-action attribute contracts.
  • Per-action requestBody attributes — the documented canonical ontology (ADR-107). Each action’s attributes are the action’s persisted canonical input ontology (ADR-107 D3) — the typed, documented inputs reconciled across the action’s rules at publish (D5), so two rules naming one quantity present as a single input — shipped in the deployed bundle as input_schema.json and read back by the discovery endpoint (with additionalProperties: false). Each property carries its name, value type (string | number | boolean | enum with enum allowed_values), description, and required flag — not names only. Server-injected and computed keys (request_id, request_time, authenticated_caller, the top-level action) are excluded per D2; if the deployed bundle cannot be loaded the endpoint degrades gracefully (lists the actions, omits the per-attribute enrichment).