AgentsOpen SourceFreeActiveMachine-verified· intermediate · ~15 min setup

Validate a WrenAI semantic model's references before an agent queries through it

Before letting an agent query through WrenAI's governed semantic layer (MDL), validate that every relationship and metric resolves to a model and column that actually exist, so a stale definition fails a check instead of quietly returning wrong-but-plausible numbers.

by Shilpa Mitra· verified today· v1.0.0

Run this workflow

CI-verified, 2/2 fixtures passing.

Build this with your agent

One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.

Intended Use

Teams putting an agent in front of a warehouse through WrenAI's MDL semantic layer. CI parses the MDL and asserts referential integrity: every relationship references models that exist, and every metric and dimension resolves to a real model and column. No warehouse connection, no key, no model call. The step where the agent turns a question into SQL through the layer is fenced, because no green check can promise a model's query is correct.

Not for

  • Promising a generated answer is correct, this checks the semantic model is internally consistent, not that a query result is right
  • A model maintained by nobody, the layer only pays off if you keep it current; a dangling reference here is exactly the wrong-but-plausible-number risk made visible
  • Following old GenBI tutorials, WrenAI restructured in May 2026, the turnkey web app now lives on the `legacy/v1` branch and main is the context layer and SDK

The Stack

Tested Against

github.com/Canner/WrenAI (2026-07; legacy/v1 holds the old app)python@3.11node@20

Side effects & data flow

Network
none, local only
Writes
./mdl.json, ./validate.py
Credentials
none required

Prerequisites

  • Python 3 to run the validator
  • A real MDL exported from your WrenAI project to check against

Steps

  1. 1

    Parse the MDL and assert every reference resolves

    Build the model-to-columns map from the MDL, then check each relationship's endpoints and join condition, and each metric's measure and dimension, against columns that actually exist. Rename orders.amount and the metric's measure stops resolving here, loudly, instead of the agent returning a plausible wrong total. CI runs this against a fixture MDL. Pointing an agent at the layer is fenced.

    cat > mdl.json <<'JSON'
    {
      "models": [
        { "name": "orders", "columns": [{"name":"id"},{"name":"customer_id"},{"name":"amount"}] },
        { "name": "customers", "columns": [{"name":"id"},{"name":"region"}] }
      ],
      "relationships": [
        { "name": "orders_customer", "models": ["orders","customers"], "condition": "orders.customer_id = customers.id" }
      ],
      "metrics": [
        { "name": "revenue_by_region", "baseObject": "orders", "measure": {"column":"amount","expression":"sum"}, "dimension": {"model":"customers","column":"region"} }
      ]
    }
    JSON
    python3 - <<'PY'
    import json, sys
    
    with open("mdl.json") as f:
        mdl = json.load(f)
    
    cols = {}
    for m in mdl.get("models", []):
        cols[m["name"]] = set(c["name"] for c in m.get("columns", []))
    
    def resolve(model, column):
        if model not in cols:
            return "model '" + str(model) + "' does not exist"
        if column is not None and column not in cols[model]:
            return "column '" + str(model) + "." + str(column) + "' does not exist"
        return None
    
    errors = []
    
    for rel in mdl.get("relationships", []):
        for mm in rel.get("models", []):
            e = resolve(mm, None)
            if e:
                errors.append("relationship '" + rel.get("name", "?") + "': " + e)
        cond = rel.get("condition", "")
        if " = " in cond:
            for side in cond.split(" = "):
                side = side.strip()
                if "." in side:
                    a, b = side.split(".", 1)
                    e = resolve(a, b)
                    if e:
                        errors.append("relationship '" + rel.get("name", "?") + "' condition: " + e)
    
    for met in mdl.get("metrics", []):
        meas = met.get("measure", {})
        e = resolve(met.get("baseObject"), meas.get("column"))
        if e:
            errors.append("metric '" + met.get("name", "?") + "' measure: " + e)
        dim = met.get("dimension")
        if dim:
            e = resolve(dim.get("model"), dim.get("column"))
            if e:
                errors.append("metric '" + met.get("name", "?") + "' dimension: " + e)
    
    if errors:
        print("BAD: semantic model has dangling references (the wrong-but-plausible-number risk):")
        for x in errors:
            print("  - " + x)
        sys.exit(1)
    
    print("MDL OK: " + str(len(mdl.get("models", []))) + " models, " + str(len(mdl.get("relationships", []))) + " relationship, " + str(len(mdl.get("metrics", []))) + " metric; all refs resolve (orders.customer_id -> customers.id, revenue_by_region measures orders.amount by customers.region)")
    PY
  2. 2

    Point the agent at the governed layer (the model step, not checked by CI)

    Install WrenAI's skills into your coding agent (npx skills add Canner/WrenAI --skill '*'), start a session and run the wren-onboarding skill to wire up the MDL and connectors, then ask questions in plain English. The agent's translation of a question into SQL through the layer is the fenced step, no green check can promise a model's query is correct.

Eval, 2 fixtures

Last passed: verified today
  • mdl-okcontainstimeout 30s · max $0

    Expected: MDL OK: 2 models, 1 relationship, 1 metric; all refs resolve (orders.customer_id -> customers.id, revenue_by_region measures orders.amount by customers.region)

  • clean-exitexit_codetimeout 30s · max $0

    Expected: 0

Results

WrenAI's thesis is that agents fail on business data not because they cannot write SQL, but because they do not know what your warehouse means: you describe entities, relationships, and metrics once in a semantic model (their MDL) and any agent queries through that governed layer across 20-plus sources. The catch is that the semantic layer is both the value and the cost, it only pays off if you keep it current, and a stale definition returns wrong-but-plausible numbers. This recipe makes the layer's internal consistency a check: every relationship, metric, and dimension must resolve to a model and column that exist. Note WrenAI restructured in May 2026, the old turnkey GenBI web app now lives on the `legacy/v1` branch and main is the context layer and SDK, so old tutorials may not match.

Did this work for you?

Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.

Related workflows

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).