Skip to main content

explain_why

explain_why answers "where did that decision come from?" Give it the decision_id from a prior answer and it returns the cited instruction record in full — the deterministic explanation text, the policy and version that produced it, the resolution paths that put the asset in scope, and whether the record belongs to the current deployment publication or a historical one. It is read-only and advisory: it explains a decision your published policies produced, it does not re-decide or enforce anything. Protocol alias: explain-why.

When to useDirect link to When to use

Chain it after any decision-bearing answer. authorize_use surfaces the winning instruction's decision_id at the top level, and every instruction record cited by get_decision_context, inspect_governance_rules, and validate_query_context findings carries its own decision_id — so an agent can go from any verdict to its evidence without the workspace app. Use it when a human asks "why was this denied," when writing an audit trail, or when a stored decision_id from an earlier run needs to be re-checked against what is currently published.

InputDirect link to Input

The input is discriminated on kind — exactly one of the two shapes:

FieldTypeRequiredDescription
kindstringYes"decision" to explain an instruction decision; "validation" is accepted by the schema but answers not_supported.
decision_idstring (UUID)With kind: "decision"The serving-row UUID of the instruction decision to explain, taken from a prior answer.
validation_idstring (UUID)With kind: "validation"Reserved; no validation-record read surface exists.

Both shapes are strict — unknown keys are rejected. A decision_id that cannot be resolved in this workspace (including any cross-tenant ID) returns the typed asset_not_found error; unknown and cross-tenant references are indistinguishable.

OutputDirect link to Output

There are two answer states here — a resolvable decision always has an explanation, so not_enough_published_state does not occur. Both states carry the same evidence fields:

FieldDescription
explanationDeterministic plain text (at most 4,000 chars) composed only from allow-listed tokens already on the cited row: the decision value, the normalized identifier path, the policy name and version, the instruction key, the scenario key, and the row's own decision reason. Query text, policy source, and free use text never appear.
recordThe full cited instruction record: decision_id, instruction key and family, scenario key and lane, category, priority, weight, enforcement mode, decision and reason, extracted parameters, resolved scope, policy provenance, resolution paths, and conflict fields.
currenttrue when the cited row belongs to the current deployment publication, false when it is historical. Historical rows remain explainable; the label tells you which you got.
publicationThe publication_id and published_at of the publication the row belongs to.

The explanation follows a fixed shape, for example: Decision 'conditional' on acmecloud_demo.public.customers was produced by policy 'AcmeCloud transfer guardrails' v1 (instruction 'transfer_governance:spec.transferGovernance', scenario 'residency.cross_border_transfer', current publication): ... — stable enough to log, diff, and show to reviewers.

Answer statesDirect link to Answer states

StateReason codesWhen
answeredThe cited row is a settled decision; the explanation and record stand on their own.
review_requiredconflicted_published_state, decision_requires_reviewThe cited row is conflicted (the record's conflicts_with entries name every opposing instruction and its decision), or the row's decision is an explicit require_review. The explanation and record are still returned — the state tells you not to treat it as settled.

Failures stay outside the answer model: asset_not_found for an unresolvable decision_id, not_supported for kind: "validation", and invalid_parameters for a malformed input such as a non-UUID.

ExampleDirect link to Example

Explaining the winning decision from the AcmeCloud transfer authorization:

{ "kind": "decision", "decision_id": "accef000-0000-4000-8000-000000000010" }
{
"state": "answered",
"explanation": "Decision 'conditional' on acmecloud_demo.public.customers was produced by policy 'AcmeCloud transfer guardrails' v1 (instruction 'transfer_governance:spec.transferGovernance', scenario 'residency.cross_border_transfer', current publication): acme-transfer v1 transfer_governance:spec.transferGovernance → conditional on acmecloud_demo.public.customers",
"current": true,
"record": {
"decision_id": "accef000-0000-4000-8000-000000000010",
"instruction_key": "transfer_governance:spec.transferGovernance",
"instruction_family": "transfer_governance",
"scenario_key": "residency.cross_border_transfer",
"scenario_lane": "platform",
"category": "advisory",
"priority": 2,
"weight": 1,
"enforcement_mode": "enforce",
"decision": "conditional",
"decision_reason": "acme-transfer v1 transfer_governance:spec.transferGovernance → conditional on acmecloud_demo.public.customers",
"parameters": {
"defaultEffect": "conditional",
"rules": [
{
"operations": ["export"],
"destinationSystems": ["SALESFORCE"],
"destinationJurisdictions": ["US"],
"consumerJurisdictions": ["EU"],
"effect": "conditional",
"requiresApproval": true,
"requiresAnonymization": true,
"requiredRole": "PRIVACY_ADMIN"
},
{ "destinationSystems": ["ADS_PLATFORM"], "effect": "deny" },
{ "destinationSystems": ["EXTERNAL_LLM_VENDOR"], "effect": "deny" }
]
},
"scope": { "database": "acmecloud_demo", "schema": "public", "table": "customers", "column": null },
"provenance": {
"policy_id": "accef000-0000-4000-8000-000000000011",
"policy_version_id": "accef000-0000-4000-8000-000000000012",
"policy_name": "AcmeCloud transfer guardrails",
"version_number": 1
},
"primary_resolution_source": "selector",
"resolution_paths": [{ "source": "selector", "ref": null }],
"conflict_status": "none",
"conflict_group_id": null,
"conflicts_with": []
},
"publication": {
"publication_id": "accef000-0000-4000-8000-000000000001",
"published_at": "2026-07-16T00:00:00.000Z"
}
}

Note the record's decision is conditional — the published row's collapsed verdict over all authored transfer rules — while its parameters carry every authored rule verbatim, so the full destination matrix is auditable from the explanation. Keys inside parameters are the authored policy DSL, the documented exemption from the snake_case wire convention.

Limits and notesDirect link to Limits and notes

  • Explanations cite the published serving row. For destination-aware transfer answers, the destination-specific verdict is a read-time derivation of authorize_use; explain_why explains the underlying published row, and the authorize response's reason and conditions are the explanation surface for the per-destination result.
  • kind: "validation" is intentionally unsupported — validation runs are not stored as explainable records. To explain a validation finding, use the decision_id on any instruction it cited.
  • Historical decision_id values remain resolvable after a new publication goes live; check current before treating an explanation as the live answer, and re-run the original tool for the current verdict.
  • The tool explains exactly one record per call; a conflicted answer's other sources each have their own decision_id to explain.
  • Every accepted invocation consumes one metered MCP call; a schema-rejected input (invalid_parameters) is not metered.