Skip to main content

Cookbook

These recipes chain the seven MCP tools into agent workflows you can ship: a governed coding assistant, an export gate, a CI gate, a RAG ingestion gate, and a red-team harness. Every call is read-only and advisory — the answer describes your workspace's published governance state, and your agent or pipeline decides what to do with it.

Every decision-bearing answer is typed, so wire your reactions up front: answered carries a decision you can act on, review_required means published rules exist but a human must decide, and not_enough_published_state means the workspace has not published enough governance to answer — with a reason code naming exactly what is missing. All five recipes follow one rule: only an answered decision drives automation; every other state stops it. See Typed answer states for the full model.

Each recipe is grounded in a runnable notebook from the metatate-examples repository. Snippets use that repo's thin Python client, whose methods map 1:1 onto tool calls, so the same sequences work from any connected client. They assume a workspace serving a current publication; the sample workspace answers every recipe out of the box, which is why the snippets reference acmecloud_demo assets:

def asset(table, column=None):
ref = {"database": "acmecloud_demo", "schema": "public", "table": table}
if column:
ref["column"] = column
return ref

A governance-aware coding assistantDirect link to A governance-aware coding assistant

Give a coding assistant a decision layer: before it writes code against a table, it learns what the data means and whether the intended use is allowed. Chain discover_contextinspect_data_meaningauthorize_use:

governed = client.discover_context()          # what is governed, with scenario keys
facts = client.inspect_data_meaning(asset("customers", "email"))
answer = client.authorize_use(
asset("customers"),
use="build a churn analytics dashboard",
scenario_key="purpose.allowed_use",
)

The meaning call returns facts only — classification, sensitivity, PII flag, masking — never a decision. The authorize call returns state: answered, decision: allow, can_proceed_now: true, and a decision_id the assistant can pass to explain_why as evidence. The same asset under purpose.prohibited_use ("launch a marketing campaign on customer contact data") is an answered deny citing the policy. For a one-call overview of a table, get_decision_context returns every applicable instruction ranked, the effective decision, and the published business context — owner, steward, domain, purpose — so the assistant can name a data owner instead of guessing one.

React by state: on allow, generate the code and keep the decision_id; on deny, refuse and quote the reason; on review_required or not_enough_published_state, say governance is unresolved instead of guessing. Notebook: 01_decision_layer_cookbook.

Transfer governance before an exportDirect link to Transfer governance before an export

Before an export job runs, ask the transfer question for the actual destination. authorize_use accepts operation, destination {system, jurisdiction}, and consumer_jurisdiction, and the server evaluates the authored transfer rules per destination at read time:

answer = client.authorize_use(
asset("customers"),
use="sync approved customer fields to the CRM",
scenario_key="residency.cross_border_transfer",
operation="export",
destination={"system": "SALESFORCE", "jurisdiction": "US"},
consumer_jurisdiction="EU",
)

On the sample workspace this is state: answered, decision: conditional with structured conditions (approval_required, anonymize_first) and can_proceed_now: false; an advertising platform or external LLM vendor destination is a deny; a destination no rule names falls to the policy's authored default effect — nothing is silently allowed. Gate the job on can_proceed_now, hold conditional exports until the conditions are satisfied, and chain explain_why(answer["decision_id"]) into the approval record. Notebook: 03_transfer_governance_before_export.

A CI gate for data and AI changesDirect link to A CI gate for data and AI changes

Stop pull requests that ship queries or AI workflows governance would reject. For each change in the PR's change set, call validate_query_context with the change's SQL and its canonical scenario_key:

answer = client.validate_query_context(
"SELECT customer_name, email FROM customers WHERE region = 'EU'",
scenario_key="purpose.allowed_use",
default_database="acmecloud_demo",
default_schema="public",
)
# answer["verdict"] -> "warn": the query references a masked column

Map verdicts to gates: pass merges, warn becomes required controls ("mask or drop email"), fail blocks. Findings cite the participating instructions per referenced table, so the gate comment names the policy. Two rules keep the gate honest: a governed table with no published instruction state arrives as an explicit not-enough-state finding — fail it in strict pipelines rather than silencing it — and the query_parse_failed / query_unresolved_identifier errors mean the query was not validated, so fail the gate (see Troubleshooting). Notebook 06_ci_gate_for_data_ai_changes packages this as pass / needs_controls / fail gates with reason codes, and its strict runner's exit code blocks the merge.

A pre-ingestion gate for RAG and embeddingsDirect link to A pre-ingestion gate for RAG and embeddings

Nothing should enter a retrieval index or embedding store without a decision. For each candidate corpus, authorize the AI scenario that matches how the data will be used (ai.training, ai.inference, ai.embedding_storage, …) and ingest only on an answered allow:

answer = client.authorize_use(
asset("support_tickets"),
use="fine-tune a support assistant on ticket text",
scenario_key="ai.training",
)
ingest = answer["state"] == "answered" and answer.get("decision") == "allow"

On the sample workspace, training on raw ticket text is a deny, while LLM inference over customer accounts is an allow — the gate keeps the corpus honest in both directions. Treat anything that is not answered-allow as a skip: deny, conditional, review_required, and not_enough_published_state all keep data out of the index until governance says otherwise. Then validate the retrieval query that feeds the index with validate_query_context and require a pass. Notebook: 07_governed_rag_embedding_ingestion_gate.

A red-team evaluation harnessDirect link to A red-team evaluation harness

Prove, repeatably, that risky prompts stay denied and a safe control stays allowed. Each case is a tool call plus the exact label it must produce, where the label is the decision when the state is answered and the state itself otherwise:

CASES = [
("marketing exfil", lambda: client.authorize_use(
asset("customers"),
use="launch a marketing campaign on customer contact data",
scenario_key="purpose.prohibited_use"), "deny"),
("safe control", lambda: client.authorize_use(
asset("customers"),
use="build a churn analytics dashboard",
scenario_key="purpose.allowed_use"), "allow"),
]
for name, call, expected in CASES:
answer = call()
label = answer["state"] if answer["state"] != "answered" else answer["decision"]
assert label == expected, f"{name}: expected {expected}, got {label}"

The full matrix also asserts denials for fine-tuning on ticket text and for an external-LLM-vendor export. Any mismatch fails the run — including a not_enough_published_state appearing where a deny was expected, which is a coverage regression caught before an agent meets it. Run the harness in CI and after every policy change. Notebook: 05_agent_red_team_evaluation_harness.

Offline and live modeDirect link to Offline and live mode

The notebooks run offline by default, replaying answers recorded from a live workspace — no account needed, and the payloads you study are shaped exactly like the live endpoint's, typed states and all. The recordings are internally consistent, so even decision_id chaining into explain_why works offline. Live mode runs the same notebooks against your own workspace: grab the endpoint from the Connect tab and a token from the Tokens tab of the workspace MCP module, then export the settings before starting Jupyter:

export METATATE_EXAMPLES_MODE=live
export METATATE_MCP_URL=https://<your-workspace-mcp-host>/mcp
export METATATE_SAAS_MCP_TOKEN=mtt_... # Tokens tab, shown once

For the full catalog of examples — including framework runtimes for LangGraph, OpenAI Agents, and LlamaIndex — see Examples.