modellerUpdated 2026-04-26

Cross-model calculation recipes

What this covers

A recipe is a multi-step calculation the agent can run against more than one model in a single turn. Recipes solve the class of analytical questions whose answer requires combining numbers from different fact tables — questions a single semantic query cannot answer. This article explains when a recipe is the right tool, how the recipe spec is shaped, how the engine executes it, and how to author recipes that the agent picks up reliably.

Why recipes exist

The semantic engine answers single-model questions: one fact table, one set of measures and dimensions, one time window. That covers most questions, but not all. Some questions are inherently multi-model:

The agent could in principle ask each query and stitch the result together in narration, but in practice that path is unreliable: the LLM forgets which numbers it has, the citations get muddled, and the judge has no clean structure to evaluate. A recipe gives the engine a deterministic plan to execute and the LLM a single result to narrate.

What a recipe is

A recipe is a small JSON document a modeller authors. It has:

Recipes live under Tenant Administration → Projects → \<project> → Conversational agent → Recipes. The same surface is exposed via the recipes API.

Recipes sub-tab — recipe list with the inline editor open, showing parameters, steps, and combine expression.

When to write a recipe

Write a recipe when:

Do not write a recipe when:

Anatomy of a recipe

A worked example. The recipe answers "what is inventory turnover for [time window]" by dividing COGS by average inventory.

{
  "name": "Inventory turnover",
  "description": "Inventory turnover is COGS divided by average inventory over the same window. Call this when the user asks about stock turn or how fast inventory moves.",
  "parameters": [
    {
      "name": "window",
      "description": "The time window the question is about.",
      "resolves_to_glossary_entity": true
    }
  ],
  "steps": [
    {
      "name": "cogs",
      "model_id": "<orders model id>",
      "measures": ["cogs"],
      "dimensions": [],
      "filters": [{"name": "order_date", "op": "in_window", "value": "{window}"}],
      "limit": 1
    },
    {
      "name": "avg_inventory",
      "model_id": "<stock model id>",
      "measures": ["avg_inventory"],
      "dimensions": [],
      "filters": [{"name": "snapshot_date", "op": "in_window", "value": "{window}"}],
      "limit": 1
    }
  ],
  "combine": "cogs / avg_inventory",
  "notes": "Express the answer as a unitless ratio with two decimal places. Mention the window."
}

The engine resolves {window} from the user's question via the glossary, runs the two queries against their respective models, evaluates the combine, and returns the value plus the two underlying step results. The narration LLM turns this into prose. The judge sees the combine and the step rows and verifies the answer against them.

How the LLM picks a recipe

The answer LLM is shown the description and parameters of every recipe in the project. When the user's question matches a recipe's description well, the LLM emits a run_recipe tool call instead of a query tool call. Two practical implications:

Common pitfalls

Operating recipes

After authoring a recipe, exercise it:

  1. Open the Chat tab and ask the question the recipe is meant to answer. Check that the trace drawer shows a run_recipe plan, that each step ran, and that the combine value matches a hand calculation.
  2. Watch the judge verdict on the Metrics → Recent calibration table. If the judge consistently warns or fails on recipe answers, the rubric needs a section that judges combine values explicitly.
  3. Add the question to the per-model glossary's Example questions so the eval runner exercises the recipe path on every run.

Related reading