Skip to content
Mini workshop · 60 minutes · Free

Intro to context engineering in Omni.

Adding context at different layers in Omni, and how each one makes the AI's answers more accurate. We add one layer at a time and check the answers before and after.

What this is, and how to use it

Ask an AI agent for revenue on a raw Salesforce model and it answers instantly, confidently, and wrong: open pipeline and lost deals folded in, nothing on screen to warn you. Ask for revenue this year and it has a second guess to make: does your company run on the calendar year or a fiscal year, and if fiscal, which month does it start? The fix is not a more expensive model. Given the right context, a smaller and cheaper model returns reliably accurate answers; without it, the priciest model on the market is just more confident about the same bad guess. When Anthropic fixed the context its own analytics agent reads, accuracy went from roughly 21% to over 95% with the model unchanged. That work is context engineering, and this page shows all of it at a scale you can absorb in an hour.

It runs in Omni and it is self-paced. Each layer below is a short video that adds only that one layer, the exact copy-pasteable YAML behind it, and the Omni docs. Reproduce it in your own instance with the follow-along kit, or join a live 60-minute run with Q&A.

What is in this mini workshop

Five ways to add context, and how they work together

The agent answers from a semantic model, and there are five different instruments for putting context into it: structure, vocabulary, prose, examples, configuration. Each one alone fixes a specific class of wrong answer. Stacked, they cover for each other: the prose can point at a governed measure, the examples can reference metrics that exist, and the configuration settles what no amount of prose will. That is what turns a confident guess into an answer your team can act on. Jump to any layer.

Show me revenue this year
One question, asked twice
Before: skip all five layers and you still get an answer
Before: raw model

Revenue means whatever the agent decided it means.

Here, the sum of every opportunity amount in the table: open pipeline and lost deals included, "this year" read as the calendar year, no flag that either definition was invented.

Follow along in your own instance

You need an Omni instance (an active trial or customer account) and a Snowflake schema you can load six CSVs into. The kit has the synthetic Salesforce-shaped dataset (six tables, 320 opportunities, deliberately trapped fields), the Snowflake DDL, the raw starter model so your "before" matches ours, and every layer file on this page.

  1. Run the DDL, load the six CSVs, point an Omni connection at the schema.
  2. Paste the starter views over the generated ones. Ask "how many channel wins do we have?" and get 0. That zero is your baseline.
  3. One branch per layer, paste that layer's YAML, refresh, validate, re-ask. Watch it flip.

Why branches? A branch is an isolated copy of the model on top of the shared one. You can change joins, definitions, and context there, ask the Agent the same questions, and nobody else sees a thing until you merge. That is what makes each before/after clean: one branch, one change, same questions. It is also how you should ship context changes for real.

Download the follow-along kit (110 KB)
Layer 1 of 5

Joins and relationships

Structure

How to think about it: This layer is the map. Until a join is declared, the agent does not fail on an unreachable table, it answers from whatever it can reach. Keep the declared paths straightforward rather than clever: the job here is legibility to the agent, not covering every use case.

New to Omni? A Topic is a curated dataset: one starting table, the tables joined to it, the fields people can use, and the context written for the AI. The Agent picks a Topic first and answers from inside it, so the Topic sets both what the AI can see and where most of the context on this page lives.

Be specific about the joins. A raw model can see every table but knows no routes between them, so questions about an unjoined table do not fail, they get silently answered from whatever the model can reach. Declaring the join is what puts fields in scope, and declaring it precisely is what protects granularity: the joins block is a path (subscriptions hangs off accounts, not off the base view), and relationship_type is what lets Omni sum MRR once per subscription instead of once per opportunity that account happens to have. That is the payoff of semantic modeling: the route and the grain are declared once, and the Agent, every workbook, and every dashboard compile through that one definition instead of each writing its own SQL.

One shortcut to know about. This demo declares its relationships inside the Topic file so the whole layer is a single paste. That scopes them to this one Topic, which is fine for a focused use case but not DRY. In a real model, put relationships in the model's relationships file, defined once and reused by every Topic, and let each Topic's joins block simply choose the path.

The syntax below is the entire layer. Expand a file, copy it, paste it into your own model.

cx_sales_pipeline.topic.yaml The Topic file: the declared join path (subscriptions nested under accounts) plus the relationship types that protect the grain.
# LAYER 1 of 5: joins and relationships, and nothing else.
#
# Starter topic plus one declared path: accounts -> subscriptions. The
# subscriptions table sits in the same schema the whole time; the raw model can
# see it but the Topic cannot reach it, so every MRR question dies before it
# starts. Declaring the relationship is what puts those fields in scope.
#
# Docs: https://docs.omni.co/modeling/relationships
base_view: cx_training__opportunities
label: Sales Pipeline (Workshop)

# The joins block is the PATH, not a list of tables. subscriptions hangs off
# accounts, not off the base view, so it nests under accounts. Declaring it flat
# here is the classic mistake: Omni cannot find a route from opportunities to
# subscriptions and drops the view from the Topic with a warning.
joins:
  cx_training__accounts:
    cx_training__subscriptions: {}
  cx_training__activities: {}

relationships:
  - join_from_view: cx_training__opportunities
    join_to_view: cx_training__accounts
    join_type: always_left
    on_sql: ${cx_training__opportunities.account_id} =
      ${cx_training__accounts.account_id}
    relationship_type: many_to_one

  - join_from_view: cx_training__opportunities
    join_to_view: cx_training__activities
    join_type: always_left
    on_sql: ${cx_training__opportunities.opportunity_id} =
      ${cx_training__activities.opportunity_id}
    relationship_type: one_to_many

  # The layer. Reach subscriptions THROUGH accounts, not from opportunities:
  # one subscription per account, so this is one_to_one from the account side.
  # relationship_type is not paperwork. It is what lets Omni sum MRR once per
  # subscription instead of once per opportunity the account happens to have.
  - join_from_view: cx_training__accounts
    join_to_view: cx_training__subscriptions
    join_type: always_left
    on_sql: ${cx_training__accounts.account_id} =
      ${cx_training__subscriptions.account_id}
    relationship_type: one_to_one
Layer 2 of 5

Field metadata and governed measures

Vocabulary

How to think about it: This layer is the vocabulary. Your field definitions are the prompt. This is where you draw the line between dimensions, the things you slice by, and measures, the things you aggregate, say how each is meant to be used, and settle on one canonical dataset so "revenue" means one thing instead of five.

Your data definitions are the prompt. Five elements per field earn their space: a label carrying the unit, a description saying what the field excludes and what to use instead, synonyms harvested from how people actually ask, the values (every value of an enum with all_values, a few representative ones for open-ended fields with sample_values), and governed measures so a loaded word like "revenue" resolves to one agreed calculation instead of a guess. A field can also carry its own ai_context, and the two do different jobs. The description is the definition people read in the workbook and the Agent reads too. ai_context is written for the Agent alone: the instruction ("this is not revenue, use closed_revenue"), not the definition. It also survives when the context window gets tight: Omni prunes values and descriptions first, and never prunes ai_context.

The sharpest proof here is the pair that does NOT flip. Ask the raw model "how much have we booked?" and it answers 10,016,474: exactly right, no context engineering at all. Ask it "show me revenue" and it answers 49,932,864: the same metric, 5x wrong. One word changed. The model is not weak at arithmetic, it is guessing at your vocabulary, and this layer removes the guess.

The syntax below is the entire layer. Expand a file, copy it, paste it into your own model.

opportunities.view.yaml The opportunities view, fully documented: labels, descriptions, synonyms, enum values, and the governed measures, including closed_revenue.
# LAYER 2 of 5: field metadata and governed measures, and nothing else.
#
# Same columns as the raw view. What changes is that every field now carries the
# five elements that earn their space: label (with the unit), description (what
# it excludes and what to use instead), synonyms (the words users actually say),
# values (every value of an enum, never a sample), and governed measures so
# "revenue" resolves to one agreed calculation instead of a guess.
#
# No ai_context here on purpose, that is Layer 3. This layer is the answer to
# "show me revenue": closed_revenue exists, it is labeled Bookings, and
# "revenue" is one of its synonyms.
#
# Docs: https://docs.omni.co/modeling/dimensions/parameters/index
#       https://docs.omni.co/modeling/measures/index
#       https://docs.omni.co/modeling/dimensions/parameters/all-values

# Reference this view as cx_training__opportunities
schema: CX_TRAINING
table_name: OPPORTUNITIES

dimensions:
  opportunity_id:
    sql: '"OPPORTUNITY_ID"'
    label: Opportunity ID
    primary_key: true

  account_id:
    sql: '"ACCOUNT_ID"'
    label: Account ID
    description: Foreign key to the account. Join to accounts for name and segment.

  amount:
    sql: '"AMOUNT"'
    label: Deal amount (USD)
    description: Deal value in USD. For booked revenue only, filter to Closed Won
      (see the closed_revenue measure).
    synonyms: [ deal value, deal size, contract value ]

  stage_name:
    sql: '"STAGE_NAME"'
    label: Sales stage
    description: Current pipeline stage. The first four are open, the last two are closed.
    all_values:
      [
        Prospecting,
        Qualification,
        Proposal,
        Negotiation,
        Closed Won,
        Closed Lost
      ]
    synonyms: [ stage, pipeline stage, deal stage ]

  close_date:
    sql: '"CLOSE_DATE"'
    label: Close date
    description: "Close date for closed deals, projected close date for open deals.
      Use this for fiscal-period math. The fiscal year starts in JULY: FY26 =
      2025-07-01 through 2026-06-30."
    synonyms: [ closed date, expected close ]

  created_date:
    sql: '"CREATED_DATE"'
    label: Created date
    description: Date the opportunity was created.
    synonyms: [ opened date, create date ]

  is_closed:
    sql: '"IS_CLOSED"'
    label: Is closed
    description: TRUE for BOTH Closed Won AND Closed Lost. Does NOT mean won. Pair
      with Is Won to tell wins from losses. Open pipeline is is_closed = FALSE.
    synonyms: [ closed ]

  is_won:
    sql: '"IS_WON"'
    label: Is won
    description: TRUE only for Closed Won. is_closed = TRUE and is_won = FALSE means
      Closed Lost.
    synonyms: [ won ]

  type:
    sql: '"TYPE"'
    label: Opportunity type
    description: Type of business for the deal.
    synonyms: [ deal type, business type ]

  competitor_c:
    sql: '"COMPETITOR_C"'
    label: Win channel / competitor
    description: "MISLEADING NAME. WON deals: the win channel (values like 'CH -
      Partner Referral'). LOST deals: the competitor who won. NULL for open
      deals. Channel wins = competitor_c LIKE 'CH%' AND stage_name = 'Closed
      Won'. To answer 'who do we lose to', filter to Closed Lost first."
    sample_values:
      [
        CH - Partner Referral,
        CH - Existing Relationship,
        CH - Direct Outreach,
        CH - RFP Response,
        Heighliner Systems,
        Mentat Analytics,
        No Decision
      ]
    synonyms: [ win channel, competitor, channel wins, who we lose to ]

measures:
  count:
    label: Number of opportunities
    aggregate_type: count
    synonyms: [ deal count, number of deals ]

  total_amount:
    sql: '"AMOUNT"'
    label: Total deal value
    description: Sum of amount across ALL opportunities regardless of stage. For
      booked revenue use closed_revenue.
    aggregate_type: sum
    format: usdcurrency
    synonyms: [ sum of amount ]

  closed_revenue:
    sql: CASE WHEN "IS_WON" = TRUE THEN "AMOUNT" ELSE 0 END
    label: Bookings (closed won revenue)
    description: Sum of amount for Closed Won deals only. This is bookings. Use
      this, not total_amount, for won or booked revenue.
    aggregate_type: sum
    format: usdcurrency
    synonyms: [ bookings, closed revenue, won revenue, revenue ]

  open_pipeline:
    sql: CASE WHEN "IS_CLOSED" = FALSE THEN "AMOUNT" ELSE 0 END
    label: Open pipeline
    description: Sum of amount for open deals (is_closed = FALSE). Excludes won and lost.
    aggregate_type: sum
    format: usdcurrency
    synonyms: [ pipeline, open deals ]

  won_count:
    sql: CASE WHEN "IS_WON" = TRUE THEN 1 ELSE 0 END
    label: Won deals
    description: Count of Closed Won deals.
    aggregate_type: sum
    synonyms: [ wins ]

  closed_count:
    sql: CASE WHEN "IS_CLOSED" = TRUE THEN 1 ELSE 0 END
    label: Closed deals
    description: Count of all closed deals, won and lost. This is the win rate denominator.
    aggregate_type: sum

  latest_close_date:
    sql: '"CLOSE_DATE"'
    label: Latest close date
    description: Most recent close_date in the group. Used to break ties in rankings.
    aggregate_type: max

  win_rate:
    sql: ${won_count} / NULLIF(${closed_count}, 0)
    label: Win rate
    description: Won deals divided by closed deals. Only closed deals are in the
      denominator; open deals are excluded. Do NOT divide by all deals.
    synonyms: [ win percentage, close rate ]
Layer 3 of 5

AI context

Prose

How to think about it: This layer is the expert over your shoulder. This is how you retire a key-person dependency. Everything the one person who knows this dataset carries in their head, the caveats they would say out loud while you used it, gets extracted and written down where the agent reads it.

ai_context is plain-English notes written where the Agent reads them. Field-level ai_context, from layer 2, covers one column; this layer adds the three levels above it, each with its own job. View ai_context says what the fields mean, and it is where trap fields get called out loudly: a competitor column that holds the win channel on won deals, a closed flag that is true for losses. Topic ai_context says what the dataset is: its grain, its edges, the business rules that span fields. Model ai_context says how to think: which source to trust first, and when an answer may call itself done. Implementation is exactly what it looks like: write down what the person who knows the data knows, in the YAML.

Two things the demo does not show. First, the data team does not have to write all of it. Anyone building in a workbook can add or edit field definitions, including descriptions and ai_context, in that workbook's model layer; it stays local until someone promotes it to the shared model, reviewed as a YAML diff. That is how the caveat the sales ops analyst knows gets into the model without waiting in the data team's queue. Second, context can vary by user. Model, Topic, and view ai_context accept user attributes ({{omni_attributes.region}}), so when "my pipeline" means a different segment for each rep, one Topic serves all of them. Field-level ai_context does not support this.

The syntax below is the entire layer. Expand a file, copy it, paste it into your own model.

model.yaml (model level) How to think: the order of resort and the definition of done.
# LAYER 3 of 5, file 3 of 3: model-level ai_context.
#
# View ai_context says what fields mean. Topic ai_context says what the dataset
# is. Model ai_context says how to THINK: which source to trust first, and when
# an answer is allowed to call itself finished. It is the thin router, read
# before anything else, and it is the layer that turns a silent guess into a
# clarifying question.
#
# Deliberately absent: fiscal_month_offset. Prose alone has to argue the AI into
# a July fiscal year on every single question. The combined branch sets the
# offset once and stops arguing. That contrast is the point of the lab.
#
# Docs: https://docs.omni.co/modeling/models/ai-context

ai_context: |
  ORDER OF RESORT (which source to trust first):
  1. Metrics and rules written into the curated model context come first.
  2. Curated, documented dimensions next.
  3. Raw, undocumented columns last, and only when nothing above answers the question.
     Flag it when you do, and name the field and view you used.

  DEFINITION OF DONE for every answer:
  - Name the dataset (Topic) and the filters you applied.
  - State the time window (and remember the fiscal year starts in JULY).
  - When the question is ambiguous (no time window, no metric named), ASK a clarifying
    question instead of guessing. A guessed window is a wrong answer with good posture.

ai_chat_topics: [ cx_sales_pipeline, cx_product_usage ]

# Short cache so workshop data refreshes show up quickly. To force it after a
# reload: POST /api/v1/models/{id}/cache_reset/workshop_fresh (see RUNBOOK).
cache_policies:
  workshop_fresh:
    max_cache_age: 1 hour

default_cache_policy: workshop_fresh
opportunities.view.yaml (view level) What the fields mean: the raw view untouched, plus one block calling out the trap fields.
# LAYER 3 of 5, file 1 of 3: ai_context, and nothing else.
#
# The raw view, untouched, plus one block at the bottom. No labels, no
# descriptions, no measures. Every field is still named exactly what the
# warehouse named it. The only thing that changed is that someone wrote down
# what they know.
#
# Note what this ai_context does NOT say: it never names closed_revenue or
# win_rate, because on this branch those measures do not exist. Context and
# model objects co-evolve. Compare with the combined branch, where the same
# rules can point at a governed measure instead of spelling out the filter.
#
# Docs: https://docs.omni.co/modeling/views/parameters/ai-context

# Reference this view as cx_training__opportunities
schema: CX_TRAINING
table_name: OPPORTUNITIES

dimensions:
  opportunity_id:
    sql: '"OPPORTUNITY_ID"'
    primary_key: true

  account_id:
    sql: '"ACCOUNT_ID"'

  amount:
    sql: '"AMOUNT"'

  stage_name:
    sql: '"STAGE_NAME"'

  close_date:
    sql: '"CLOSE_DATE"'

  created_date:
    sql: '"CREATED_DATE"'

  is_closed:
    sql: '"IS_CLOSED"'

  is_won:
    sql: '"IS_WON"'

  type:
    sql: '"TYPE"'

  competitor_c:
    sql: '"COMPETITOR_C"'

measures:
  count:
    aggregate_type: count

ai_context: |
  One row per sales opportunity (deal). opportunity_id is unique.
  amount is the deal value in USD. It is NOT revenue on its own: it counts open and
  lost deals too. Bookings (revenue) = SUM(amount) WHERE stage_name = 'Closed Won'.
  competitor_c is MISLEADINGLY named: for WON deals it holds the win CHANNEL
  (values like 'CH - Partner Referral'); for LOST deals it holds the competitor who won;
  for OPEN deals it is NULL. "Channel wins" means competitor_c LIKE 'CH%' AND
  stage_name = 'Closed Won'. "Who do we lose to" reads competitor_c on Closed Lost rows ONLY.
  is_closed is TRUE for BOTH Closed Won AND Closed Lost, so always pair it with is_won.
  Open pipeline is is_closed = FALSE.
  Win rate = won deals / CLOSED deals. Open deals are excluded from the denominator.
  stage_name is one of: Prospecting, Qualification, Proposal, Negotiation, Closed Won, Closed Lost.
  The fiscal year starts in JULY: FY26 = 2025-07-01 through 2026-06-30.
cx_sales_pipeline.topic.yaml (Topic level) What the dataset is: its grain, its edges, and the business rules that span fields.
# LAYER 3 of 5, file 2 of 3: Topic ai_context.
#
# The starter Topic plus a description and an ai_context block. View ai_context
# covers what the fields mean; Topic ai_context covers what this DATASET is: its
# grain, its edges (what it can and cannot reach), and the business rules that
# span more than one field.
#
# Docs: https://docs.omni.co/modeling/topics/parameters/ai-context
base_view: cx_training__opportunities
label: Sales Pipeline (Workshop)
description: One row per opportunity. Pipeline, win rate, and bookings, with
  account and activity context.

joins:
  cx_training__accounts: {}
  cx_training__activities: {}

relationships:
  - join_from_view: cx_training__opportunities
    join_to_view: cx_training__accounts
    join_type: always_left
    on_sql: ${cx_training__opportunities.account_id} =
      ${cx_training__accounts.account_id}
    relationship_type: many_to_one

  - join_from_view: cx_training__opportunities
    join_to_view: cx_training__activities
    join_type: always_left
    on_sql: ${cx_training__opportunities.opportunity_id} =
      ${cx_training__activities.opportunity_id}
    relationship_type: one_to_many

ai_context: |
  Grain: one row per opportunity. opportunity_id is unique. A plain count of rows equals
  a count of distinct opportunity_id. This Topic exposes accounts and activities only;
  it does NOT reach subscriptions or product usage.

  CRITICAL BUSINESS RULES:
  1. competitor_c is MISLEADINGLY named. For WON deals it is the win channel
     (values like 'CH - Partner Referral'). For LOST deals it is the competitor who won.
     For open deals it is NULL. "Channel wins" = competitor_c LIKE 'CH%' AND
     stage_name = 'Closed Won'. "Who do we lose to" = competitor_c on Closed Lost rows only.
  2. is_closed is TRUE for BOTH Closed Won AND Closed Lost. Always pair with is_won.
  3. Bookings (revenue) = SUM(amount) WHERE stage_name = 'Closed Won'. Never report a
     bare SUM(amount) as revenue: it includes open pipeline and lost deals.
  4. Win rate = won deals / closed deals. Open deals are EXCLUDED from the denominator.
     Never divide wins by all opportunities.
  5. The fiscal year starts in JULY. FY26 = 2025-07-01 through 2026-06-30. Fiscal
     quarters: Q1 Jul-Sep, Q2 Oct-Dec, Q3 Jan-Mar, Q4 Apr-Jun. "Last fiscal quarter" is
     the prior 3-month block by close_date.
  6. For "top N accounts" rankings, break ties by the most recent close_date.
  7. activities is a child of opportunities. Count activities; do not read deal amounts
     off a joined activity row (the deal repeats once per activity).
Layer 4 of 5

Sample queries

Examples

How to think about it: This layer is the worked example. Show instead of describe, and the agent matches the next question against the pattern. Keep them short. A compact query that unpacks one piece of logic gets reused everywhere, while dumping a sprawling CTE here teaches the agent nothing it can apply to the next question.

A worked example beats another paragraph of description. A sample query pairs a real prompt with its canonical query; the model matches new questions against it and reuses the pattern. This branch contains three worked examples and nothing else: no labels, no descriptions, no ai_context. The fields are still named competitor_c and is_closed and nothing explains them. What flips the answer is pattern-matching, not understanding, and for recurring questions that is enough.

The syntax below is the entire layer. Expand a file, copy it, paste it into your own model.

cx_sales_pipeline.topic.yaml The Topic file with three worked examples, each pairing a real prompt with its canonical query.
# LAYER 4 of 5: sample_queries, and nothing else.
#
# The starter Topic plus worked examples. No labels, no descriptions, no
# ai_context anywhere: the fields are still named competitor_c and is_closed and
# nothing explains them. The model is not told the rule; it is SHOWN the answer
# to a question it actually gets, and it matches the next question against it.
#
# Each entry pairs a prompt (what a human types) with the canonical query (what
# a careful analyst would run). The prompt is half the example: it is what the
# match happens against.
#
# The per-query ai_context parameter is deliberately omitted here. It exists,
# and the combined branch uses it, but on this branch it would smuggle Layer 3
# in through the back door and we would not know which piece did the work.
#
# Note what is missing and cannot be added: a bookings example. Summing amount
# needs a measure, and measures are Layer 2. Sample queries teach the model to
# reuse your pattern, not to invent your metric.
#
# Docs: https://docs.omni.co/modeling/topics/parameters/sample-queries
base_view: cx_training__opportunities
label: Sales Pipeline (Workshop)

joins:
  cx_training__accounts: {}
  cx_training__activities: {}

relationships:
  - join_from_view: cx_training__opportunities
    join_to_view: cx_training__accounts
    join_type: always_left
    on_sql: ${cx_training__opportunities.account_id} =
      ${cx_training__accounts.account_id}
    relationship_type: many_to_one

  - join_from_view: cx_training__opportunities
    join_to_view: cx_training__activities
    join_type: always_left
    on_sql: ${cx_training__opportunities.opportunity_id} =
      ${cx_training__activities.opportunity_id}
    relationship_type: one_to_many

sample_queries:
  Channel wins:
    query:
      fields: [ cx_training__opportunities.count ]
      base_view: cx_training__opportunities
      filters:
        cx_training__opportunities.competitor_c:
          starts_with: CH
        cx_training__opportunities.stage_name:
          is: Closed Won
      topic: cx_sales_pipeline
    prompt: how many channel wins did we have?

  Who we lose to:
    query:
      fields:
        [
          cx_training__opportunities.competitor_c,
          cx_training__opportunities.count
        ]
      base_view: cx_training__opportunities
      filters:
        cx_training__opportunities.stage_name:
          is: Closed Lost
      sorts:
        - field: cx_training__opportunities.count
          desc: true
      topic: cx_sales_pipeline
    prompt: who do we lose to most?

  Open opportunities:
    query:
      fields: [ cx_training__opportunities.count ]
      base_view: cx_training__opportunities
      filters:
        cx_training__opportunities.is_closed:
          is: false
      topic: cx_sales_pipeline
    prompt: how many open opportunities do we have right now?
Layer 5 of 5

The fiscal calendar

Config

How to think about it: This layer is the setting. Strictly, this one is semantic modeling, not context engineering. fiscal_month_offset does not instruct the LLM, it instructs SQL generation. The Agent only has to ask for "this fiscal year"; the semantic layer guarantees the July start in the SQL, even when the Agent's own reasoning gets it wrong. When prose and config disagree, config wins, and one rule set once saves restating it in every Topic.

The layer that argues with the other four. Layer 3 says, in plain English, that the fiscal year starts in July. The Agent reads it, believes it, even reaches for the right date literal, and still answers off the calendar year, because relative dates resolve against the model fiscal calendar, and that calendar is a setting, not a sentence. One line of config fixes what a paragraph of prose could not. Whenever a rule can be enforced in the model instead of explained to the Agent, enforce it: then it holds no matter what the Agent decides.

The syntax below is the entire layer. Expand a file, copy it, paste it into your own model.

model.yaml The model file whose one load-bearing line is fiscal_month_offset: 6.
# LAYER 5 of 5: one line of model config, and nothing else.
#
# This layer exists because of a result we did not expect and kept anyway.
#
# The ai_context layer (Layer 3) says, in plain English, "The fiscal year starts
# in JULY. FY26 = 2025-07-01 through 2026-06-30." The Agent reads it, believes
# it, and even reaches for the right literal: it writes
# OMNI_DATETIME_LITERAL('this fiscal year') into the SQL. And it STILL returns
# the calendar-year answer, because the literal itself resolves against the
# model's fiscal calendar, and that calendar is a setting, not a sentence.
#
# fiscal_month_offset: 6 means the fiscal year starts 6 months after January:
# July. One line, and every relative date literal in every question, on every
# surface, resolves correctly forever.
#
# The lesson is not "prose is useless". It is that context engineering has more
# than one instrument, and prose is the wrong instrument for a calendar. Some
# things you tell the model. Some things you configure, and then stop arguing.
#
# Docs: https://docs.omni.co/modeling/models/fiscal-month-offset

fiscal_month_offset: 6

ai_chat_topics: [ cx_sales_pipeline, cx_product_usage ]

# Short cache so workshop data refreshes show up quickly. To force it after a
# reload: POST /api/v1/models/{id}/cache_reset/workshop_fresh (see RUNBOOK).
cache_policies:
  workshop_fresh:
    max_cache_age: 1 hour

default_cache_policy: workshop_fresh

All five together

The finale branch turns every layer on at once, and adds what composition makes possible: the ai_context can now point at governed measures ("use closed_revenue") instead of spelling out filters, and the sample queries can reference metrics that exist. Each layer is worth something alone. Together they are a model where "revenue", "channel wins", and "this fiscal year" mean what your company means.

  • The raw model is not stupid, it is starved. It already answers "who do we lose to", "open opportunities", and "win rate by segment" correctly. Ambiguity is what kills it, and ambiguity is what these layers remove.
  • The dangerous answer is the plausible one. Account counts in place of MRR, calendar bookings in place of fiscal. Nothing on screen says "I guessed."
  • Context has more than one instrument. Some things you write in prose, some you encode as worked examples, and some, like a fiscal calendar, you take out of the LLM's hands and enforce in the semantic model.
  • Keeping it right is a loop, not a project. Freeze ten real questions as a prompt set in Omni's AI Hub and run an eval against the branch before you merge each context change, so a fix to one answer cannot quietly break another. Turn every wrong answer users report into a new prompt, and route corrections through an owner. Unowned context is stale context.
  • Context can also be a procedure. The five layers shape how the Agent answers one question. An Omni Skill packages a repeatable task, like the weekly pipeline summary or a data quality check, as saved instructions anyone runs by name, so the choices about which measures, filters, and format to use are made once instead of in every prompt.

Want this on your own data?

The full 2-day workshop takes your team from a raw model to a tuned one with a measured before/after pass rate.