explain_why
explain_why answers "where did that decision come from?" It explains three different kinds of thing, and you tell it which by the kind discriminator:
kind | Identifier | What it explains |
|---|---|---|
decision | decision_id | A published rule — one instruction serving row, with the deterministic explanation text, the policy and version behind it, and whether the row is still current. |
authorization | authorization_id | A recorded authorize_use evaluation — the inputs it evaluated, the decision it reached, the serving rows it cited, and who asked. |
validation | validation_id | A recorded validate_query_context evaluation — the verdict, the per-finding summary, and a privacy-safe query fingerprint. |
It is an operationally pure read: it explains what already happened and records nothing. Protocol alias: explain-why.
When to useDirect link to When to use
Chain it after any decision-bearing answer:
authorize_usereturns both adecision_id(the winning published rule) and anauthorization_id(that whole evaluation). Explain the first when you want the rule; explain the second when you want the call — including which purpose and which actor role it ran under.validate_query_contextreturns avalidation_idand afinding_idper finding, plus adecision_idon every cited instruction.get_decision_context,inspect_governance_rules, and validation findings all cite instruction records that carry their owndecision_id.
Use it when a human asks "why was this denied", when writing an audit trail, or when a stored identifier from an earlier run needs re-checking against what is published now. Ledger identifiers stay resolvable in later sessions.
InputDirect link to Input
The input is a discriminated union on kind. All three shapes are strict: unknown keys are rejected, and you must supply exactly the identifier that matches the kind.
{ "kind": "decision", "decision_id": "<uuid>" }
{ "kind": "authorization", "authorization_id": "<uuid>" }
{ "kind": "validation", "validation_id": "<uuid>" }
Passing decision_id with kind: "authorization" — or any other mismatch — is invalid_parameters. There is no shared identifier field across kinds, deliberately: a serving-row id and a ledger-record id are different things and confusing them would return the wrong evidence.
An identifier that cannot be resolved in this workspace returns the typed asset_not_found error. Unknown, cross-workspace, and (for ledger records) foreign-token references are all indistinguishable.
Output: kind: "decision"Direct link to output-kind-decision
Discriminated on state. A resolvable row always has an explanation, so not_enough_published_state does not occur here.
| Field | Description |
|---|---|
state | answered, or review_required when the cited row is conflicted or is an explicit require_review. |
reason_code | On review_required only: conflicted_published_state or decision_requires_review. |
explanation | Deterministic plain text (≤4,000 chars) composed only from allow-listed tokens already on the 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. |
record | The full cited instruction record, including its validity window (effective_from / effective_until) and conflict fields. |
current | true when the row belongs to the current deployment publication, false when it is historical. Historical rows stay explainable; the label tells you which you got. |
publication | The publication_id and published_at of the publication the row belongs to. |
The explanation follows a fixed shape, for example: Decision 'conditional' on master.public.customers was produced by policy 'Customer 360 transfer guardrails' v1 (instruction 'transfer_governance:spec.transferGovernance', scenario 'residency.cross_border_transfer', current publication): ... — stable enough to log, diff, and show to reviewers.
Output: kind: "authorization"Direct link to output-kind-authorization
This shape is discriminated on kind, not on state — it is a record of an evaluation, not a live answer.
| Field | Description |
|---|---|
kind | "authorization". |
authorization_id | Echo of the identifier. |
answer_state | The typed state of the original answer: answered, review_required, or not_enough_published_state. |
decision | The winning decision, or null on an insufficient-state answer. |
reason_code | The original reason code, or null. |
evaluated | The canonical, redacted inputs: asset, scenario_key (null when unresolved), operation, destination {system, jurisdiction}, consumer_jurisdiction, and purpose_context. Never the free use text. |
evaluated.purpose_context | {purpose_key, purpose_category, registry_version}, or null for a purposeless call or a record written before purpose shipped. |
cited_decision_ids | The serving decision ids the original answer cited, winner first. Each is explainable with kind: "decision". |
satisfied_exception_ids | The server-verified exception ids the evaluation consumed, or null when none were cited. These are server-derived — never client correlation ids. |
provenance | Replay and attribution context — see below. |
The provenance object:
| Field | Description |
|---|---|
provenance.evaluated_at | The evaluation instant, byte-identical to the one the answer reported. |
provenance.evaluator_version | The evaluator build that produced the answer. |
provenance.envelope_version | The evidence envelope version. |
provenance.actor.kind | The token's declared agent kind, or null. |
provenance.actor.name | The actor name recorded at evaluation time. Survives token rotation and deletion. |
provenance.actor.bound_role | The role the evaluation actually used, or null if the token declared none. This is the authoritative record of the actor axis — it is nested here, under provenance.actor, not at the top level. |
provenance.publication | {publication_id, published_at, is_current}, or null when the answer was produced without a publication. published_at is the value recorded at evaluation time, so the evidence survives deletion of the publication; is_current labels current versus historical now. |
Output: kind: "validation"Direct link to output-kind-validation
| Field | Description |
|---|---|
kind | "validation". |
validation_id | Echo of the identifier. |
answer_state | The typed state of the original answer. |
verdict | pass, warn, or fail; null on an insufficient-state answer. |
reason_code | The original reason code, or null. |
statement_kind | The normalized statement kind from the evaluation envelope, or null. |
query_fingerprint | A stable hash of the canonical evaluation envelope. This identifies the evaluation, not the SQL — the same query under a different purpose or actor context has a different fingerprint, by design. |
findings | One summary per finding: finding_id, ref, status, decision, reason_code. |
purpose_context | Top-level here (validation records carry no purpose inside their canonical input): {purpose_key, purpose_category, registry_version} or null. |
provenance | The same shape as the authorization provenance above, including provenance.actor.bound_role. |
Ledger explain output never contains raw SQL or raw use text. Raw content lives only in a protected admin/owner-readable store, and only when your workspace has enabled the corresponding privacy switch (default off).
ExamplesDirect link to Examples
Explaining a published ruleDirect link to Explaining a published rule
{ "kind": "decision", "decision_id": "accef000-0000-4000-8000-000000000010" }
{
"state": "answered",
"explanation": "Decision 'conditional' on master.public.customers was produced by policy 'Customer 360 transfer guardrails' v1 (instruction 'transfer_governance:spec.transferGovernance', scenario 'residency.cross_border_transfer', current publication): transfer-guardrails v1 transfer_governance:spec.transferGovernance → conditional on master.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",
"decision": "conditional",
"decision_reason": "transfer-guardrails v1 transfer_governance:spec.transferGovernance → conditional on master.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" }
]
},
"effective_from": null,
"effective_until": null,
"scope": { "database": "master", "schema": "public", "table": "customers", "column": null },
"provenance": {
"policy_id": "accef000-0000-4000-8000-000000000011",
"policy_version_id": "accef000-0000-4000-8000-000000000012",
"policy_name": "Customer 360 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"
}
}
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.
Explaining a recorded authorizationDirect link to Explaining a recorded authorization
{ "kind": "authorization", "authorization_id": "6f0a1c58-2e4f-4a3a-9c5b-1f70b2a5d9c4" }
{
"kind": "authorization",
"authorization_id": "6f0a1c58-2e4f-4a3a-9c5b-1f70b2a5d9c4",
"answer_state": "answered",
"decision": "allow",
"reason_code": null,
"evaluated": {
"asset": { "database": "master", "schema": "public", "table": "customers", "column": null },
"scenario_key": "purpose.allowed_use",
"operation": null,
"destination": null,
"consumer_jurisdiction": null,
"purpose_context": {
"purpose_key": "analytics.reporting",
"purpose_category": "analytics",
"registry_version": 2
}
},
"cited_decision_ids": ["accef000-0000-4000-8000-000000000020"],
"satisfied_exception_ids": null,
"provenance": {
"evaluated_at": "2026-07-29T10:15:00.000Z",
"evaluator_version": "…",
"envelope_version": 1,
"actor": {
"kind": "coding_agent",
"name": "ci-gate",
"bound_role": "ANALYST"
},
"publication": {
"publication_id": "accef000-0000-4000-8000-000000000001",
"published_at": "2026-07-16T00:00:00.000Z",
"is_current": true
}
}
}
Note what this answers that a rule explanation cannot: which purpose was stated, which role the token was bound to at the time, and whether the publication that produced the answer is still current.
Answer states and errorsDirect link to Answer states and errors
kind: "decision" returns answered or review_required. The two ledger kinds are not answer-state shaped at all — they report the original answer's state in answer_state.
Failures stay outside the answer model:
asset_not_found— the identifier does not resolve in this workspace, or the ledger record belongs to a different token. All causes are indistinguishable.invalid_parameters— a malformed input: a non-UUID, an unknown key, or an identifier that does not match the statedkind.
Limits and notesDirect link to Limits and notes
- The tool explains exactly one record per call. A conflicted answer's other sources each have their own
decision_id. - Historical
decision_idvalues remain resolvable after a new publication goes live; checkcurrentbefore treating an explanation as the live answer, and re-run the original tool for the current verdict. - Ledger records are retention-managed. A purged record is
asset_not_foundlike any other unresolvable id. - Every accepted invocation consumes one metered MCP call; a schema-rejected input (
invalid_parameters) is not metered.
RelatedDirect link to Related
authorize_use— the source ofdecision_idandauthorization_idvalidate_query_context— the source ofvalidation_idandfinding_idget_decision_context— every applicable decision for an asset, each with adecision_id- Typed answer states and Deployments
- MCP tool schemas — full input/output schemas