Skip to main content

Decision Explainability

Reference page. The intro explains what an explanation is and why it matters; the rest documents the /explain payload, fields, SDK methods, and limits. For the companion list surface, see Decision Record.

Available on platform v7.1.0+. The V1.1 forensic fields (policy_version_at_decision, latest_policy_version) require platform v1.1.

In v11 the policy decision engine decides every governed request, and a decision it authored names its matched policies by their v11 identifier (for example corpus:static_policies:sys__pii__ssn:warn, where v10.x recorded sys_pii_ssn) and records the digest of the policy bundle it was decided under. See What is recorded.

Every policy decision AxonFlow makes can be explained after the fact. Given a decision_id, the platform returns the matched policies, rule detail, risk level, override availability, the version of the policy that fired the decision, and a rolling 24-hour count of times the same caller hit the same rule.

This closes the "why was this blocked?" gap. A plugin block stops being a dead end — the user or downstream tooling can pull the full context and decide whether to override, escalate, or accept.

Overrides in v11

override_available and override_existing_id describe v10.x session overrides. In v11 a session bypass does not silence the policy decision engine: to change how a decision comes out, change the policy for your organization.

For the listing companion (GET /api/v1/decisions) and the broader decision-oriented execution record framing, see Decision Record.

The shape​

The response payload is frozen per ADR-043. Additive fields may appear in future versions (extra fields are tolerated forward), but existing field names, types, and semantics are stable across minor versions.

{
"decision_id": "dec_wf123_step4",
"timestamp": "2026-04-17T12:00:00Z",
"decision": "blocked",
"reason": "SQL injection patterns detected in query",
"risk_level": "high",
"policy_matches": [
{
"policy_id": "pol-sqli-detector",
"policy_name": "SQL Injection Detector",
"action": "block",
"risk_level": "high",
"allow_override": true,
"policy_description": "Blocks SQL injection patterns using keyword + regex detection"
}
],
"matched_rules": [
{
"policy_id": "pol-sqli-detector",
"rule_id": "sqli-union-select",
"rule_text": "Contains UNION SELECT keyword combination",
"matched_on": "query.sql"
}
],
"override_available": true,
"override_existing_id": "ov-f3a81c...",
"historical_hit_count_session": 3,
"policy_source_link": "https://policies.axonflow/sqli-detector",
"tool_signature": "Bash",
"policy_version_at_decision": 3,
"latest_policy_version": 5
}

Every field except decision_id, timestamp, decision, reason, and policy_matches is optional. Consumers that don't find an expected field should treat the absence as "context not available from this platform version" and continue.

The top-level decision is the canonical verdict (allowed, blocked, redacted, needs_approval, or error). The action inside each policy_matches entry is the matched policy's configured action (block, redact, warn, log, or require_approval), which is a separate vocabulary from the verdict.

V1.1 — Why is this blocked NOW that wasn't 2 days ago?​

The two V1.1 fields — policy_version_at_decision and latest_policy_version — exist to answer the most common decision-archaeology question without requiring an extra diff endpoint or cross-referencing the policy timeline by hand.

FieldTypeRecorded byRead byOmitted when
policy_version_at_decisionintAudit-write path at decision time. Stored in audit_logs.policy_details->>'policy_version'./explain reads back from the same JSONB key.Decision was made before V1.1 (no version was recorded); decision matched a dynamic policy (no version concept).
latest_policy_versionintn/a — looked up at explain time against static_policy_versions./explain does the lookup per first matched policy_id.policy_version_at_decision is omitted; the policy_id has been deleted; the policy was never versioned.

These two fields count versions of a v10.x system policy. A decision made by the v11 engine is decided under a policy bundle, identified by its digest rather than by a per-policy version: the digest on the decision (policy_bundle) is what to compare with the digest your organization has active now.

Reading the two integers together​

The two fields are designed to be consumed as a pair:

policy_version_at_decisionlatest_policy_versionOperator interpretation
33Policy hasn't changed since the decision. The block is "the same rule that's been there all along." Question shifts from "what changed?" to "why is the user trying this now?"
35Policy has changed twice since the decision. Pull /api/v1/system-policies/{policy_id}/versions for v3 vs v5 to see what tightened.
3omittedPolicy was deleted. The block was authoritative at the time, but the rule that fired no longer exists in your tenant.
omittedomittedDecision was either (a) made before V1.1 (no version recorded) or (b) by a dynamic policy. Use the existing reason and matched_rules fields.

Worked example — reviewer response​

A plugin user reports: "I keep getting blocked on postgres.query. This was working last week."

# 1. Find the most recent block on the affected tool
DECISION_ID=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
-H "X-Tenant-ID: $TENANT_ID" \
"https://your-platform/api/v1/decisions?decision=blocked&tool_signature=postgres.query&limit=1" \
| jq -r '.decisions[0].decision_id')

# 2. Pull the explanation
EXP=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
-H "X-Tenant-ID: $TENANT_ID" \
"https://your-platform/api/v1/decisions/$DECISION_ID/explain")

# 3. Read the two version integers
echo "$EXP" | jq '{
policy_id: .policy_matches[0].policy_id,
policy_version_at_decision: .policy_version_at_decision,
latest_policy_version: .latest_policy_version,
reason: .reason
}'
# {
# "policy_id": "sys_sqli_drop_table",
# "policy_version_at_decision": 3,
# "latest_policy_version": 5,
# "reason": "SQL injection patterns detected"
# }

# 4. Diff v3 → v5 in the policy timeline
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
-H "X-Tenant-ID: $TENANT_ID" \
"https://your-platform/api/v1/system-policies/sys_sqli_drop_table/versions" \
| jq '.versions | map(select(.version >= 3 and .version <= 5))'

# Operator now sees what changed between v3 (when the user's previous run succeeded
# under a permissive rule) and v5 (when the rule was tightened). Decide whether to
# revert the policy or escalate (v10.x also offered a session override here).

For a decision made by the v11 engine, the same question is answered by digests. GET /api/v1/typed-policies/active names the digest in force now; if it differs from the decision's policy_bundle, a new version was activated since, and on Enterprise the portal's activation history says who activated it and why. A change to a shipped policy's action for your organization is recorded the same way.

V1.2 — rule-level diff endpoint​

A first-party rule-level diff that takes a decision_id and returns the structured rules_added / rules_removed / rules_changed between policy_version_at_decision and latest_policy_version is on the V1.2 roadmap (GET /api/v1/decisions/:id/policy-version-diff). Until V1.2, compose the existing static-policies version endpoint as shown above — the two integers in V1.1 already give reviewers a complete forensic answer for static-policy decisions.

API​

curl -X GET https://your-platform/api/v1/decisions/{decision_id}/explain \
-u "$CLIENT_ID:$CLIENT_SECRET"

See Decisions API for the full endpoint reference.

SDK usage​

Naming is locked across all 5 SDKs (ADR-043 §"SDK parity"):

// TypeScript (v5.4.0+)
const exp = await client.explainDecision("dec_wf123_step4");
if (exp.overrideAvailable) {
// offer a governed override
}
if (exp.policyVersionAtDecision && exp.latestPolicyVersion !== exp.policyVersionAtDecision) {
// policy moved since the decision, surface the version drift to the user
}

The two V1.1 fields deserialize as additive optional fields in every SDK — pre-V1.1 client builds will silently ignore them, so the SDK→server compatibility surface is unchanged.

MCP tool parity​

The explain_decision MCP tool is served by the agent's MCP server at /api/v1/mcp-server, not by any individual plugin. Every plugin (OpenClaw, Claude Code, Cursor, Codex) points its MCP client at the same agent endpoint, so a single registration on the platform makes the tool available everywhere. Verify with tools/list against the agent:

curl -s -X POST https://your-platform/api/v1/mcp-server \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"1","method":"tools/list"}' \
| jq '.result.tools[] | .name'

Expected output includes explain_decision, create_override, delete_override, list_overrides alongside the pre-existing governance tools. V1.1 adds list_recent_decisions as the listing companion — same tier-throttled behavior as the underlying GET /api/v1/decisions HTTP endpoint.

Arguments: a single decision_id string. Response: the DecisionExplanation shape above, marshaled as a JSON string inside the MCP tools/call result content. See /docs/governance/overrides for the override-creation tools that pair with this one, and /docs/governance/decisions for the listing companion's MCP wrapper.

Authorization​

The caller must either own the decision (the user_email matches) or belong to the same tenant as the decision's originator. Cross-tenant explanation requests return 403.

Admin-level cross-tenant explanation is out of scope for this endpoint — use the audit search API with appropriate admin credentials for that.

Retention​

Explanation is bounded by the tier's audit retention:

TierRetention
Community3 days
Evaluation14 days
Enterprise10 years
Enterprise+10 years

Beyond retention, the endpoint returns 404 — no synthesized "we don't know why anymore" placeholder.

Historical hit count​

historical_hit_count_session counts how many times the same (policy_id, user_email) combination appeared in audit logs within a rolling 24-hour window from the decision's timestamp.

This is intended for at-a-glance "I keep hitting this rule" awareness. Full cross-decision queries go through audit search.

See also​

  • Decision Record — list + explain composed into the decision-oriented execution record framing
  • Decisions API — full endpoint reference for both list and explain
  • Static Policies API — GET .../{id}/versions to read what changed between policy_version_at_decision and latest_policy_version
  • Session Overrides — the v10.x action override_available: true unlocked; in v11 a session bypass does not silence the engine
  • Audit Logging — cross-reference the full lifecycle

Rollout Checklist​

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