Skip to main content

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

FieldTypeRequiredDescription
sqlstringYesThe query text (1–100,000 chars).
default_databasestringNoDatabase used to qualify unqualified table identifiers.
default_schemastringNoSchema used to qualify unqualified table identifiers.
scenario_keystringNoCanonical scenario key stating the query's intent.
usestringNoFree-text intent (1–2,000 chars), mapped deterministically to one canonical scenario.
purpose_keystringNoA registry purpose such as research.general or commercial.general. Decision-bearing — see Purpose. Exact match only.
data_access_contextobjectNo{as_of: <ISO-8601 timestamp>} for date-of access-window evaluation. Use the same context as the authorization call.
on_behalf_ofstringNoWho the agent is acting for. Audit metadata only, decision-neutral, never echoed back.
operationstringNoTransfer operation token, for example export.
destinationobjectNoTransfer destination; at least one of system or jurisdiction when present.
destination.systemstringNoDestination system token.
destination.jurisdictionstringNoDestination jurisdiction token.
consumer_jurisdictionstringNoJurisdiction 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_guidance rule 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 absent purpose_key yields require_review on 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:

FieldDescription
advisoryAlways true.
validation_idThe durable ledger record for this evaluation — pass to explain_why as {"kind": "validation", "validation_id": ...}.
evaluated_atRFC 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_roleThe token's bound role, or null.

Both answered and review_required carry the closed verdict, the findings, and publication provenance:

FieldDescription
verdictpass (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).
findingsOne entry per referenced table, in reference order.
publicationCurrent-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. decision is the effective decision over the participating rows, decision_reason is the winner's reason, and instructions lists every participating instruction record, ranked winner first, each with its own decision_id for explain_why chaining and its own validity window. A governed reference where nothing published applies to this query's shape or intent is an explicit evaluated finding with decision: null and 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 at evaluated_at. Either way the finding contributes warn; it is never a silent pass.
  • A conflicted reference carries the full conflict group with every opposing source and decision: 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

StateReason codesWhen
answeredEvery reference evaluated without conflict or review escalation; the verdict and findings stand on their own.
review_requiredconflicted_published_state, decision_requires_reviewSome 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_stateno_current_publication, scenario_unresolvedThe 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 pass is advisory — enforcement stays in your systems.
  • Only the current deployment publication is read; drafts and unpublished changes never affect the verdict.
  • SELECT 1 and other queries with zero table references are the one legitimate zero-findings pass, returned with publication: 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_first condition. 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_of is 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 — is invalid_parameters and is not metered; every accepted invocation, including one that ends in a parse error, consumes one metered MCP call.