Skip to main content

Decision Record

Reference page. This documents the two read APIs for inspecting past decisions: the list surface and the per-decision explain surface. To understand the decisions themselves first, see Decision Explainability and Audit Logging.

AxonFlow records every policy decision the platform makes and exposes a slim listing surface alongside the per-decision explainability endpoint. On these read surfaces a decision verdict is one of allowed, blocked, redacted, needs_approval, or error. Together they form the decision-oriented execution record: enough structure for an admin, another agent, or future-you to reconstruct why a workflow's outcome changed.

In v11 every decision on these surfaces comes from the policy decision engine, which decides every enforcement plane. A decision it authored names its matched policies by their v11 identifier (for example corpus:static_policies:sys__sqli__drop__table where v10.x recorded sys_sqli_drop_table), and records the policy bundle it was decided under.

This is different from a raw audit log. Audit logs are a chronological dump; the decision record is the control-plane reasoning trail: which policy fired, at which version, with what risk level, and, for a v10.x decision, whether a session override could unblock it.

Available on platform v7.7.0+. The list endpoint and policy_version_at_decision field require v1.1.

Role-scoped reads (platform v9.10.0+, Enterprise)

On fleet deployments these read surfaces are scoped by the caller's validated per-user token: an admin-role token reads the full tenant, a developer-role token reads only its own rows, and a caller with no per-user identity reads zero rows (a behavior change — previously the shared tenant credential read everything).

The two surfaces​

SurfacePurposeWhen to use
GET /api/v1/decisions (list)Show me the most recent decisions for my tenant"What just got blocked?" UX in plugins, dashboards, alerts.
GET /api/v1/decisions/:id/explain (single)Explain this specific decisionAfter a block, when the user wants to understand or appeal.

Both surfaces share the same security model: the caller's X-Tenant-ID is enforced at the SQL WHERE level, not as a post-fetch comparison. There is no enumeration oracle.

Listing recent decisions​

curl -u "$CLIENT_ID:$CLIENT_SECRET" \
"https://your-platform/api/v1/decisions?limit=20&decision=blocked" \
-H "X-Tenant-ID: $TENANT_ID"

Response (slim summary, NOT the full DecisionExplanation):

{
"decisions": [
{
"decision_id": "dec_wf123_step4",
"timestamp": "2026-05-07T12:16:36Z",
"decision": "blocked",
"policy_id": "sys_sqli_drop_table",
"tool_signature": "postgres.query"
}
]
}

To get the full reason and matched-rule detail for any one of these, call the explain endpoint with the decision_id.

Filters​

ParamTypeEffect
sinceRFC3339 timestampOnly decisions after this time. Defaults to the start of your tier's window (see below).
decisionallowed | blocked | redacted | needs_approval | errorExact-match the decision outcome. Must be a canonical value; legacy allow/deny are rejected with 400.
policy_idstringExact-match a single policy_id within policy_details.policy_ids.
tool_signaturestringExact-match the tool the decision was scoped to (e.g. postgres.query, slack.send).
limitintegerCapped per tier (see below).

Tier-gated window and page size​

The list surface is intentionally tiered: Free users see the most recent block, Pro users get the full audit-retention window so they can actually browse the decision record over time.

TierWindow (lookback)Max page size
SaaS Freelast 24h5
SaaS Pro ($9.99 / 90d)last 30d100
Self-host Communitylast 24h5
Self-host Evaluationlast 14d100
Self-host Enterprisefull audit retention1000

Note: the listing window is bounded by your tier's audit retention. Pro's "last 30d" matches Pro's 30-day audit retention exactly; the listing surface cannot return decisions older than the audit row itself.

Hitting the page-size cap​

When a Free tier user requests limit > 5 (or sees more than 5 decisions in the window), the response is 429 with the standard upgrade envelope:

{
"error": "decision list page limit reached for your tier",
"limit_type": "decision_list_size",
"tier": "Free",
"upgrade": {
"wording": "Free returns the most-recent 5 decisions in the last 24h. Pro returns 100 across 30 days.",
"compare_url": "https://getaxonflow.com/pro",
"buy_url": "https://buy.stripe.com/..."
}
}

Plugins and host CLIs surface this as a clear "upgrade to Pro to see your full block history" prompt rather than silently truncating. The envelope shape matches the rate-limit-429 envelope returned by the daily-quota cap, so a plugin can use a single parse path for both.

Explaining a specific decision​

For the full structured explanation of any decision_id (matched policies, matched rules, risk level, override availability, historical hit count), see /docs/governance/explainability.

The explain endpoint surfaces two V1.1 fields that the list endpoint omits for brevity:

  • policy_version_at_decision — the version of the matched policy that was applied when this decision was made.
  • latest_policy_version — the current head of the same policy.

Together these answer the "why is this blocked NOW that wasn't 2 days ago?" question without a separate diff endpoint:

  1. Operator sees policy_version_at_decision = v3 on a recent block.
  2. Operator sees latest_policy_version = v5 on the same response.
  3. Operator now knows the policy has changed since the user's last successful run.

Rule-level diff between two policy versions is on the V1.2 roadmap (GET /api/v1/decisions/:id/policy-version-diff).

SDK methods​

All five SDKs ship explain_decision today (axonflow-sdk-go, axonflow-sdk-python, axonflow-sdk-typescript, axonflow-sdk-java, axonflow-sdk-rust). The list_decisions companion lands across all five SDKs as part of the V1.1 release train.

SDKMethodModule
Goclient.ListDecisions(ctx, opts) ([]DecisionSummary, error)top-level
Pythonawait client.list_decisions(opts) -> list[DecisionSummary]axonflow.decisions
TypeScriptawait client.listDecisions(opts): Promise<DecisionSummary[]>top-level
Javaclient.listDecisions(opts): List<DecisionSummary>top-level
Rustclient.list_decisions(opts).await -> Vec<DecisionSummary>top-level

Each follows the cross-SDK naming convention locked in ADR-043 §"SDK parity".

MCP tool parity​

The list_recent_decisions MCP tool is served by the agent's MCP server at /api/v1/mcp-server, alongside the existing explain_decision tool. Every plugin (OpenClaw, Claude Code, Cursor, Codex) gets it for free — no per-plugin registration needed.

Arguments: an optional since (RFC3339), decision (canonical allowed/blocked/redacted/needs_approval/error), limit (capped per tier). Returns the same DecisionSummary[] shape as the HTTP endpoint.

Authorization​

Same model as explain_decision: the caller's X-Tenant-ID is required and enforced at the SQL WHERE level. Cross-tenant listing returns 403; an unauthenticated request returns 401.

Within a tenant, the listing returns every decision the tenant has authority over (matching the same scope as audit search).

See also​

Rollout Checklist​

Use this page as one layer of the broader governance rollout: