Skip to main content

Decision Explainability

Available on platform v7.1.0+.

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, 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.

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": "deny",
"reason": "SQL injection patterns detected in query",
"risk_level": "high",
"policy_matches": [
{
"policy_id": "pol-sqli-detector",
"policy_name": "SQL Injection Detector",
"action": "deny",
"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"
}

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.

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 4 SDKs (ADR-043):

Go (v5.4.0+):

exp, err := client.ExplainDecision(ctx, "dec_wf123_step4")
if err != nil { return err }
if exp.OverrideAvailable {
// offer the user a governed override action
}

Python (v6.4.0+):

exp = await client.explain_decision("dec_wf123_step4")
if exp.override_available:
# offer a governed override
pass

TypeScript (v5.4.0+):

const exp = await client.explainDecision("dec_wf123_step4");
if (exp.overrideAvailable) {
// offer a governed override
}

Java (v5.4.0+):

DecisionExplanation exp = axonflow.explainDecision("dec_wf123_step4");
if (exp.isOverrideAvailable()) {
// offer a governed override
}

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 6 pre-existing governance tools.

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.

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:

TierRetentionScope
Community7 daysOwn decisions only
Evaluation30 daysOwn decisions only
Enterprise365 daysAny decision in org
Enterprise+CustomAny decision in org

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