validate_query_context
validate_query_context answers whether a SQL query is safe to run under your published governance. The server parses the SQL, resolves every referenced table, evaluates each reference against the current deployment publication's deployed instruction decisions, and returns a pass / warn / fail verdict with a per-reference finding for each table. It never executes, rewrites, or blocks the query, and it never changes your governance. Protocol alias: validate-query-context.
Like authorize_use, it is not side-effect-free: every answer is recorded as a durable, citable validation record before it is returned. See Recorded evidence.
When to useDirect link to When to use
Call it as a pre-flight check whenever an agent is about to run or ship SQL — an analytics query, a pipeline transformation, a query embedded in code under review. It complements authorize_use: validate when you hold actual SQL and want every referenced asset checked at once, authorize when the question is a purpose against one asset. Stating your intent via scenario_key or use makes the verdict intent-aware; stating purpose_key makes it purpose-aware; adding transfer context makes transfer rules answer for a specific destination.
InputDirect link to Input
| Field | Type | Required | Description |
|---|---|---|---|
sql | string | Yes | The query text (1–100,000 chars). |
default_database | string | No | Database used to qualify unqualified table identifiers. |
default_schema | string | No | Schema used to qualify unqualified table identifiers. |
scenario_key | string | No | Canonical scenario key stating the query's intent. |
use | string | No | Free-text intent (1–2,000 chars), mapped deterministically to one canonical scenario. |
purpose_key | string | No | A registry purpose such as research.general or commercial.general. Decision-bearing — see Purpose. Exact match only. |
data_access_context | object | No | {as_of: <ISO-8601 timestamp>} for date-of access-window evaluation. Use the same context as the authorization call. |
on_behalf_of | string | No | Who the agent is acting for. Audit metadata only, decision-neutral, never echoed back. |
operation | string | No | Transfer operation token, for example export. |
destination | object | No | Transfer destination; at least one of system or jurisdiction when present. |
destination.system | string | No | Destination system token. |
destination.jurisdiction | string | No | Destination jurisdiction token. |
consumer_jurisdiction | string | No | Jurisdiction of the data consumer. |
Inputs are strict — unknown keys are rejected. There is no role input: the evaluated actor role comes only from the verified token's bound_role. There is also no satisfied_conditions here, by design — verifying a granted exception is an authorize_use surface, and rewriting a query never certifies anonymization.
SQL that cannot be parsed returns a typed query_parse_failed error, and a table identifier that cannot be fully qualified returns query_unresolved_identifier (provide default_database and default_schema); in both cases the query was not validated — parse problems are never converted into an empty pass. An intent that cannot be mapped to exactly one canonical scenario returns the typed scenario_unresolved answer rather than a guess; when transfer context is present and the use text maps to nothing, the intent defaults to residency.cross_border_transfer.
PurposeDirect link to Purpose
This tool runs the same purpose evaluation as authorize_use, so the same SQL can validate differently under different purposes. The full semantics are on the Purpose keys page; the parts that bite here:
- Purpose is decision-bearing only where a
usage_guidancerule participates in a reference's verdict. Where none does, purpose changes nothing. - A matching prohibition denies; a matching permitted entry can keep
allow; an unmatched purpose, an unmapped legacy authored string, or an absentpurpose_keyyieldsrequire_reviewon those rules — which raises the verdict. - Purpose never erases another family's restriction. A mask, a deny, or a role restriction on a referenced table still drives the verdict.
A non-registry purpose_key is invalid_parameters; the call never runs.
Proving an access windowDirect link to Proving an access window
For an applicable access_window instruction, validation requires a provable
lower bound on the policy's exact time_column. A narrower bound passes; an
absent or broader bound fails. Ambiguous expressions, OR, NOT, and bounds
on the wrong column also fail closed. Rolling rules use the server's
evaluation instant; date-of rules use data_access_context.as_of. The server
does not approximate time arithmetic or trust a client-asserted result.
OutputDirect link to Output
The response is one of the three typed answer states, always with advisory: true. All three states carry the evaluation-provenance fields and the ledger id.
Common to every state:
| Field | Description |
|---|---|
advisory | Always true. |
validation_id | The durable ledger record for this evaluation — pass to explain_why as {"kind": "validation", "validation_id": ...}. |
evaluated_at | RFC 3339 UTC instant, minted once per call. Validity windows are filtered at this instant. |
evaluated_purpose | {purpose_key, purpose_category, registry_version}, or null. |
evaluated_actor_role | The token's bound role, or null. |
Both answered and review_required carry the closed verdict, the findings, and publication provenance:
| Field | Description |
|---|---|
verdict | pass (only allow or log-only effects, or no table references at all), warn (masking, review, retention, or conditional obligations apply, a reference is conflicted, or a reference has no usable published instruction state), or fail (a referenced asset is effectively denied). |
findings | One entry per referenced table, in reference order. |
publication | Current-publication provenance; null only for the zero-reference pass, where nothing was read. |
Each finding contains a server-minted finding_id, the table ref (column is always null — findings are table-scoped), a status, and the evidence:
status: "evaluated"— the reference was resolved and judged.decisionis the effective decision over the participating rows,decision_reasonis the winner's reason, andinstructionslists every participating instruction record, ranked winner first, each with its owndecision_idforexplain_whychaining and its own validity window. A governed reference where nothing published applies to this query's shape or intent is an explicit evaluated finding withdecision: nulland no instructions — a pass contribution.status: "not_enough_published_state"— the reference cannot be judged.reason_code: "no_published_instruction_state"means there is no published instruction state at all (or the table is outside the current catalog);reason_code: "not_currently_effective"means rules exist for the reference but none is inside its validity window atevaluated_at. Either way the finding contributeswarn; it is never a silent pass.- A conflicted reference carries the full
conflictgroup with every opposing source anddecision: null.
Which rows participate in a reference's verdict is intent- and column-aware:
- Column-level rows participate only when the query references that column or projects
*. Column extraction is conservative and over-inclusive by design: every identifier that is not a keyword, function name, or table reference counts as a possible column, and*counts only in projection positions — so an alias that shadows a governed column makes the verdict more cautious, never less. - Table-level rows participate when they match the stated intent scenario. Without intent, only always-applicable read controls (
access.read) participate — purpose, AI, and retention scenarios encode intents an unqualified query cannot assert. - Transfer-governance rows participate when transfer context is present, evaluated per destination exactly as in
authorize_use.
The per-reference verdict aggregates over every participating row — a participating deny fails the query even when a more specific column-level row outranks it as the citation winner. Ranking follows the standard order: specificity, then priority, then restrictiveness (deny > mask_full > mask_partial > require_review > conditional > retain > log_only > allow), then a stable key.
Answer statesDirect link to Answer states
| State | Reason codes | When |
|---|---|---|
answered | — | Every reference evaluated without conflict or review escalation; the verdict and findings stand on their own. |
review_required | conflicted_published_state, decision_requires_review | Some reference's applicable rows conflict (that finding carries all opposing sources), or some reference's effective decision is an explicit require_review. Findings and the verdict are still returned, plus next_actions. |
not_enough_published_state | no_current_publication, scenario_unresolved | The query references tables but no current publication exists, or the stated intent could not be mapped to a canonical scenario. |
Reference-level gaps surface inside findings as not_enough_published_state findings — including not_currently_effective, where rules exist but none is in force at evaluated_at. Only a missing publication or unresolved intent escalates to a whole-query not_enough_published_state answer.
Recorded evidenceDirect link to Recorded evidence
Every answer is written to the decision ledger before it returns, and the write is awaited: if it fails, the call fails with internal_error rather than returning an answer nobody can cite.
The record holds the verdict, a per-finding summary, the recorded purpose context, an immutable actor snapshot, and a privacy-safe query fingerprint. It never holds your SQL. Raw SQL is stored only in a protected admin/owner-readable table, and only when your workspace enables the corresponding privacy switch (default off).
One property to internalize: the fingerprint identifies the evaluation, not the SQL. The same query text under a different purpose or a different actor context produces a different fingerprint, by design — so do not use it as a query-text hash or a cache key for the SQL alone.
Retrieve the record any time with explain_why {"kind": "validation", "validation_id": "..."}.
ExampleDirect link to Example
A query joining a governed table to an ungoverned one, with a stated intent and a stated purpose:
{
"sql": "SELECT c.customer_name, c.email FROM customers c JOIN legacy_customer_backup b ON b.customer_id = c.customer_id",
"default_database": "master",
"default_schema": "public",
"scenario_key": "purpose.allowed_use",
"purpose_key": "analytics.reporting"
}
The governed reference warns on the masked email column; the ungoverned reference is an explicit gap:
{
"state": "answered",
"advisory": true,
"verdict": "warn",
"validation_id": "9c2f7b10-4d3e-4a58-8b7c-51e0a6d2f983",
"evaluated_at": "2026-07-29T10:15:00.000Z",
"evaluated_purpose": {
"purpose_key": "analytics.reporting",
"purpose_category": "analytics",
"registry_version": 2
},
"evaluated_actor_role": null,
"findings": [
{
"finding_id": "1a5c3e77-9b02-4c6d-a1f8-64e2b7c0d915",
"ref": { "database": "master", "schema": "public", "table": "customers", "column": null },
"status": "evaluated",
"reason_code": null,
"decision": "mask_partial",
"decision_reason": "email-masking v1 masking:spec.accessControl.masking → mask_partial on master.public.customers.email",
"instructions": [
{
"decision_id": "accef000-0000-4000-8000-000000000002",
"instruction_key": "masking:spec.accessControl.masking",
"instruction_family": "masking",
"scenario_key": "masking.display",
"scenario_lane": "platform",
"category": "enforceable",
"priority": 2,
"weight": 1,
"enforcement_mode": "enforce",
"decision": "mask_partial",
"decision_reason": "email-masking v1 masking:spec.accessControl.masking → mask_partial on master.public.customers.email",
"parameters": { "type": "partial", "exemptRoles": ["DATA_STEWARD", "PRIVACY_ADMIN"] },
"effective_from": null,
"effective_until": null,
"scope": { "database": "master", "schema": "public", "table": "customers", "column": "email" },
"provenance": {
"policy_id": "accef000-0000-4000-8000-000000000003",
"policy_version_id": "accef000-0000-4000-8000-000000000004",
"policy_name": "Customer 360 email masking (taxonomy)",
"version_number": 1
},
"primary_resolution_source": "taxonomy",
"resolution_paths": [{ "source": "taxonomy", "ref": "pii.contact.email" }],
"conflict_status": "none",
"conflict_group_id": null,
"conflicts_with": []
},
{
"decision_id": "accef000-0000-4000-8000-000000000020",
"instruction_key": "usage_guidance:spec.usage.permittedUses:permitted",
"instruction_family": "usage_guidance",
"scenario_key": "purpose.allowed_use",
"scenario_lane": "human",
"category": "advisory",
"priority": 2,
"weight": 1,
"enforcement_mode": "enforce",
"decision": "allow",
"decision_reason": "Purpose `analytics.reporting` is named by this policy's permitted-uses list (policy Customer 360 customer use guardrails).",
"parameters": { "kind": "permitted", "uses": ["analytics", "reporting", "support"] },
"effective_from": null,
"effective_until": null,
"scope": { "database": "master", "schema": "public", "table": "customers", "column": null },
"provenance": {
"policy_id": "accef000-0000-4000-8000-000000000006",
"policy_version_id": "accef000-0000-4000-8000-000000000007",
"policy_name": "Customer 360 customer use guardrails",
"version_number": 1
},
"primary_resolution_source": "selector",
"resolution_paths": [{ "source": "selector", "ref": null }],
"conflict_status": "none",
"conflict_group_id": null,
"conflicts_with": []
}
],
"conflict": null
},
{
"finding_id": "7d4b1f92-3c05-4e8a-9f11-2b8c6a0e5d47",
"ref": { "database": "master", "schema": "public", "table": "legacy_customer_backup", "column": null },
"status": "not_enough_published_state",
"reason_code": "no_published_instruction_state",
"decision": null,
"decision_reason": null,
"instructions": [],
"conflict": null
}
],
"publication": {
"publication_id": "accef000-0000-4000-8000-000000000001",
"published_at": "2026-07-16T00:00:00.000Z"
}
}
The same query selecting only region and aggregated arr would pass — the masked column row participates only when email is actually referenced.
Note the substituted decision_reason on the usage-guidance row: it names the purpose that was evaluated. Drop purpose_key from that request and the same row answers require_review instead, escalating the whole answer to review_required — see the purpose-missing example.
Limits and notesDirect link to Limits and notes
- The tool validates; it never runs the query, and a
passis advisory — enforcement stays in your systems. - Only the current deployment publication is read; drafts and unpublished changes never affect the verdict.
SELECT 1and other queries with zero table references are the one legitimate zero-findings pass, returned withpublication: null.- Column extraction works from the SQL text, not your live schema, so it can only over-include; a governed column the query truly reads is never missed.
- Findings are table-scoped; there is no per-column finding row — column evidence appears through the participating column-level instructions.
- Revalidating rewritten SQL never satisfies an
anonymize_firstcondition. A validation proves only what the analyzer can observe — references, columns, statement shape. It does not certify anonymization, and that query's verdict stands on its own. Satisfying that gate requires an approval-reference exception through the request lane. on_behalf_ofis recorded as a presence flag always, and as raw text only under your workspace's raw-text privacy switch. It is never returned in any answer.- Malformed input — including a non-registry
purpose_key— isinvalid_parametersand is not metered; every accepted invocation, including one that ends in a parse error, consumes one metered MCP call.
RelatedDirect link to Related
authorize_use— purpose-level authorization for one asset- Purpose keys — the complete
purpose_keysemantics explain_why— evidence for adecision_idor avalidation_idinspect_data_meaning— masking and classification facts behind warn verdicts- Typed answer states and Scenarios
- MCP tool schemas — full input/output schemas