Freshness contracts (the OBML refresh: block)¶
A freshness contract describes how the physical table that a dataObject maps to refreshes. It's the input to OBSL's :doc:result cache <result-cache> TTL composition. Authored once per source table.
Where it lives¶
On the dataObject, alongside code, database, schema. Two dataObject entries that map to the same physical table should declare equivalent contracts; if they disagree, OBSL emits a SHARED_TABLE_CONTRACT_DISAGREEMENT warning at load time and applies the strictest.
dataObjects:
Orders:
database: WAREHOUSE
schema: PUBLIC
code: ORDERS
refresh:
mode: interval
interval: 1h
anchor: "00:00"
timezone: UTC
columns:
...
Modes¶
| Mode | When to use | Required fields |
|---|---|---|
interval |
Table refreshes on a fixed cadence (hourly batch, daily ETL, etc.) | interval |
heartbeat |
Table refreshes irregularly; an external job pings the heartbeat endpoint after each refresh | max_staleness |
static |
Table effectively never changes (lookup tables, country codes) | none |
A dataObject without a refresh: block is treated as unknown. By default, queries touching unknown-freshness tables are not cached (CACHE_UNKNOWN_FRESHNESS_POLICY=no_cache).
Field reference¶
Interval mode¶
| Field | Type | Default | Description |
|---|---|---|---|
interval |
duration | required | ISO 8601 (PT1H, P1D) or shorthand (1h, 15m, 1d). Sub-second values rejected. |
anchor |
HH:MM string |
null | Optional time-of-day anchor. With no anchor, "next refresh" = last_observed + interval. With an anchor, refresh boundaries align to the anchor in timezone. |
timezone |
IANA TZ | UTC |
Only used when anchor is set. |
Heartbeat mode¶
| Field | Type | Default | Description |
|---|---|---|---|
max_staleness (alias maxStaleness) |
duration | required | Maximum time between heartbeats before the table is considered stale. The cache TTL is max_staleness - time_since_last_heartbeat. |
Static mode¶
No fields. Use sparingly — appropriate for true reference data only. Static tables don't constrain TTL composition; a query whose every touched table is static caches up to CACHE_MAX_TTL_SECONDS.
Multi-fact, one source¶
The cleanest case for source-level contracts:
dataObjects:
Sales:
database: WAREHOUSE
schema: PUBLIC
code: ORDERS
filter: "is_return = false"
refresh:
mode: heartbeat
maxStaleness: 5m
Returns:
database: WAREHOUSE
schema: PUBLIC
code: ORDERS
filter: "is_return = true"
refresh:
mode: heartbeat
maxStaleness: 5m
Sales and Returns are two semantic facets on top of the same physical table. A multi-fact CFL query touching both collapses to one physical table reference (WAREHOUSE.PUBLIC.ORDERS). One heartbeat to that table invalidates every cached query depending on it — even if the query went through both Sales and Returns.
OBSL graph integration¶
The contract is exposed in the model's RDF graph as obsl:hasRefreshPolicy on obsl:DataObject:
PREFIX obsl: <https://ralforion.com/ns/obsl#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?label ?database ?schema ?code ?mode ?interval WHERE {
?do a obsl:DataObject ;
rdfs:label ?label ;
obsl:database ?database ;
obsl:schema ?schema ;
obsl:code ?code ;
obsl:hasRefreshPolicy ?policy .
?policy obsl:refreshMode ?mode .
OPTIONAL { ?policy obsl:refreshInterval ?interval }
}
Agents can ask the model what it expects of its sources — the freshness contract is data, not configuration.
Validation¶
| Code | When |
|---|---|
REFRESH_PARSE_ERROR |
refresh.mode missing or invalid; interval mode missing interval; heartbeat mode missing max_staleness. |
SHARED_TABLE_CONTRACT_DISAGREEMENT |
Two dataObjects on the same physical table declare disagreeing refresh blocks. Warning, not error — the strictest contract is applied and the query keeps working. |
The strictness ordering: unknown > heartbeat > interval > static. Within the same mode, the smaller window wins (shorter interval, smaller max_staleness).