Skip to content
Browse documentation
Working with data

Define data semantics

Write business definitions, model mappings, and reusable measures, with a worked sales example.

Suppose a user asks: “Show revenue by region.” Here is the order table behind that question, also used in Inspect your data:

order_idregionstatusamountrefund_amountordered_at
1Eastpaid120202026-01-03
2Eastrefunded80802026-01-04
3Eastpending20002026-01-05
4Westpaid90302026-01-06
5Eastpaid50null2026-01-07
6Westcancelled50002026-01-08

The structure tells AVA that amount and refund_amount are numbers. It does not tell AVA which orders count as revenue. You supply that meaning through semantic().

Decide what revenue means

For this example, agree on three rules:

  • Count paid and refunded orders; exclude pending and cancelled orders.
  • Subtract cumulative refunds from the original amount. A missing refund is zero. Amounts are USD.
  • Group by the order's region and use its original order date.

Apply those rules to the rows above:

OrderContribution to revenueWhy
1100 USD120 minus the 20 refund.
20 USDFully refunded: 80 minus 80.
30 USDPending, so excluded.
460 USD90 minus the 30 refund.
550 USDThe missing refund is treated as zero.
60 USDCancelled, so excluded.

East therefore has 150 USD in revenue; West has 60 USD . The definition below records the rules that produce those numbers.

Write the definition

Load the table above as inline JSON, exposed by DuckDB as data. Its corresponding semantic definition is:

javascript
/** @satisfies {import('@antv/ava').Semantic} */
const semantic = {
  description: 'Sales reporting in USD. Revenue means net revenue from paid and refunded orders. Pending and cancelled orders do not count. A missing refund is zero. Dates are order dates, not refund dates.',
  properties: { owner: 'Sales operations' },
  models: [{
    name: 'data',
    tableReference: { table: 'data' },
    primaryKey: 'order_id',
    properties: { description: 'One row per order; refunds are cumulative amounts on the original order.' },
    columns: [
      { name: 'amount', type: 'BIGINT', properties: { description: 'Order amount before refunds.', unit: 'USD' } },
      { name: 'refund_amount', type: 'BIGINT', properties: { description: 'Cumulative refund, not a separate refund transaction. Null means zero.', unit: 'USD' } },
      { name: 'status', type: 'VARCHAR', properties: { description: 'Only paid and refunded orders contribute to revenue.' } },
      {
        name: 'net_amount',
        type: 'DOUBLE',
        isCalculated: true,
        expression: 'amount - COALESCE(refund_amount, 0)',
        properties: { description: 'Per-order amount after refunds, before filtering by status.', unit: 'USD' },
      },
    ],
  }],
  cubes: [{
    name: 'sales',
    baseObject: 'data',
    measures: [{
      name: 'net_revenue',
      expression: "SUM(CASE WHEN status IN ('paid', 'refunded') THEN amount - COALESCE(refund_amount, 0) ELSE 0 END)",
      type: 'DOUBLE',
      description: 'Revenue after refunds, counting only paid and refunded orders.',
      properties: { unit: 'USD', synonyms: ['revenue', 'net sales'] },
    }],
    dimensions: [{ name: 'region', expression: 'region', type: 'VARCHAR' }],
    timeDimensions: [{ name: 'order_date', expression: 'CAST(ordered_at AS DATE)', type: 'DATE' }],
  }],
};

Read it in three parts:

  1. description and properties explain meaning. The shared description defines revenue; model and column properties explain row grain, units, and nulls. Keys such as unit and synonyms are annotations, not special SDK options.
  2. models connects meaning to data. name: 'data' matches the discovered model. The calculated net_amount describes a per-order value. Unlisted physical columns remain available from the schema.
  3. cubes defines reusable analysis. sales groups a measure and its grouping expressions. net_revenue contains both the status filter and refund calculation. dimensions supplies region grouping; timeDimensions supplies order-date grouping.

The measure includes its eligibility rule so that it still applies when grouping by region or date. The calculated net_amount alone does not decide which orders count toward revenue.

For your own source, inspect await ava.schema() first. Match existing model and column names exactly and retain physical source qualifiers. Each semantic model requires exactly one of tableReference, refSql, or baseObject; each column requires name and type. Copy discovered types for physical fields and declare result types for calculations. See model sources for the alternatives.

You can start smaller: { description: 'Your business rules here.' } is valid. Add models for field-specific meaning, relationships for joins, and cubes for reusable measures as your questions require them.

Save the definition and see the result

After configuring ava and loading a source with source(), pass the definition as the setter's return value:

typescript
await ava.semantic(undefined, () => semantic);
const saved = await ava.semantic();

undefined uses the default options; semantic currently has no configurable options. The setter saves a validated copy. Subsequent analyze(), translate(), and suggest() calls combine it with the schema and available profile.

Use the CLI

Save the definition as a JSON file, for example with JSON.stringify(semantic, null, 2) in your application. After loading the source with ava source, use its dataset ID:

sh
ava semantic "$DATASET_ID" --file semantic.json
ava semantic "$DATASET_ID" > semantic-readback.json

--file takes a plain path without @ and replaces the complete session definition. Reading without --file returns the current definition, initially {}. Both operations work without a model key. To clear the definition, load a file containing {}. Invalid files leave the previous value intact.

Keep the JSON file for later sessions; the CLI does not load it automatically. Schema/profile updates retain semantic, while a new source session starts without it. The AVA skill guides agents to resolve material definition gaps with the user and save reusable confirmations with their source and scope. File discovery and scope checks are agent responsibilities.

analyze, translate, and suggest use these definitions as model context. Agent-written query DSL must apply them explicitly; semantic does not rewrite executed queries.

Query using the definition

For “Show revenue by region,” a query applying these definitions can look like this:

sql
SELECT
  region,
  CAST(SUM(
    CASE WHEN status IN ('paid', 'refunded')
      THEN amount - COALESCE(refund_amount, 0)
      ELSE 0
    END
  ) AS DOUBLE) AS net_revenue
FROM data
GROUP BY region
ORDER BY region;

The result for the six orders above is:

regionnet_revenue (USD)
East150
West60

This is an example query; model-generated SQL and column aliases can vary.

A logical model, cube, view, or calculated column does not automatically become a queryable database object. For example, SQL using net_amount must expand its expression or define it in a subquery or CTE. The query above embeds the measure expression and reads the physical data table.

Check and maintain your definition

Before using a definition for real questions:

  1. Confirm row grain, units, eligible records, null handling, and time meaning with the data owner. For joins, check that a row is not counted multiple times.
  2. Test expressions on a small dataset with known answers, including edge cases such as the refunds and excluded orders shown above.
  3. Inspect generated SQL and compare executed results with those known answers. Setter validation checks the object structure; it does not prove the SQL or business logic is correct.

Keep the complete definition in application code or a JSON file and version it with the data contract.

For exact field shapes, source mappings, relationships, views, merge rules, and validation errors, continue to the semantic fields in the API reference. See Inspect your data for the other inputs and AVA API for the method signature.