Agents now connect to datahashi over OAuth — scoped tokens, no API keys to paste
datahashi
Open console
← Docs

Write your first model

Model two warehouse tables — customer and order — into entities, measures, a relationship, and the metrics your consumers are allowed to ask for.

Updated

A semantic model is a folder of YAML. This walks through the smallest useful one: a customer table and an order table, turned into governed metrics like “average order value by region.”

The tables

customer                       order
  id            (primary key)    id            (primary key)
  region        text             customer_id   → customer.id
  signup_date   date             status        text
  ltv           numeric          order_date    timestamp
                                 amount        numeric

The rules

  1. Entity — a business noun, usually one table, with a primary key.
  2. Dimension — an attribute you slice by. Categorical, never aggregated.
  3. Measure — one expression plus one aggregation.
  4. Metric — a named formula over measures, computed after aggregation.
  5. Relationship — a named edge in the join graph, many-side to one-side.
  6. Filter — always { dimension, op, value }, never raw SQL.

The model

entities:
  - name: Customer
    table: public.customer
    primary_key: id
    dimensions:
      - name: region
        column: region
        type: string
        values: [na, emea, apac]
      - name: signup_date
        column: signup_date
        type: date
        grains: [month, quarter, year]
    measures:
      - name: customer_count
        expr: id
        agg: count
      - name: lifetime_value
        expr: ltv
        agg: sum

  - name: Order
    table: public.order
    primary_key: id
    dimensions:
      - name: status
        column: status
        type: string
        values: [pending, completed, cancelled, refunded]
      - name: order_date
        column: order_date
        type: timestamp
        grains: [day, week, month, quarter, year]
    measures:
      - name: revenue
        expr: amount
        agg: sum
      - name: order_count
        expr: id
        agg: count
      - name: unique_customers
        expr: customer_id
        agg: count_distinct

relationships:
  - name: order_customer
    from: Order
    to: Customer
    cardinality: many_to_one
    keys:
      - [customer_id, id]

metrics:
  - name: avg_order_value
    expr: revenue / order_count
    description: Sum of order amounts divided by order count.
  - name: orders_per_customer
    expr: order_count / customer_count

Things the compiler cares about

  • Primary keys are mandatory. The compiler needs them to keep a measure correct when a query joins across a one-to-many relationship (fan-out).
  • No avg aggregation. “Average order value” is a metricrevenue / order_count, computed after aggregation — not avg(amount) on raw rows.
  • count_distinct is non-additive. It does not freely re-aggregate across time or dimensions the way sum and count do.
  • Relationship names are unique model-wide. If two entities connect more than one way (bill_to, ship_to), each edge needs its own name so a query can pick one.

Apply, then publish

Two separate actions, on purpose:

  1. Apply draft writes your changes into the working model and validates it. Errors block here, not in production.
  2. Publish freezes the current draft into an immutable artifact — the model plus its policy bundle, versioned. Consumers only ever see published artifacts.

Next: Query the API.