openapi: 3.2.0 info: title: Axonflow Decisions & Overrides API version: 11.1.0 contact: name: AxonFlow Support url: https://getaxonflow.com/support license: name: Business Source License 1.1 url: https://github.com/getaxonflow/axonflow/blob/main/LICENSE description: 'Operations tagged Decisions & Overrides across 2 of this provider''s published API definitions: axonflow-orchestrator-api.yaml, axonflow-orchestrator-openapi.yml. Each path carries the servers of the definition it was published in.' servers: - url: https://orchestrator.getaxonflow.com description: Production (SaaS) - url: http://localhost:8081 description: Local Development tags: - name: Decisions & Overrides description: 'Decision explainability (ADR-043) and session-scoped policy overrides (ADR-044). List recent governance decisions, explain a specific decision, and create/list/revoke time-boxed policy overrides.' paths: /api/v1/overrides: post: tags: - Decisions & Overrides summary: Create a session-scoped policy override (retired in v11) description: 'Retired in v11 (PRD v11 §1.5, #4252). The workflow step gate no longer reads an ADR-044 session override (the last deciding reader, the agent''s tier pass, goes with #4281), so this route writes nothing: a policy is enabled, disabled or re-actioned in the organization''s typed document through `/api/v1/typed-policies`. The route stays registered so a caller is told where the write went. The guards that authenticate the request answer first (the agent''s proxy token, 403; the per-user identity, 401; the tenant, 400), then every caller is answered `409 LEGACY_POLICY_WRITE_FROZEN`. The body is not read. The override reads (`GET`) are unaffected.' operationId: createPolicyOverride parameters: - name: X-User-Email in: header required: true description: Authenticated user identity (falls back to X-User-ID); missing identity is a 401 schema: type: string - $ref: '#/components/parameters/TenantIDHeader' - $ref: '#/components/parameters/OrgIDHeader' requestBody: required: false description: Not read. The route answers the freeze before any body is decoded (#4252). content: application/json: schema: $ref: '#/components/schemas/CreateOverrideRequest' example: policy_id: pii-us-ssn-redact policy_type: static tool_signature: mcp:github/create_issue override_reason: Approved incident-response exception INC-4432 ttl_seconds: 1800 responses: '400': description: Missing X-Tenant-ID content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing user identity (X-User-Email / X-User-ID) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: The request did not arrive through the agent gateway (proxy authentication) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '409': $ref: '#/components/responses/PolicyOverrideWriteRetired' get: tags: - Decisions & Overrides summary: List policy overrides description: 'Lists the tenant''s policy overrides, newest first, capped at 100 rows. Revoked overrides are excluded unless `include_revoked=true`. **Role-scoped reads (#2922):** `admin`/`owner` list every override in the tenant; every other role lists only the overrides they created (`created_by`). Revoking an override is scoped the same way. The role/scope is trusted only over the internal proxy-auth channel.' operationId: listPolicyOverrides parameters: - $ref: '#/components/parameters/TenantIDHeader' - name: policy_id in: query required: false description: Filter by policy UUID or slug/name schema: type: string - name: include_revoked in: query required: false description: Include revoked overrides (only the literal `true` enables it) schema: type: boolean default: false responses: '200': description: Override list headers: X-Axonflow-Read-Scope: $ref: '#/components/headers/XAxonflowReadScope' content: application/json: schema: type: object properties: overrides: type: array description: Always a JSON array (`[]` when empty) items: $ref: '#/components/schemas/OverrideSummary' count: type: integer '400': description: Missing X-Tenant-ID header content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': description: Internal error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' servers: - url: https://orchestrator.getaxonflow.com description: Production (SaaS) - url: http://localhost:8081 description: Local Development /api/v1/overrides/{id}: get: tags: - Decisions & Overrides summary: Get a policy override description: 'Returns one override by id, tenant-scoped. Another tenant''s override returns 404.' operationId: getPolicyOverride parameters: - name: id in: path required: true description: Override id schema: type: string - $ref: '#/components/parameters/TenantIDHeader' responses: '200': description: Override detail headers: X-Axonflow-Read-Scope: $ref: '#/components/headers/XAxonflowReadScope' content: application/json: schema: $ref: '#/components/schemas/OverrideDetail' '400': description: Missing override id or missing X-Tenant-ID header content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: 'Override not found — also returned when the override exists but was created by another user and the caller is scoped to `own-rows`. Same body in both cases (non-oracle); `X-Axonflow-Read-Scope` distinguishes them. ' headers: X-Axonflow-Read-Scope: $ref: '#/components/headers/XAxonflowReadScope' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': description: Internal error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' delete: tags: - Decisions & Overrides summary: Revoke a policy override (retired in v11) description: 'Retired in v11 (PRD v11 §1.5, #4252). The step gate no longer reads a session override (the last deciding reader, the agent''s tier pass, goes with #4281), so this route revokes nothing and leaves the row as the record of what was granted. After the guards that authenticate the request (403, 401, 400, as on `POST`), every caller is answered `409 LEGACY_POLICY_WRITE_FROZEN`.' operationId: revokePolicyOverride parameters: - name: id in: path required: true description: Override id schema: type: string - name: X-User-Email in: header required: true description: Authenticated user identity (falls back to X-User-ID); missing identity is a 401 schema: type: string - $ref: '#/components/parameters/TenantIDHeader' - $ref: '#/components/parameters/OrgIDHeader' responses: '400': description: Missing X-Tenant-ID header content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing user identity (X-User-Email / X-User-ID) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: The request did not arrive through the agent gateway (proxy authentication) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '409': $ref: '#/components/responses/PolicyOverrideWriteRetired' servers: - url: https://orchestrator.getaxonflow.com description: Production (SaaS) - url: http://localhost:8081 description: Local Development /api/v1/decisions: get: tags: - Decisions & Overrides summary: List recent governance decisions description: 'Lists recent decisions for the tenant, newest first. The lookback window and maximum page size are **tier-gated**: Community/Free sees the last 5 decisions in 24h; Evaluation 100 decisions in 14 days; Enterprise up to 1000 with an unbounded window. Requesting a `limit` above the tier cap returns **429** with the upgrade envelope (`error`, `limit_type: decision_list_size`, `tier`, `limit`, `remaining`, `upgrade{tier, wording, compare_url, buy_url}`) plus the `X-Axonflow-Tier-Limit` and `X-Axonflow-Upgrade-URL` headers. A `since` earlier than the tier window is silently clamped to the window. **Role-scoped reads (#2922):** `admin`/`owner` list every user''s decisions; every other role lists only their own (rows attributed to their identity). A caller with no resolvable per-user identity gets an empty list. The role/scope is trusted only over the internal proxy-auth channel. `DEPLOYMENT_MODE=community` lists tenant-wide, and `community-saas` lists tenant-wide over the agent gateway — see `POST /api/v1/audit/search` for the single-operator rationale (#3060).' operationId: listDecisions parameters: - $ref: '#/components/parameters/TenantIDHeader' - name: limit in: query required: false description: Page size (positive integer). Defaults to the tier maximum. schema: type: integer minimum: 1 - name: since in: query required: false description: RFC3339 lower bound; defaults to (and is clamped to) the tier lookback window schema: type: string format: date-time - name: decision in: query required: false description: Canonical verdict filter schema: type: string enum: - allowed - blocked - redacted - needs_approval - error - name: policy_id in: query required: false description: Filter to decisions where this policy fired schema: type: string - name: tool_signature in: query required: false description: Filter by tool signature schema: type: string responses: '200': description: Decision list (`decisions` is `[]` when empty, never `null`) headers: X-Axonflow-Read-Scope: $ref: '#/components/headers/XAxonflowReadScope' content: application/json: schema: $ref: '#/components/schemas/DecisionListResponse' '400': description: Invalid limit, since (must be RFC3339), or decision value content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing X-Tenant-ID header content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Requested limit exceeds the tier page cap headers: X-Axonflow-Tier-Limit: schema: type: string example: decision_list_size X-Axonflow-Upgrade-URL: schema: type: string example: https://getaxonflow.com/pricing/ content: application/json: schema: $ref: '#/components/schemas/DecisionListLimitEnvelope' '500': description: Internal error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' servers: - url: https://orchestrator.getaxonflow.com description: Production (SaaS) - url: http://localhost:8081 description: Local Development /api/v1/decisions/{id}/explain: get: tags: - Decisions & Overrides summary: Explain a governance decision description: 'Returns the explanation for a decision id (ADR-043): matched policies, verdict and reason, risk level, override availability (and any existing override id), the caller''s 24h hit count for the first matched policy, and policy version drift (version at decision time vs latest). Tenant-scoped via `X-Tenant-ID`; a decision belonging to another tenant returns 404 (not 403) so the endpoint is not an existence oracle.' operationId: explainDecision parameters: - name: id in: path required: true description: Decision id schema: type: string - $ref: '#/components/parameters/TenantIDHeader' - name: X-User-Email in: header required: false description: Caller identity used for the per-user historical hit count (falls back to X-User-ID) schema: type: string responses: '200': description: Decision explanation headers: X-Axonflow-Read-Scope: $ref: '#/components/headers/XAxonflowReadScope' content: application/json: schema: $ref: '#/components/schemas/DecisionExplanation' '400': description: Missing decision id content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing X-Tenant-ID header content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Not authorized to explain this decision (defensive tenant mismatch) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: 'Decision not found, past the retention window, belongs to another tenant, or belongs to another user while the caller is scoped to `own-rows`. All four return the same body (non-oracle); `X-Axonflow-Read-Scope` is what separates a scoping outcome from a genuinely missing decision — the "explain fails on a decision the platform produced seconds ago" symptom. ' headers: X-Axonflow-Read-Scope: $ref: '#/components/headers/XAxonflowReadScope' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '500': description: Internal error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' servers: - url: https://orchestrator.getaxonflow.com description: Production (SaaS) - url: http://localhost:8081 description: Local Development components: headers: XAxonflowReadScope: description: "RBAC read-scope diagnostic. Echoes the read scope the orchestrator\nresolved server-side for this caller and applied to the query:\n\n- `tenant` — the caller may read every user's rows within the\n (header-forced) tenant: a validated `admin`/`owner` role over the\n internal proxy-auth channel, the customer-portal's tenant-scope\n assertion, or a single-operator deployment (`DEPLOYMENT_MODE=community`,\n and `community-saas` for requests proven to have arrived over the\n agent gateway — there the organization, tenant and credential are one\n `cs_`, so \"tenant-wide\" is that one evaluator's own data).\n- `own-rows` — the caller has a validated per-user identity but no\n tenant-wide authority, so the read was restricted to rows stamped\n with their own canonical `user_email`.\n- `none` — fail-closed. The caller presented neither tenant-wide\n authority nor a per-user identity, so **zero rows** were returned.\n The orchestrator also logs one diagnostic line on this path.\n\nThe header exists so that a `200` with an empty page — or a `404` on a\nrecord that does exist — is distinguishable from a genuinely empty\ntrail. That ambiguity is what let a whole deployment read zero rows\nwithout anyone noticing.\n\n**Diagnostic only.** The response body is byte-for-byte unchanged by\nits presence or value, and nothing keys authorization off it. It is a\nRESPONSE header the orchestrator writes, never an input: a\nclient-supplied `X-Axonflow-Read-Scope` REQUEST header is stripped by\nthe agent gateway and is never trusted. Scope is derived from the\nvalidated identity carried over the internal agent→orchestrator\nproxy-auth channel.\n\nRead scope is also a separate axis from administrative authority.\n`tenant` here does not imply the caller may run whole-tenant compliance\nexports or the cost/usage/execution family — those stay admin-gated and\nstill return 403.\n\nStamped before the handler writes its status line, so it is present on\nany response produced after the scope is resolved (including the\ndeliberately non-oracle `404`s), and absent on requests rejected before\nthat point (e.g. a missing `X-Tenant-ID`).\n" schema: type: string enum: - tenant - own-rows - none example: tenant responses: PolicyOverrideWriteRetired: description: 'Policy overrides are retired in v11 (PRD v11 §1.5, #4252). The workflow step gate no longer reads a session override (the last deciding reader, the agent''s tier pass, goes with #4281), so the session override writes answer this in every edition and on every database role, after the guards that authenticate the request. A policy is enabled, disabled or re-actioned in the organization''s typed document through `/api/v1/typed-policies`. The body is the coded envelope with `code` the string `LEGACY_POLICY_WRITE_FROZEN`. ' content: application/json: schema: $ref: '#/components/schemas/CodedErrorResponse' example: error: code: LEGACY_POLICY_WRITE_FROZEN message: 'Policy overrides are retired in v11: a policy is enabled, disabled or re-actioned in the organization''s typed document (a shipped system control in its system_controls section) through the typed authoring route at /api/v1/typed-policies. Reads on this endpoint are unaffected.' parameters: OrgIDHeader: name: X-Org-ID in: header required: false description: 'Organization identifier. Falls back to authenticated org from the Basic auth client when omitted. Surface for callers that need to override (e.g. cross-org admin reads in Enterprise). ' schema: type: string example: travel-us TenantIDHeader: name: X-Tenant-ID in: header required: true description: 'Tenant identifier scoping the request. The AxonFlow Agent gateway sets this header after authentication; the orchestrator fails closed when it is absent (401 on audit/decision endpoints, 400 on others — see each operation). Clients cannot widen their scope through it: handlers force the tenant filter from this header, never from the request body. ' schema: type: string example: travel-us schemas: DecisionListLimitEnvelope: type: object description: 'Tier-limit envelope returned with 429 when the requested page size exceeds the tier cap. Same shape as the plugin rate-limit envelopes. ' properties: error: type: string example: Free tier shows the last 5 decisions in 24h. Pro raises this to 100 decisions in the last 30 days. limit_type: type: string example: decision_list_size tier: type: string description: Effective tier the request was evaluated under limit: type: integer description: The tier's page cap remaining: type: integer example: 0 upgrade: type: object properties: tier: type: string example: Pro wording: type: string compare_url: type: string example: https://getaxonflow.com/pricing/ buy_url: type: string OverrideDetail: $ref: '#/components/schemas/OverrideView' OverrideSummary: $ref: '#/components/schemas/OverrideView' DecisionListResponse: type: object properties: decisions: type: array description: Always an array (`[]` when empty) items: $ref: '#/components/schemas/DecisionListItem' CreateOverrideRequest: type: object required: - policy_id - policy_type - override_reason properties: policy_id: type: string description: Policy UUID, or the human-readable slug (static) / name (dynamic) policy_type: type: string enum: - static - dynamic tool_signature: type: string description: Optional tool scope for the override override_reason: type: string maxLength: 500 description: Mandatory justification (ADR-044) ttl_seconds: type: integer format: int64 description: 'Override lifetime in seconds. Omitted/0 defaults to 3600. Clamped server-side to [60, 86400]; the response reports any clamp. ' OverrideView: type: object description: 'THE element shape for `policy_overrides`. One shape behind all three of this plane''s views — list, by-id and create. It is NOT member-for-member identical to the agent plane''s view of the same table (`PolicyOverride`), and saying so precisely matters: this shape adds the deprecated `organization_id` alias and does not carry `updated_by` / `updated_at`. What it does carry is everything an integrator was missing — `action_override` and `enabled_override` above all. Issue #3944: these were three different shapes. The list view omitted `org_id`, `tool_signature`, `created_by` and — the two that matter — `action_override` and `enabled_override`, which are what an override DOES; the by-id view omitted the same two and spelled the organisation key `organization_id`; and the create response echoed no `override_reason`, the mandatory ADR-044 justification the caller had just supplied. Listing overrides through this plane could not tell an operator what any of them was. The three schema NAMES below are retained and each is now this one shape, so a client generated from an earlier revision keeps its type. ' properties: id: type: string policy_id: type: string description: 'The policy this override applies to. **Its VALUE differs by view, deliberately, and has since before the three views were collapsed onto one shape.** `GET /api/v1/overrides` and `GET /api/v1/overrides/{id}` return the canonical UUID stored on the row. Before v11.0.0 the create ECHOED THE VALUE SENT IN THE REQUEST and does not resolve it to a UUID — a caller may create an override by slug or name, and the create response hands back what they sent. A client correlating a created override with a later listing must key on `id`, not on `policy_id`. One SHAPE across the views is what issue #3944 asked for and what these three now have; identical values on every member is a different property, and this member does not hold it. ' policy_type: type: string description: '`static` or `dynamic`. Deliberately NOT declared as an `enum`: on a RESPONSE property oasdiff reads a newly-declared enum as values being ADDED to a set clients already switch on, and reports four breaking changes for a constraint that tightens nothing. ' tenant_id: type: string description: 'ABSENT on an org-scoped override — the key is omitted, not sent as `null`. A NULL tenant column is what makes a row org-scoped rather than tenant-scoped, and the platform marshals that as an omitted member. An earlier revision of this description said "null", which no response ever contains and which this schema would reject. ' org_id: type: string description: 'The organisation that owns this override. This is the canonical spelling on this surface — it is what the column is called and what the agent plane''s view of the same table has always emitted. ' organization_id: type: string deprecated: true description: 'DEPRECATED alias of `org_id`, always carrying the same value. Read `org_id`. Retained rather than removed because this is a published endpoint and a shipped client may read it. Unrelated to the `organization_id` on the POLICY surface, which is current and not deprecated; the two share a name and nothing else. ' action_override: type: string description: 'WHAT THE OVERRIDE DOES. Absent from this plane''s views before #3944, which is why an operator could not tell one override from another. Every row created before v11.0.0 carries `allow`. ' enabled_override: type: boolean description: 'Whether the policy is force-enabled or force-disabled by this override. Absent on a row created before v11.0.0 through `POST /api/v1/overrides`, which does not set it. ' override_reason: type: string description: The mandatory ADR-044 justification (max 500 chars). expires_at: type: string format: date-time tool_signature: type: string description: Present when the override is restricted to a single tool. created_by: type: string created_at: type: string format: date-time revoked_at: type: string format: date-time description: Present only on revoked overrides revoked_by: type: string description: Present only on revoked overrides required: - id - policy_id - policy_type - org_id - organization_id - override_reason - created_by - created_at CodedErrorResponse: type: object description: 'The CODED error envelope: `{error: {code, message}}`, where `code` is a screaming-snake string enum. This is what the per-handler `writeError` methods emit across the policy API, the LLM provider API, the agents, template, unified-execution and media-governance APIs, and every handler in the RBI module (362 call sites in total). It is one of TWO error SHAPES this document describes. `code` is a STRING on this envelope; it is never an HTTP status integer. `LLMProviderAPIError` is this shape with the `code` enum constrained to the five values the LLM-provider handlers emit. ' properties: error: type: object properties: code: type: string description: Machine-readable error code, screaming snake case. example: NOT_FOUND message: type: string required: - code - message required: - error DecisionExplanation: type: object description: ADR-043 decision explanation properties: decision_id: type: string timestamp: type: string format: date-time policy_matches: type: array description: Always an array (`[]` when empty) items: type: object properties: policy_id: type: string policy_name: type: string action: type: string risk_level: type: string allow_override: type: boolean policy_description: type: string matched_rules: type: array description: Per-rule match detail; omitted when unavailable items: type: object properties: policy_id: type: string rule_id: type: string rule_text: type: string matched_on: type: string decision: type: string description: 'Verdict echoed verbatim from the audit row''s policy_decision column (not normalized), so historical spellings can appear ' reason: type: string risk_level: type: string override_available: type: boolean description: 'Always false from v11.0.0: session overrides are retired (PRD v11 §1.5, #4252), so explain offers none. ' override_existing_id: type: string description: 'Not set from v11.0.0: explain offers no override, and the step gate no longer reads one (#4252). ' historical_hit_count_session: type: integer description: Rolling-24h hit count for the first matched policy by this user policy_source_link: type: string tool_signature: type: string policy_version_at_decision: type: integer description: Policy version recorded at decision time; omitted when unknown latest_policy_version: type: integer description: Current head version of the first matched (static) policy context: type: object additionalProperties: type: string context_truncated: type: boolean DecisionListItem: type: object properties: decision_id: type: string timestamp: type: string format: date-time decision: type: string description: Canonical verdict enum: - allowed - blocked - redacted - needs_approval - error policy_id: type: string tool_signature: type: string context: type: object additionalProperties: type: string description: Decision context, truncated to at most 5 keys (sorted) transfer_basis: type: string description: UU PDP Pasal 56 transfer basis; Enterprise cross-border rows only data_residency: type: string description: ISO 3166-1 alpha-2 destination country; Enterprise only ErrorResponse: type: object description: 'The FLAT error envelope: `{success, error}`. This is what `sendErrorResponse` emits, which is the orchestrator''s dominant error writer (240 call sites), so it is the shape of every error from the core request, audit, plan, workflow, execution and connector surfaces. It is one of THREE error SHAPES this document describes. See `CodedErrorResponse` and `TripletErrorResponse` for the other two, and the note on `components.responses` for why there is more than one. `LLMProviderAPIError` is a code-constrained refinement of the coded shape, not a fourth shape. This paragraph said "TWO" until issue #3941. `TripletErrorResponse` was added by the #3901 reconciliation and this sentence was not updated with it, so the document undercounted its own families — which is the same defect one level up as the operations that named the wrong one. ' properties: success: type: boolean example: false error: type: string description: Human-readable message. There is no machine-readable code on this envelope. required: - success - error securitySchemes: basicAuth: type: http scheme: basic description: OAuth2-style client credentials (clientId:clientSecret) BearerAuth: type: http scheme: bearer bearerFormat: JWT description: Enterprise JWT token (see /scripts/generate-jwt.sh) x-refined-from: - axonflow-orchestrator-api.yaml - axonflow-orchestrator-openapi.yml