Skip to main content

Decision Record

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

AxonFlow records every policy decision (allow / deny / require_approval) the platform makes, and exposes a slim listing surface alongside the per-decision explainability endpoint. Together they form the decision-oriented execution record — enough structure for an operator, another agent, or future-you to reconstruct why a workflow's outcome changed.

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 whether an override could unblock it.

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=deny" \
-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": "deny",
"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).
decisionallow | deny | require_approvalExact-match the decision outcome.
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 (allow/deny/require_approval), 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