Semantic Layer
A metric is a business contract
Section titled “A metric is a business contract”A metric is not merely a column or query result. It is a versioned agreement about the business number an agent is allowed to analyze: what it means, what it excludes, how it is calculated, and where it may be used. Marivo can test whether the declared object and matching preview evidence are technically ready; only the accountable business owner can approve whether the contract is correct for the intended decision.
Where the semantic layer comes from
Section titled “Where the semantic layer comes from”Use a semantic layer already maintained by your team, or build one for a new project.
Clone an existing project
Section titled “Clone an existing project”When models/ contains semantic objects, reuse them. Install dependencies,
configure local environment variables, and ask the agent to check whether the
objects fit the current question. If there is no gap, start analysis without
regenerating objects. Run scoped readiness when the cloned model changed or a
workflow requests technical certification.
Start a new project
Section titled “Start a new project”Give the agent the datasource, business goal, and decided rules. The agent uses
marivo-semantic to inspect evidence and draft objects;
the user confirms and adjusts business meaning. Technical validation is not
business approval.
marivo-semantic supplies the stable ordering and ownership boundaries. Current
constructor placement, signatures, prerequisites, loaded-object lookup, and
error recovery come from marivo.help(...), result cards, .contract(), and
structured errors. Before reading user data, the agent must ask for required
non-observable inputs such as the accountable domain owner or target business
concept. Sampling cannot answer those questions.
python -m marivo help only confirms the active interpreter, installed package
version, and environment fingerprint before handing off to Python.
marivo.help(...) is the sole public focused-help coordinator. Qualified
datasource.*, semantic.*, and analysis.* content remains owned by its
native registry; md and ms execute domain operations and intentionally have
no .help() aliases.
Datasource authoring errors display a stable code and stage. When bounded acquisition fails during execution, retry that exact acquisition at most once only when the caller’s remaining data-access budget permits it. Caller-provided read-count, row, and timeout limits take precedence. If the same structured code and backend name recur, stop and report the backend blocker instead of resampling or bypassing Marivo.
Maintain and share the semantic layer with Git
Section titled “Maintain and share the semantic layer with Git”Semantic objects are Python source files under models/ and belong in version control:
- team members clone the project to reuse the same definitions;
- use a branch and pull request for definition changes, showing owner,
ai_context,guardrails, and affected metrics; - review business meaning and owner approval, not only whether Python runs;
- commit datasource
*_envreferences, never credential values; - do not commit
.marivo/runtime state, local caches, or generated secrets; - after a semantic change, ask the agent to check configuration and scoped readiness.
Git makes declarations reviewable and shareable. It does not grant business approval or prove current data is ready.
Work with the agent on business meaning
Section titled “Work with the agent on business meaning”The agent can inspect datasource evidence, draft semantic objects, validate them, and explain unresolved choices. You remain responsible for deciding whether a metric represents the business outcome you intend to analyze.
When asking the agent to create or revise a metric, share the rules that matter to the business: important inclusions or exclusions, the relevant event time, the unit, and known limits on how the metric may be used. You do not need to translate those rules into Marivo fields or Python.
For example:
Add a completed-order revenue metric for this project. Exclude cancelled and fully refunded orders, use the order completion time, and ask me about any business rule that cannot be established from the project.
The agent uses marivo-semantic to inspect evidence and handle the authoring
workflow. Review the proposed definition in business terms: does it measure the
right outcome, and are the important boundaries visible? Correct the meaning
before approving it. Passing validation or readiness does not approve the
business definition on your behalf.
When the agent should pause
Section titled “When the agent should pause”The agent should ask for input when available evidence cannot settle a business choice or when two plausible definitions would produce materially different results. Technical declaration details, previews, and readiness checks do not need separate user instructions.
How Marivo represents the contract
Section titled “How Marivo represents the contract”Datasource → Domain → Entity → dimensions / time dimension / measures → metricPython declarations are the contract. Decorator bodies return ibis expressions,
not raw SQL strings. SQL is metadata only, through
provenance=ms.from_sql(sql=..., dialect=...), for example when checking
parity with an established query.
This small representation has one orders entity, a region dimension, an
order_date time dimension, an amount measure, and a revenue
aggregate metric. Its concise declarations illustrate the object graph; production
objects also need the user-approved ai_context described below:
import marivo.datasource as mdimport marivo.semantic as ms
sales = ms.domain(name="sales", owner="Mina Zhang")warehouse = ms.ref.datasource("warehouse")orders = ms.entity(name="orders", datasource=warehouse, source=md.table("orders"), primary_key=["order_id"])region = ms.dimension_column(name="region", entity=orders, column="region")order_date = ms.time_dimension_column(name="order_date", entity=orders, column="order_date", granularity="day", is_default=True)amount = ms.measure_column(name="amount", entity=orders, column="amount", additivity="additive", unit="CNY")revenue = ms.aggregate(name="revenue", measure=amount, agg="sum")After user approval and loading, routine analysis reads the metric and observes it:
revenue_entry = catalog.metrics.get("sales.revenue")revenue_entry.details().show()frame = session.observe(revenue_entry, time_scope={...}, grain="month")If the metric was just authored or changed, finish the scoped authoring closeout described below before this routine analysis sequence.
Model reusable events
Section titled “Model reusable events”An Event gives one business occurrence a reusable identity, occurrence time,
and named participant roles. The source Entity is inferred from
owner(occurred_at), so Event authoring does not repeat an entity argument.
Every Event uses the single @ms.event(...) decorator form.
For an Entity where each row is one order creation, the Event has no additional
row filter and explicitly returns ms.all_rows():
order_id = ms.dimension_column( name="order_id", entity=orders, column="order_id",)
@ms.event( name="order_created", identity=(order_id,), occurred_at=order_date, participants=( ms.participant(name="order", cardinality="one"), ), ai_context=ms.ai_context( business_definition="An order creation occurrence." ),)def order_created(order_rows): return ms.all_rows()For a filtered Event on a shared event-log Entity, return the restricted
boolean expression instead, for example
ms.bind(event_type, event_rows) == "payment_succeeded". Do not use a
body-free constructor, bare True, or a separate filtered-Event API.
ms.participant_role(event=order_created, name="order") resolves the typed
role used by Event analysis. Omitting path means the Event source Entity
itself is the participant; a non-empty path follows declared Relationships to
another Entity. A role used as the analysis subject must have
cardinality="one". Its endpoint Entity’s declared primary key is the subject
identity, so callers do not provide that identity again.
Model a normative lifecycle
Section titled “Model a normative lifecycle”A StateModel declares the allowed states and Event-driven transitions for one subject Entity. It describes business meaning only. Replay windows, seeds, completeness assumptions, cohorts, and observed violations belong to analysis, not to the StateModel.
For example, assume the order source also exposes a paid timestamp and status:
paid_at = ms.time_dimension_column( name="paid_at", entity=orders, column="paid_at", granularity="second",)order_status = ms.dimension_column( name="order_status", entity=orders, column="status",)
@ms.event( name="order_paid", identity=(order_id,), occurred_at=paid_at, participants=( ms.participant(name="order", cardinality="one"), ), ai_context=ms.ai_context( business_definition="An order entered the paid state." ),)def order_paid(order_rows): return ms.bind(order_status, order_rows) == "paid"
created = ms.lifecycle_state(name="created", initial=True)paid = ms.lifecycle_state(name="paid", terminal=True)
order_lifecycle = ms.state_model( name="order_lifecycle", subject=orders, states=(created, paid), transitions=( ms.inception(on=order_created), ms.transition( from_state=created, on=order_paid, to_state=paid, ), ), ai_context=ms.ai_context( business_definition="Normative commercial order lifecycle." ),)State names are local immutable authoring values. Exactly one state is initial;
terminal states cannot have outgoing transitions. An Event trigger is inferred
only when exactly one cardinality="one" participant ends at the StateModel
subject. If several roles qualify, pass the exact
ms.participant_role(...) handle instead of repeating Event and role fields.
After loading, use ms.model_state(model=order_lifecycle, name="paid") to
construct the typed state identity accepted by analysis. A StateModel may be
valid without an inception for future projection-based seeding, but it is not
ready for replay from inception. Use marivo.help("semantic.state_model") for
the current constructor contract and the catalog entry’s
verify/preview/readiness continuations for current-state guidance.
Build the model
Section titled “Build the model”1. Connect a datasource
Section titled “1. Connect a datasource”Datasources are declared in models/datasources/*.py with a typed helper per
backend. The helper registers the connection; it does not return a value. Semantic
files refer to it through the exact ms.ref.datasource("warehouse") factory.
import marivo.datasource as mdimport marivo.semantic as ms
md.duckdb( name="warehouse", path="warehouse.duckdb", ai_context=ms.ai_context( business_definition="Local DuckDB warehouse for sales analysis.", guardrails=["Use only for development or approved local analysis."], ),)Every helper (md.duckdb, md.sqlite, md.mysql, md.postgres, md.trino, md.clickhouse)
shares these parameters:
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
name | str | Yes | — | Global lowercase snake_case datasource name. Used by ms.ref.datasource(name). |
ai_context | AiContextValue | No | None | Agent-facing context, via ms.ai_context(...). |
extra | dict | No | None | Rare JSON-safe ibis keyword arguments the typed helper does not model. |
Backend-specific parameters:
| Helper | Required | Optional |
|---|---|---|
md.duckdb | — | path (default ":memory:"), read_only (default False) |
md.sqlite | — | path (default ":memory:"), read_only (default False), type_map |
md.mysql | host, database | port (3306), autocommit, user_env, password_env |
md.postgres | host, database | port (5432), schema, autocommit, user_env, password_env |
md.trino | host, catalog | port (8080), schema, source, timezone, http_scheme, client_tags, session_properties, user_env, auth_env |
md.clickhouse | host | port (9000 / 9440 secure), database, secure, settings, user_env, password_env |
ClickHouse datasources disable clickhouse-connect’s autogenerated session id by
default so cached analysis backends can run repeated or concurrent queries
without sharing a server session lock. If a datasource needs ClickHouse session
state such as temporary tables, opt in explicitly with
extra={"autogenerate_session_id": True}. Keep server/query settings in
settings={...}; client keyword arguments belong in extra={...}.
Marivo’s bounded read-only paths enforce the ClickHouse server setting
readonly=1; they preserve other declared settings and never retry through a
write-enabled connection.
SQLite datasources use md.table(...) for tables and views. read_only=True
enables SQLite’s connection-level query-only mode, and type_map={...} can map
declared SQLite type names to Ibis type strings when affinity inference is not
enough. Parquet, CSV, and JSON descriptors remain DuckDB-only. SQLite does not
support median or percentile metrics, or string strptime expressions in the current Ibis
backend; Marivo reports these as structured unsupported-operation errors. Use a
supported aggregation and a native temporal column instead.
import marivo.datasource as md
md.trino( name="lake", host="trino.example.internal", catalog="hive", user_env="TRINO_USER", auth_env="TRINO_AUTH",)2. Declare a domain and entity
Section titled “2. Declare a domain and entity”Domain
Section titled “Domain”ms.domain(...) opens a namespace. Call it once per _domain.py. It returns a
Ref[domain] you can pass as domain= to override the active domain for an object
declared in a sibling file.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
name | str | Yes | — | Domain namespace, e.g. "sales". Objects become <name>.<object>. |
owner | str | Yes | — | Human owner accountable for semantic correctness and quality, e.g. "Mina Zhang". |
default | bool | No | True | When True, decorators in this file resolve to this domain unless domain= is passed. |
ai_context | AiContextValue | No | None | Agent-facing context, via ms.ai_context(...). |
import marivo.semantic as ms
ms.domain(name="sales", owner="Mina Zhang")Entity
Section titled “Entity”An entity is one physical source (a table or file) plus its primary key. It is the anchor that dimensions, measures, and metrics attach to.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
name | str | Yes | — | Entity name. Becomes <domain>.<name>. |
datasource | Ref[datasource] | Yes | — | ms.ref.datasource("warehouse"). |
source | source builder | Yes | — | Datasource-owned md.table(...), md.parquet(...), md.csv(...), or md.json(...). |
primary_key | list[str] | No | None | Column names forming the primary key. |
versioning | ms.snapshot | ms.validity | No | None | Snapshot or SCD2 validity versioning (see below). |
domain | Ref[domain] | No | file default | Override the active domain. |
ai_context | AiContextValue | No | None | Agent-facing context, via ms.ai_context(...). |
warehouse = ms.ref.datasource("warehouse")
orders = ms.entity( name="orders", datasource=warehouse, source=md.table("orders"), primary_key=["order_id"], ai_context=ms.ai_context(business_definition="One row per order."),)Source builders
Section titled “Source builders”| Builder | Required | Optional | Use for |
|---|---|---|---|
md.table(name) | name | database | A table in the datasource (use database="schema" for Trino/MySQL). |
md.parquet(path) | path | hive_partitioning | Self-describing Parquet files through DuckDB. |
md.csv(path, schema=...) | path, typed schema | header, delimiter | CSV file source through DuckDB. |
md.json(path, schema=...) | path, typed schema | format | JSON file source, glob, or DuckDB httpfs URL. |
3. Add fields
Section titled “3. Add fields”Dimension
Section titled “Dimension”A dimension is a categorical attribute you group or filter by. For direct
physical columns, use ms.dimension_column(...); for expressions (e.g.
table.region.upper()), use the @ms.dimension decorator whose body returns a
single ibis expression over the entity table.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
name | str | Yes | — | Dimension name. Becomes <domain>.<entity>.<name>. |
entity | Ref[entity] | Yes | — | The owning entity. |
column | str | Yes | — | Physical column name on the entity table. |
domain | Ref[domain] | No | file default | Override the active domain. |
ai_context | AiContextValue | No | None | Agent-facing context, via ms.ai_context(...). |
region = ms.dimension_column( name="region", entity=orders, column="region", ai_context=ms.ai_context(business_definition="Sales reporting region."),)Time dimension
Section titled “Time dimension”A time dimension is a special dimension that carries grain and parsing metadata.
Only time dimensions can serve as the time axis for session.observe.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
name | str | Yes | — | Dimension name. |
entity | Ref[entity] | Yes | — | The owning entity. |
column | str | Yes | — | Physical column name on the entity table. |
granularity | grain literal | Yes | — | year, quarter, month, week, day, hour, minute, or second — the finest grain at which queries are meaningful. |
parse | parse variant | No | None | How the source column becomes a time value (see below). Omit for native temporal columns — the parse variant is inferred at analysis time. |
is_default | bool | No | False | Marks the default time axis when the entity has several. observe uses it when time_dimension= is omitted. |
domain | Ref[domain] | No | file default | Override the active domain. |
ai_context | AiContextValue | No | None | Agent-facing context, via ms.ai_context(...). |
Parse variants
Section titled “Parse variants”The parse= value declares the physical encoding of the column. When omitted,
the parse variant is inferred from the column’s ibis dtype at analysis time
(native date, datetime, and timestamp columns do not need an explicit parse).
For string or integer columns, provide ms.strptime(format) or
ms.hour_prefix(prefix). The variant must be compatible with granularity
(e.g. an hour grain needs a time-bearing format).
| Builder | Source column is… | Key parameters |
|---|---|---|
(omit parse) | a native temporal column | — |
ms.datetime() | a native datetime | timezone (IANA), sample_interval |
ms.timestamp() | a native timestamp | timezone (IANA), sample_interval |
ms.strptime(format) | a string/integer to parse | timezone, sample_interval |
ms.hour_prefix(prefix) | an hour-only partition | sample_interval — prefix is the day-grain Ref[time_dimension] that supplies the date |
For a native naive datetime or timestamp, declare its source timezone (for
example, ms.datetime(timezone="Asia/Shanghai")). An explicit
ms.datetime() or ms.timestamp() without timezone= makes readiness return
the undeclared_naive_time_axis blocker: runtime would otherwise fall back to
the datasource read timezone, while analysis windows use the report timezone
and may shift at day or hour boundaries. The zero-query gate reports those
runtime-resolved contexts but never guesses the business timezone.
sample_interval like (5, "minute") marks a periodically-sampled axis used
by semi-additive folds.
For time-series and panel frames, bucket_start is always a report-timezone
bucket label emitted as a timezone-naive timestamp or date. For example, with
report_timezone="Asia/Shanghai", the first hourly bucket for a Shanghai
business day is shown as 2026-06-20 00:00:00, not the equivalent UTC instant.
# Day partition stored as the string "20260131"log_date = ms.time_dimension_column( name="log_date", entity=orders, column="dt", granularity="day", parse=ms.strptime("%Y%m%d"), is_default=True,)
# Native UTC timestamp, usable for sub-day bucketsevent_ts = ms.time_dimension_column( name="event_ts", entity=orders, column="event_ts", granularity="minute", parse=ms.timestamp(timezone="UTC"),)Measure
Section titled “Measure”A measure is a row-level quantitative fact you intend to aggregate (e.g. an
amount or quantity). For direct physical columns, use ms.measure_column(...);
for expressions over one or more columns, use @ms.measure. Measures carry
additivity and an optional unit.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
name | str | Yes | — | Measure name. Becomes <domain>.<entity>.<name>. |
entity | Ref[entity] | Yes | — | The owning entity. |
column | str | Yes | — | Physical column name on the entity table. |
additivity | additivity value | Yes | — | "additive", "non_additive", or ms.semi_additive(...). |
unit | str | No | None | UCUM unit token: "USD", "CNY", "%", "ms", "{order}". |
domain | Ref[domain] | No | file default | Override the active domain. |
ai_context | AiContextValue | No | None | Agent-facing context, via ms.ai_context(...). |
amount = ms.measure_column( name="amount", entity=orders, column="amount", additivity="additive", unit="CNY",)4. Define metrics
Section titled “4. Define metrics”A metric is not just a query result. It is the analysis-ready business number an
agent may observe: its definition and calculation are declared in version
control; its entity, dimensions, time behavior, unit, and additivity describe
how it can be analyzed; ai_context states the business meaning and guardrails;
and readiness can certify new or changed definitions before they are declared
complete.
Choose the metric for the job
Section titled “Choose the metric for the job”| Need | Use | Example |
|---|---|---|
| Sum or other direct aggregation of a row-level value | ms.aggregate | revenue from amount |
| Count entities or rows | ms.count | completed orders |
| Compute a weighted mean from row-level measures | ms.weighted_mean | request-weighted latency |
| Combine metrics | ms.ratio, ms.linear | conversion rate |
| Accumulate a time series | ms.cumulative | year-to-date revenue |
| Require a custom Ibis expression | @ms.metric | a calculation not covered above |
Choose the builder that matches the task. Use @ms.metric only when the
existing builders cannot express the calculation.
Simple metric from a measure — ms.aggregate
Section titled “Simple metric from a measure — ms.aggregate”Aggregates a measure. No body; additivity is inherited from the measure.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
name | str | Yes | — | Metric name. |
measure | Ref[measure] | Yes | — | The measure to aggregate. |
agg | aggregation | Yes | — | "sum", "mean", "min", "max", … |
fold | fold | No | None | Time-fold override for semi-additive measures. |
filter | WhereFilter | No | None | AND filter from ms.where(dimension=value, ...); scalars mean equality and non-empty tuple/list values mean membership. |
unit | str | No | inherited | Override the unit derived from the measure. |
domain / ai_context | — | No | — | As elsewhere. |
revenue = ms.aggregate(name="revenue", measure=amount, agg="sum")us_revenue = ms.aggregate(name="us_revenue", measure=amount, agg="sum", filter=ms.where(region="US"))Count metrics — ms.count
Section titled “Count metrics — ms.count”Use ms.count instead of declaring a redundant measure just to count entity
rows. The helper accepts refs only and infers the metric domain from the entity
ref. Pass filter=ms.where(...) to count a subset (e.g. failures) without a
hand-written metric body.
order_count = ms.count(name="order_count", entity=orders, ai_context=ms.ai_context(business_definition="Total number of orders."))failed_count = ms.count(name="failed_count", entity=orders, filter=ms.where(state="FAILED"))ms.where(**conditions) builds AND-joined equality
(dimension=value) or membership (dimension=(value1, value2)) filters. Keys are local
semantic dimensions declared on the metric’s target entity, not arbitrary
physical columns. Scalar values are str / int / float / bool; non-empty
tuple/list values are normalized to membership. Multiple conditions are
AND-joined. Load rejects missing filter dimensions, and preview/analysis checks
literal compatibility against the resolved Ibis type before query submission.
If a valid authored literal cannot be compared with the physical dtype, both
paths raise filter_value_runtime_incompatible with
query_executed=False and declaration_preserved=True. Marivo preserves the
business literal and asks the user or business owner to confirm any code/label
mapping; it never rewrites the rule from physical types or sampled values.
Query-free static verification and semantic_static readiness can continue
while runtime evidence remains unavailable.
terminal_count = ms.count( name="terminal_count", entity=queries, filter=ms.where(type=(2, 4)),)Weighted means — ms.weighted_mean
Section titled “Weighted means — ms.weighted_mean”ms.weighted_mean accepts two measures from the same entity and owns both the
row-level multiplication and aggregation:
avg_latency = ms.weighted_mean( name="avg_latency", value=latency, weight=request_count,)It computes SUM(value * weight) / NULLIF(SUM(weight), 0) over rows where both
inputs are non-null. weight must be additive. The result inherits the value
measure’s unit and persists exact numerator / weight components for
weighted_mix attribution. It also accepts filter, unit, domain, and
ai_context.
Derived metrics — ms.ratio / ms.linear
Section titled “Derived metrics — ms.ratio / ms.linear”Body-free metrics composed from other metrics. The computation comes entirely from the components.
| Builder | Required | Computes |
|---|---|---|
ms.ratio(name, numerator, denominator) | both refs | numerator / denominator (e.g. average order value, rates) |
ms.linear(name, add, subtract) | add (≥2 terms total) | sum of add minus subtract (e.g. net = gross - refunds) |
Each also accepts unit, domain, and ai_context.
net_revenue = ms.linear(name="net_revenue", add=[gross_revenue], subtract=[refunds])aov = ms.ratio(name="aov", numerator=total_amount, denominator=orders_count)At observe time, division evaluates under a fixed zero_division="null" policy:
a bucket whose denominator (for ms.ratio) or paired weight sum (for ms.weighted_mean)
is present but zero yields a null value — never +/-inf — and the affected row
count is recorded on the observed frame as meta.zero_denominator_rows and
quality_summary.zero_denominator_rows.
Cumulative metrics — ms.cumulative
Section titled “Cumulative metrics — ms.cumulative”Use ms.cumulative(...) when the business question is “how much accumulated up to bucket t”.
The base must be a tier-1 ms.aggregate(...), ms.count(...), or
ms.weighted_mean(...) metric using sum, count, count_distinct, or
weighted-mean component accumulation. The anchor parameter selects the accumulation shape.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
name | str | Yes | — | Semantic metric name. |
base | Ref[metric] | Yes | — | Tier-1 simple aggregate metric using sum, count, or count_distinct. |
over | Ref[time_dimension] | None | No | None | Time axis to accumulate over. Pass explicitly unless the base root entity has exactly one time dimension. |
anchor | GrainToDate | Trailing | None | No | None | Accumulation anchor. None = all-history running total; ms.grain_to_date(...) = MTD/QTD/YTD resets; ms.trailing(...) = rolling N. |
unit / domain / ai_context | — | No | — | Same as other metrics. |
user_id = ms.measure_column(name="user_id", entity=events, column="user_id", additivity="non_additive")active_users = ms.aggregate(name="active_users", measure=user_id, agg="count_distinct")cumulative_active_users = ms.cumulative( name="cumulative_active_users", base=active_users, over=event_time,)For count_distinct base metrics, the cumulative value uses first-seen semantics: each
distinct entity is counted at the earliest bucket in which it appears, so the running total
is monotonically non-decreasing. Cumulative metrics may serve as ratio components — compose
cumulative numerator and denominator with ms.ratio(...) for cumulative rates.
Accumulation anchors
Section titled “Accumulation anchors”anchor=None (default) is the all-history running total: the observe window clips
displayed rows but does not reset the running value. Two value-object constructors open
the additional anchor kinds.
ms.grain_to_date(grain=...) resets the running total at each reset-grain boundary
(MTD / QTD / YTD / WTD). Within a reset period the value accumulates; at the boundary it
drops to the period’s first-bucket flow. grain is one of week, month, quarter,
year.
mtd_revenue = ms.cumulative( name="mtd_revenue", base=revenue, over=event_time, anchor=ms.grain_to_date(grain="month"),)ms.trailing(count=..., unit=...) is a fixed-size rolling window: the value at each
bucket is the base aggregation over the span ending at that bucket’s end boundary. Empty
windows are true zero, not carried forward. Partial windows (the span reaches before the
data start) show the actual partial accumulation and are marked partial in coverage.
unit accepts only fixed-size units (second, minute, hour, day, week);
calendar-variable units (month, quarter, year) are rejected with a teaching error.
rolling7_active = ms.cumulative( name="rolling7_active", base=active_users, over=event_time, anchor=ms.trailing(count=7, unit="day"),)Two cross-anchor rules govern query-time grain choices:
- Grain-compatibility rule (
grain_to_date): every display bucket must lie entirely within one reset period. Aweekquery grain under amonth/quarter/yearreset is illegal (week buckets straddle month boundaries);dayandhourare legal. - Integer-multiple rule (
trailing): the window span must be an integer multiple of the query grain (W_buckets = span / grain). Trailing requires a time grain.
Derived metrics may compose cumulative components. Compare is supported only when every
outer component is cumulative and all components use the same trailing or
grain_to_date anchor. all_history, mixed anchors, and cumulative/non-cumulative mixes
are rejected; attribute, decompose, and forecast remain unsupported.
Use and check the model
Section titled “Use and check the model”Read a metric before an agent uses it
Section titled “Read a metric before an agent uses it”import marivo.analysis as mvimport marivo.semantic as ms
session = mv.session.get_or_create( name="revenue-investigation", question="Why did Q4 revenue drop?",)catalog = session.catalogrevenue_entry = catalog.metrics.get("sales.revenue")region_entry = catalog.dimensions.get("sales.orders.region")
revenue_entry.details().show()current = session.observe(revenue_entry, grain="month", dimensions=[region_entry])details().show() exposes the human-authored definition, guardrails, composition,
effective entities, candidate dimensions/time dimensions, and measure lineage.
Candidate axes come from the metric’s effective entities; session.observe(...)
still validates cross-entity relationships and fanout. Use .contract().show()
only for mechanical verify/preview/readiness continuations. .show() prints the
same bounded card that .render() returns. If the object is missing or disputed,
return to semantic authoring. Otherwise the observation becomes the first artifact
in the session’s evidence-backed investigation. Newly authored or changed objects
complete scoped readiness as part of their authoring closeout.
Load the model and certify changes
Section titled “Load the model and certify changes”Once declarations are in place, load the catalog and inspect it:
import marivo.semantic as ms
catalog = ms.load() # SemanticCatalogcatalog.show() # all typed collections and countscatalog.domains.show() # top-level domainscatalog.metrics.show() # just metrics across all domains
sales = catalog.domains.get("sales")orders = sales.entities.get("orders")revenue = orders.metrics.get("revenue") # one metric objectregion = orders.dimensions.get("region") # one dimension objectEach typed collection accepts a local name, an exact full path, or a same-kind
ref within that collection’s current scope. Scoped collections never return an
out-of-scope object through a path or ref. Use the strict ref-only
catalog.require(ref) when identity comes from configuration, persistence, or
logs. Entry cards show the exact kind, full path, .ref, and bounded current
axes; omitted members point to details().show().
The global collections are domains, datasources, entities, dimensions,
time_dimensions, measures, metrics, relationships, events, and
state_models. repr(catalog) points to the same bounded, zero-query catalog
card that catalog.show() prints. Collection help exposes the exact
.items, .refs, .get(...), .render(), and .show() surface; lookup
remains exact and does not use fuzzy search.
After authoring or changing an object, run readiness to certify that the selected definition and its scoped runtime evidence are current:
catalog.verify(revenue).show()catalog.preview(revenue, using=snapshot).show()report = catalog.readiness(refs=[revenue, region])if report.status == "blocked": report.show() # blockers, with the next step for eachThese runtime calls accept the current entry or its exact ref and normalize to the ref immediately. Readiness output, preview evidence, persistence, replay, and recovery stay ref-based.
Two more checks support authoring:
ms.richness()— advisory coverage/depth report; never blocks.ms.parity_check("sales.revenue")— runs the metric against itsprovenanceSQL and compares results. Requiresprovenance=ms.from_sql(...).
For how readiness decides what is “ready,” see Readiness. For how analysis records what it concludes, see Evidence. Analysis sessions load and validate metric inputs at runtime. Use scoped readiness to certify semantic changes, then continue to the Analysis Workflow.
Advanced modeling
Section titled “Advanced modeling”Semantic contract reference
Section titled “Semantic contract reference”You work through two namespaces:
import marivo.datasource as md # connections and physical sourcesimport marivo.semantic as ms # meaning (ms.entity, ms.metric, ...)How names work
Section titled “How names work”Every object lives under a domain and is addressed by a qualified ref:
- Domain-level objects:
<domain>.<object>— e.g.sales.revenue,sales.orders. - Entity-scoped objects (dimensions and measures):
<domain>.<entity>.<field>— e.g.sales.orders.region.
A project is a folder of declaration files. Datasources are declared once under
models/datasources/; semantics live under models/semantic/<domain>/, with a
_domain.py per domain:
your-project/ marivo.toml models/ datasources/ warehouse.py # md.duckdb(name="warehouse", ...) semantic/ sales/ _domain.py # ms.domain(name="sales", owner="Mina Zhang") + entities, metrics, ...For multi-repository semantic layers, keep each business-domain package in the
same authored models/ layout and reference those roots from the central
analysis project:
[semantic]layer_paths = ["../finance-domain/models"]Marivo loads the central project’s models/ first, then each configured external
models/ root. All loaded datasources, domains, and semantic objects share one
catalog, so names must stay globally unique. marivo doctor, catalog.preview,
catalog.verify, and analysis sessions resolve datasource-backed semantic
objects against the same configured roots.
Semantic refs
Section titled “Semantic refs”Every semantic object is identified by one sealed, immutable ms.Ref value.
The runtime class is always exactly Ref; the generic kind (Ref[metric],
Ref[dimension], and so on) gives static type checkers the role. Create refs
only through an exact factory such as ms.ref.metric("sales.revenue") or from
an authoring declaration that returns the same value type.
All refs expose these read-only attributes:
| Attribute | Type | Meaning |
|---|---|---|
.path | str | Kind-relative qualified path, for example "sales.revenue". |
.kind | SemanticKind | Object kind — one of the eight values below. |
.key | str | Canonical runtime key, for example "metric:sales.revenue". |
.name | str | Final local-name segment. |
str(ref) returns .key; equality and hashing use (kind, path). Ref(...)
cannot be called directly and Ref cannot be subclassed. Public boundaries do
not accept strings or catalog entries as substitutes for refs.
| Kind | Factory | Typical authoring producer | Expression binding? |
|---|---|---|---|
domain | ms.ref.domain(path) | ms.domain(...) | No |
datasource | ms.ref.datasource(path) | datasource declaration .ref | No |
entity | ms.ref.entity(path) | ms.entity(...) | No |
dimension | ms.ref.dimension(path) | ms.dimension_column(...), @ms.dimension | ms.bind(ref, entity_alias) inside a bound expression body |
time_dimension | ms.ref.time_dimension(path) | ms.time_dimension_column(...), @ms.time_dimension | ms.bind(ref, entity_alias) inside a bound expression body |
measure | ms.ref.measure(path) | ms.measure_column(...), @ms.measure | ms.bind(ref, entity_alias) inside a bound expression body |
metric | ms.ref.metric(path) | ms.aggregate(...), @ms.metric, ms.ratio(...), … | No |
relationship | ms.ref.relationship(path) | ms.relationship(...) | No |
Ref values are never callable. Inside an expression body, bind a field ref to
the declared entity alias explicitly, for example ms.bind(amount, orders).
The loader records that binding against the compiled catalog. Binding outside
that body, with the wrong alias, or binding a non-field ref fails with a
teaching error.
An authoring ref can be passed directly to analysis. When starting from a literal identity, require catalog membership once and then pass the current entry directly:
revenue = ms.aggregate(name="revenue", measure=amount, agg="sum")frame = session.observe(revenue, time_scope={...})
revenue_entry = catalog.require(ms.ref.metric("sales.revenue"))frame = session.observe(revenue_entry, time_scope={...})catalog.require(ref) is the exact global lookup. Catalog collections provide
browsing plus local/full-path lookup. Frozen analysis consumers accept a current
catalog entry or its exact ref; authoring and persisted/configuration boundaries
remain ref-based. Persisted payloads encode refs as the versioned
{schema, kind, path} record; raw identity strings are not a public interchange
format.
ai_context: the human-to-agent contract
Section titled “ai_context: the human-to-agent contract”Every semantic object and datasource accepts an optional ai_context parameter,
constructed with ms.ai_context(...). This is where business meaning and
guardrails live — the context an agent reads before it uses the object. All
parameters are optional, but unknown keyword arguments are rejected by Python
at call time.
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
business_definition | str | No | None | What the object means in business terms, in a sentence or two. |
guardrails | list[str] | No | [] | Rules an agent must respect: required filters, exclusions, scope limits. |
ai_context=ms.ai_context( business_definition="Gross order amount before refunds.", guardrails=["Validate refund exclusions before using as net revenue."],)Versioning (optional)
Section titled “Versioning (optional)”For entities whose rows change over time, declare how to read the current state:
ms.snapshot(partition_field, grain="day", timezone=None, format=None)— daily partitioned snapshots;partition_fieldmust be aRef[dimension]orRef[time_dimension].ms.validity(valid_from, valid_to, interval, open_end, timezone=None)— SCD2 validity intervals;valid_fromandvalid_tomust beRef[dimension]orRef[time_dimension]values.intervalis"closed_open"([from, to)) or"closed_closed";open_endlists the sentinel values that mean “still current” (e.g.(None,)for SQLNULL, or("9999-12-31",)).
Custom Ibis metric — @ms.metric
Section titled “Custom Ibis metric — @ms.metric”Use this escape hatch when the metric cannot be expressed as ms.aggregate(...),
ms.count(...), or a derived metric builder. The body returns one ibis
aggregation, and you declare additivity directly.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
name | str | No | function name | Metric name. |
entities | list[Ref[entity]] | Yes | — | Entities the body reads. |
additivity | additivity value | Yes | — | "additive", "non_additive", or ms.semi_additive(...). |
root_entity | Ref[entity] | No | the single entity | Required when entities has more than one. |
fanout_policy | "block" | "aggregate_then_join" | No | "block" | How to handle join fan-out across entities. |
unit | str | No | None | UCUM unit token. |
provenance | SqlProvenance | No | None | ms.from_sql(sql=..., dialect=...) for parity checking. |
domain / ai_context | — | No | — | As elsewhere. |
@ms.metric( entities=[orders], additivity="additive", name="revenue", provenance=ms.from_sql( sql="SELECT SUM(amount) AS revenue FROM orders", dialect="duckdb", ), ai_context=ms.ai_context(business_definition="Gross order amount before refunds."),)def revenue(table): return table.amount.sum()Correctness rules for analysis
Section titled “Correctness rules for analysis”Additivity and time behavior determine whether a number remains meaningful when
an agent changes the analysis grain or segments it. SQL provenance does not
execute a metric; it gives ms.parity_check(...) a reference query for checking
that the declared semantics match an established implementation.
ms.semi_additive(over, fold)— for snapshot/status facts that are additive across most axes but folded over a time axis.overis the status time dimension ref returned by@ms.time_dimension(...);foldis"last","first","mean","max", or("percentile", 0.95).ms.from_sql(sql, dialect)— attaches SQL as provenance only, enablingms.parity_check(...). It is never executed as the metric body.
snapshot_date = ms.time_dimension_column( name="snapshot_date", entity=inventory_daily, column="snapshot_date", granularity="day",)
on_hand_units = ms.measure_column( name="on_hand_units", entity=inventory_daily, column="on_hand_units", additivity=ms.semi_additive(over=snapshot_date, fold="last"),)Relationship
Section titled “Relationship”Declares how two entities join, so metrics and dimensions can reach across them. Keys are dimension refs, not raw column names.
| Parameter | Type | Required | Default | Meaning |
|---|---|---|---|---|
name | str | Yes | — | Relationship name. |
from_entity | Ref[entity] | Yes | — | Source entity. |
to_entity | Ref[entity] | Yes | — | Target entity. |
keys | list[JoinKey] | Yes | — | One or more ms.join_on(from_key, to_key) pairs. |
domain / ai_context | — | No | — | As elsewhere. |
ms.relationship( name="orders_to_customers", from_entity=orders, to_entity=customers, keys=[ms.join_on(order_customer_id, customer_id)],)Inspect, sample once, then author
Section titled “Inspect, sample once, then author”For datasource-backed authoring, use one explicit evidence snapshot.
Consult marivo.help("datasource.authoring") for datasource inspection and
scope, marivo.help("semantic.authoring") for the semantic dependency ladder,
and marivo.help("datasource.ai_context") for the shared
ms.ai_context(...) contract:
warehouse = ms.ref.datasource("warehouse")orders = md.table("orders")
inspection = md.inspect(warehouse, orders)inspection.show()inspection.partitions().show()scope = md.partition({"dt": "20260710"}, max_rows=1000, timeout_seconds=30)snapshot = inspection.sample( scope=scope, columns=("order_id", "region", "created_at", "amount"),)snapshot.entity(columns=("order_id",)).show()snapshot.dimensions(columns=("region",)).show()snapshot.time_dimensions(columns=("created_at",)).show()snapshot.measures(columns=("amount",)).show()Inspection is metadata-only and reports schema, physical extent, partition state,
and enforceable capabilities before sampling. Scope requires positive row and
timeout guards plus explicit columns. LIMIT does not guarantee bytes scanned.
For a ClickHouse Distributed source, local system.parts observations are
shown only as scope=local_node_only notes; source-wide rows and bytes remain
unknown, and Marivo does not issue a cluster-wide fanout query.
md.duckdb(...) declares the datasource. md.table(...) selects an internal
table or view inside that datasource. md.parquet(...), md.csv(...), and
md.json(...) are DuckDB file source descriptors used with the datasource ref;
they are not datasource declarations.
events = md.json("data/events/*.json", schema={"event_id": "string"}, format="newline_delimited")orders_parquet = md.parquet("data/orders/*.parquet")orders_csv = md.csv("data/orders/*.csv", schema={"order_id": "string"})
md.inspect(warehouse, events).show()Snapshot projections are query-free and expose complete structured evidence even
when rendering is bounded. Values are memory-only by default; use
persist_values=True only when bounded plaintext project-local caching is
acceptable. Uncommon formats, keys, timezone, aggregation, unit, additivity,
relationship cardinality, and business meaning remain agent-owned.
Inspection cards include the exact source descriptor and real schema names and
types. Partition cards report the captured value source, completeness,
truncation, bounded values, and a scope template without issuing another query.
The snapshot card reports its scope, selected columns, coverage, and value/cache
state; every projection is explicitly data_access=none.
Use md.test(ref).show() as the connection-test stop point. A failed
DatasourceTestResult exposes a structured .failure, a focused .repair, and
a blocked validate_connection transition from .contract(). Snapshot
connection, source-resolution, timeout, and execution failures likewise remain
structured and state whether a query was executed; backend messages are bounded
and sanitized.
When evidence cannot mechanically settle a decision,
result.contract().judgment_requirements exposes frozen requirements with
stable IDs, subjects, evidence IDs, and
authority="user_or_business_owner". They do not recommend a value, create a
constructor transition, or record approval. Reuse the same snapshot for all
local projections. Reacquire only for missing required columns or value
evidence, staleness, or a datasource/source/scope identity mismatch.
Focused constructor help describes each call as a declaration fragment inside
its reported loader path. After loading, use the generated catalog handoff:
catalog = ms.load(), entry = catalog.<collection>.get(...),
entry.show(), and entry.contract().show().
After one Python declaration, reload its current catalog entry and run
catalog.verify(entry), catalog.preview(entry, using=snapshot), then
zero-query catalog.readiness(refs=[entry]). Use entry.ref only when an exact
persisted, logged, or configuration identity is required.
When readiness reports several missing runtime previews, repair them through
the same entry point with
catalog.preview_many(report.preview_required_refs, using=snapshot_or_mapping).
This batches compatible queries but preserves independent evidence for every ref;
it is not a batch-authoring shortcut.