Docs/Semantic layer

Define a metric

Add a custom metric to the semantic layer so the agent uses it consistently across questions.

The semantic layer is what makes Arcus answer your questions in your language. A metric is the most common thing you'll add — a SQL fragment with a grain that the agent uses whenever a question implies it.

This guide covers what a metric is, how to define one, and how to test it.

What a metric is

A metric is a named, tenant-scoped SQL aggregation. Once defined, the agent uses it whenever a question references it (or any of its synonyms).

A simple example — define revenue once, and "what's our revenue?", "how much did we sell?", and "what was GMV?" all resolve to the same SQL. No more "which revenue column?" drift across questions.

The shape is small:

metrics:
  revenue:
    expr: SUM(gross_revenue)
    table: mart.orders
    grain: order
    synonyms: [sales, gmv, top_line]
    description: "Gross revenue. Includes returns and discounts. For net, use `net_revenue`."

Five fields:

  • expr — the SQL aggregation. Must be a single expression that aggregates rows of the named table.
  • table — the source table. Must exist in the discovered schema.
  • grain — what one row represents (order, customer, campaign, etc.). Used to pick the right join when a question pulls in a different dimension.
  • synonyms — alternate names. The classifier matches on these.
  • description — human-readable note. Shown to the agent in tool calls so it picks correctly when two metrics are close.

Add a metric

There are two paths.

Path A — guided UI

In Arcus, open Settings → Semantic layer → Metrics → Add metric. The form asks for the five fields above, with autocomplete on tables and column names.

Click Save. The metric is staged in a draft version of the layer. Click Publish to activate it for all chat threads.

Path B — edit YAML directly

For bulk changes, click Edit YAML in the same settings page. You'll get the full semantic layer in a YAML editor with syntax highlighting and validation.

version: 4
entities:
  order:
    table: mart.orders
    primary_key: [date, sku, sales_channel]
    metrics:
      revenue:
        expr: SUM(gross_revenue)
        synonyms: [sales, gmv, top_line]
      net_revenue:
        expr: SUM(net_revenue)
        synonyms: [net]
      orders:
        expr: COUNT(*)
      aov:
        expr: SUM(gross_revenue) / NULLIF(COUNT(*), 0)
        synonyms: [average_order_value]
    dimensions:
      country:
        column: country_code
        synonyms: [geo, region]
      channel:
        column: sales_channel
        synonyms: [platform, source]

The editor validates against our semantic layer schema — invalid YAML can't be saved.

Test a metric

Before publishing, click Test in playground. The playground lets you ask a sample question and see:

  1. Which metrics the agent matched.
  2. The SQL it generated.
  3. The result rows.

If the agent didn't pick your new metric when you expected, check:

  • Synonyms — does the question use a word in the synonym list? Add it.
  • Grain — is the question implying a different grain? You may need a different metric or a join.
  • Description — is it clear enough that the agent disambiguates correctly when two metrics overlap?

Versioning

Every save creates a new version of the semantic layer. Old versions are kept — you can roll back from Settings → Semantic layer → Versions.

Threads opened against an older version still resolve correctly; the metric definition is bound at run time.

A real example

Say you want to define ROAS (return on ad spend) across multiple platforms.

Step 1 — pick the right grain. ROAS is usually computed per campaign, per day. So:

entities:
  ad_spend:
    table: mart.ad_spend_daily
    primary_key: [date, platform, campaign_id]
    metrics:
      spend:
        expr: SUM(spend_usd)
      conversion_value:
        expr: SUM(conversion_value_usd)
        synonyms: [revenue_attributed, ad_revenue]
      roas:
        expr: SUM(conversion_value_usd) / NULLIF(SUM(spend_usd), 0)
        synonyms: [return_on_ad_spend]
        description: "Conversion value attributed by the platform divided by spend. Treats null spend as zero. For blended ROAS across platforms, use `blended_roas`."

Now "what's our ROAS on Meta last week?" resolves to:

SELECT
  platform,
  SUM(conversion_value_usd) / NULLIF(SUM(spend_usd), 0) AS roas
FROM mart.ad_spend_daily
WHERE platform = 'meta'
  AND date >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY platform

The agent picked roas, scoped to Meta, and applied the time filter — without you writing SQL.

Best practices

  • One metric per concept. If your team has three definitions of "active user," pick the canonical one for the metric and put the others in synonyms with descriptions explaining the difference.
  • Use NULLIF to guard ratios. Division-by-zero in SQL silently returns NULL, which becomes confusing in narratives. Wrap denominators.
  • Document edge cases in description. The agent reads it. If revenue excludes returns, say so. If a metric only makes sense for certain dimensions, say so.
  • Keep the SQL simple. A metric expression should fit on one line. If it's getting complex, define a view in your warehouse and reference the view instead.

Related