# Data Contract: [Dataset or data product output port name] [version]

> Agreement between the producer of a dataset and its consumers: who owns it, what the schema means, which quality rules and service levels it guarantees, where it is served, who may access it, what it costs, how AI tools may use it, and how it may change. The structure and field names follow the Bitol Open Data Contract Standard (ODCS) v3.2.0 (released 2026-09-08, Apache-2.0), so the prose document and the YAML file in section 15 describe the same thing and the YAML can be validated against the published JSON schema. Replace every `[bracketed]` value and every row marked *Example*.

<!--
Guidance: How to use this template
- One contract per dataset that leaves its producing team: usually one output port of a data product, sometimes one table or one topic. Write it with the main consumers in the room. A contract that only the producer agreed to is a wish list.
- Keep this document and the ODCS YAML file (section 15) in the same repository and change them together in one reviewed pull request. CI validates the YAML against the ODCS JSON schema for the `apiVersion` you declare.
- Every quality rule and every service level property must be checked automatically. Write down which job enforces it and where the result is published.
- Breaking changes follow section 12 without exception. Most data incidents start with an unannounced column change.
- Organisation-wide rules (dimension names, severity levels, default thresholds) live in the Data Quality Standard (`data/data-quality-standard`); this contract applies them to one dataset. The product view (ports, owners, lifecycle of the whole product) lives in the Data Product Specification (`data/data-product-specification`). How the data is produced lives in the Data Pipeline Specification (`data/data-pipeline-spec`).
- The worked examples use Corvid Commerce, a fictional SaaS retailer whose lakehouse runs on Apache Iceberg tables transformed with dbt. The contract described is the `orders` output port (version 2.1.0, contract ID `corvid-orders-orders`) of its "Orders" data product, owned by the Sales domain. Contract IDs in the Corvid examples follow `corvid-<product>-<port>`, the same IDs the Data Product Specification uses in its port register.
-->

## Document control

<!-- Guidance: Identify the contract, its ODCS identifiers and the people accountable for it. The contract version (ODCS `version`) changes with the dataset; the document version changes with this text. Keep both. -->

| Field | Value |
|---|---|
| Document ID | [DC-ORD-001] |
| Contract ID (ODCS `id`) | [Stable identifier, for example a UUID or `corvid-orders-orders`] |
| Contract name (ODCS `name`) | [orders] |
| Contract version (ODCS `version`) | [2.1.0] |
| Contract status (ODCS `status`) | [proposed / draft / active / deprecated / retired] |
| Specification | Bitol Open Data Contract Standard v3.2.0 (`apiVersion: v3.2.0`, `kind: DataContract`) |
| Domain (ODCS `domain`) | [Sales] |
| Tenant (ODCS `tenant`) | [`corvid-commerce`] |
| Data product and output port | [Orders data product, output port `orders`] (see `data/data-product-specification`) |
| Producer owner | [@producer-owner], [team] |
| Data steward | [@steward] |
| Document version | [1.0] |
| Status of this document | [Draft / In review / Approved / Superseded] |
| Owner | [@producer-owner] |
| Approvers/reviewers | [@data-steward], [@lead-consumer], [@privacy-officer], [@platform-lead] |
| Classification | [Internal] |
| Effective from | YYYY-MM-DD |
| Last updated | YYYY-MM-DD |
| Repository path | [git URL and path of this file and the YAML] |

### Approvals

| Role | Name | Decision | Date |
|---|---|---|---|
| Producer owner | [@name] | [Approve / Approve with conditions / Reject] | YYYY-MM-DD |
| Data steward | [@name] | [Decision] | YYYY-MM-DD |
| Lead consumer (High criticality) | [@name] | [Decision] | YYYY-MM-DD |
| Privacy officer (when personal data is in scope) | [@name] | [Decision] | YYYY-MM-DD |

### Revision history

| Version | Date | Author | Change summary |
|---|---|---|---|
| 1.0 | YYYY-MM-DD | [@name] | *Example:* First active contract, ODCS v3.1.0 |
| 2.0 | 2026-10-24 | Template maintainers | Updated to ODCS v3.2.0: Document control, AI usage context, synonyms, enumerations, semantic roles, deprecation flags, variables per environment, Iceberg server type, pricing and traceability |
| [x.y] | YYYY-MM-DD | [@name] | [Change summary] |

## Contents

- [1. Fundamentals](#1-fundamentals)
- [2. Schema](#2-schema)
- [3. Semantic roles, synonyms and enumerations](#3-semantic-roles-synonyms-and-enumerations)
- [4. Data quality rules](#4-data-quality-rules)
- [5. Service level properties](#5-service-level-properties)
- [6. Servers and environments](#6-servers-and-environments)
- [7. Team, roles and access](#7-team-roles-and-access)
- [8. Support and communication](#8-support-and-communication)
- [9. Pricing](#9-pricing)
- [10. AI usage context](#10-ai-usage-context)
- [11. Consumers](#11-consumers)
- [12. Change management, versioning and deprecation](#12-change-management-versioning-and-deprecation)
- [13. Authoritative definitions and custom properties](#13-authoritative-definitions-and-custom-properties)
- [14. Traceability matrix](#14-traceability-matrix)
- [15. ODCS YAML skeleton](#15-odcs-yaml-skeleton)
- [16. Glossary and references](#16-glossary-and-references)
- [Standard and licence note](#standard-and-licence-note)

## 1. Fundamentals

<!-- Guidance: Describe the dataset in plain words: what it represents, why it exists, what it may be used for and its known limitations. These map to the ODCS `description` object (`purpose`, `usage`, `limitations`). Pitfall: writing "all analytics" as the intended use, which makes every consumer's assumption a promise. -->

The fundamentals section tells a new consumer in one minute whether this dataset is the right one. ODCS places these facts at the top of the contract: identity (`id`, `name`, `version`, `status`), ownership context (`domain`, `tenant`, `tags`) and the human description. Write the purpose as a single sentence that states the grain and the business event. Then separate the uses the producer supports from the uses the producer explicitly does not support, because an unsupported use that silently fails is the producer's reputational problem even when it is not the producer's fault.

### 1.1 Purpose

[Example: "One row per confirmed customer order on Corvid Commerce storefronts, used for revenue reporting, demand forecasting and customer analytics."]

### 1.2 Usage and limitations

| Item | Description |
|---|---|
| Intended uses (`description.usage`) | *Example:* Daily revenue reporting; customer cohort analysis; source of the `orders_daily_agg` port that forecasting models use |
| Not suitable for | *Example:* Real-time fraud decisions (data is up to 60 minutes old); statutory financial reporting (use the general ledger) |
| Known limitations (`description.limitations`) | *Example:* Orders before 2022-01-01 lack `channel`; marketplace orders arrive up to 24 hours late |
| Tags (`tags`) | *Example:* `orders`, `sales`, `gold`, `PD`, `PSEUDO` (personal data flags from the Data Classification Policy) |

## 2. Schema

<!-- Guidance: List every schema object (table, view, topic, file) and every property with type, meaning, nullability, keys and classification. The description must be specific enough that two analysts compute the same number from it. Pitfall: copying physical types only; the logical type is what tools and consumers rely on across engines. -->

ODCS describes data as a list of schema objects, each with a list of properties. An object has a `name`, a `physicalName`, a `physicalType` (for example `table`, `view`, `topic` or `file`), a `businessName`, a `description` and a `dataGranularityDescription` that states the grain. A property carries a `logicalType` from the ODCS list (`string`, `date`, `timestamp`, `time`, `number`, `integer`, `object`, `array`, `boolean`, `map`, `vector`), the engine-specific `physicalType`, and flags for `required`, `unique`, `primaryKey` with `primaryKeyPosition`, `partitioned` with `partitionKeyPosition`, `classification` and `criticalDataElement`. Give every object and every property a stable `id` when other documents or quality rules refer to it, so that renaming the property does not break the reference.

Object: `[orders]`. Physical name: `[lakehouse.sales.orders]`. Physical type: table. Grain: one row per confirmed order.

| Property | Logical type | Physical type | Required | Key | Classification | CDE | Description | Example |
|---|---|---|---|---|---|---|---|---|
| `order_id` | string | `VARCHAR(20)` | Yes | Primary (position 1), unique | Internal | Yes | Order identifier from the order service | `ORD-2026-0048213` |
| `customer_id` | string | `VARCHAR(20)` | Yes | Relationship to `customers.customer_id` | Confidential | Yes | Pseudonymous customer identifier; personal data flags `PD` and `PSEUDO` | `CUS-778120` |
| `order_ts` | timestamp | `TIMESTAMP` with time zone | Yes | Partition (position 1, by day) | Internal | Yes | Time the order was confirmed, UTC | `2026-10-14T09:12:44Z` |
| `channel` | string | `VARCHAR(12)` | No | | Internal | No | Sales channel, see enumeration in section 3; null before 2022 | `app` |
| `net_amount` | number | `DECIMAL(12,2)` | Yes | | Confidential | Yes | Order value after discounts, before tax and shipping, in `currency` | `84.50` |
| `currency` | string | `CHAR(3)` | Yes | | Internal | No | ISO 4217 alphabetic code | `EUR` |
| `status` | string | `VARCHAR(12)` | Yes | | Internal | No | Order lifecycle status, see enumeration in section 3 | `shipped` |
| `region_code` | string | `VARCHAR(4)` | No | | Internal | No | Deprecated, see section 12; use `ship_country` | `WE` |
| `ship_country` | string | `CHAR(2)` | Yes | | Internal | No | ISO 3166-1 alpha-2 country of delivery | `DE` |
| [property] | [logical type] | [physical type] | [Yes / No] | [Key] | [Class] | [Yes / No] | [Description] | [Example] |

Use `logicalTypeOptions` to tighten a type without inventing new columns: string length and pattern, number minimum and maximum, timestamp format, array item limits and, for embeddings, the vector `dimensions` and element type. Record derivations in `transformSourceObjects`, `transformLogic` and `transformDescription` so a reader can trace a property back to its sources without opening the pipeline code. Classification values must come from the Data Classification Policy (`data/data-classification-policy`); do not invent local labels. Whether a property holds personal data is a separate fact from its classification level: record it with the policy's flags (`PD`, `PSEUDO`, `PD-SC` and the others in its section 5) as tags or a custom property. The flag sets a minimum level: pseudonymised personal data stays at least Confidential.

## 3. Semantic roles, synonyms and enumerations

<!-- Guidance: Record how the business talks about the data: which properties are measures or dimensions, which other names people use, and which values are allowed. These are ODCS v3.2.0 additions. Pitfall: listing allowed values in the description text, where no validator or BI tool can read them. -->

ODCS v3.2.0 lets the contract carry semantic information that previously lived only in a BI tool or a wiki. Three features matter most.

**Semantic role.** Each property may declare a `semanticType`: `column` (the default, a physical column), `measure` (an aggregated value whose aggregation expression goes in `transformLogic`) or `dimension` (a categorical attribute used to group and filter). A measure is a separate property that aggregates a base column; the base column itself stays a `column`. Marking the row-level column as a measure that sums itself confuses the grain, and semantic-layer tools may aggregate it twice. Declare roles only where they help a semantic layer or an assistant; the governed metric itself is defined once in a Metric Definition (`data/metric-definition`) and modelled in the Semantic Model Specification (`data/semantic-model-specification`).

| Property | Semantic type | Aggregation or use | Governed metric |
|---|---|---|---|
| `net_amount` *(Example)* | column | Base value at order grain; aggregated only through `total_net_amount` | n/a |
| `total_net_amount` *(Example)* | measure | `SUM(net_amount)`, aggregated per `currency` | MET-REV-001 Net revenue |
| `channel` *(Example)* | dimension | Group and filter | n/a |
| `ship_country` *(Example)* | dimension | Group and filter | n/a |
| [property] | [column / measure / dimension] | [Expression or use] | [Metric ID] |

**Synonyms.** Each schema object and property may list `synonyms`, each with the `synonym` itself and optionally a `description`, a BCP 47 `locale`, a `source` (for example the glossary or a legacy system) and a `status`. Synonyms help search, catalogues and AI assistants map a question to the right field. They do not replace the glossary entry.

| Element | Synonym | Locale | Source | Status |
|---|---|---|---|---|
| `orders` *(Example)* | purchases | en | Business glossary | active |
| `net_amount` *(Example)* | net sales value | en | Finance team | active |
| `net_amount` *(Example)* | Nettoumsatz | de-DE | Finance team | active |
| `region_code` *(Example)* | sales region | en | Legacy ERP | deprecated |

**Enumerations.** Use the `enum` list on a property for controlled vocabularies. Each entry has a `value`, an optional human `label` and an optional `description`. A validator can then reject unknown values, and a dashboard can show labels instead of codes.

| Property | Value | Label | Description |
|---|---|---|---|
| `channel` *(Example)* | `web` | Web shop | Orders placed on the browser storefront |
| `channel` *(Example)* | `app` | Mobile app | Orders placed in the iOS or Android app |
| `channel` *(Example)* | `marketplace` | Marketplace | Orders received from partner marketplaces |
| `status` *(Example)* | `cancelled` | Cancelled | Cancelled before shipment; excluded from net revenue |
| [property] | [value] | [label] | [description] |

## 4. Data quality rules

<!-- Guidance: Each rule has an ID, a dimension, a measurable threshold, the check that enforces it, a severity and what happens on failure. Use dimension names and severity levels from the Data Quality Standard. Pitfall: rules with no threshold ("customer_id should be complete") that no job can evaluate. -->

ODCS attaches `quality` rules to a schema object or to a property. Each rule has a `type`: `text` for a human-readable expectation, `library` for a predefined metric (`nullValues`, `missingValues`, `invalidValues`, `duplicateValues`, `rowCount`), `sql` for a query whose result is compared with a threshold, or `custom` for a rule written in an external engine's own syntax. Thresholds use the ODCS comparison operators (`mustBe`, `mustNotBe`, `mustBeGreaterThan`, `mustBeGreaterOrEqualTo`, `mustBeLessThan`, `mustBeLessOrEqualTo`, `mustBeBetween`, `mustNotBeBetween`). The ODCS `dimension` field accepts `accuracy`, `completeness`, `conformity`, `consistency`, `coverage`, `timeliness` and `uniqueness`; map these to the characteristic names your Data Quality Standard (`data/data-quality-standard`) uses, which may follow ISO/IEC 25012.

| ID | Element | Dimension | Type | Rule | Threshold | Severity | Check and schedule | On failure |
|---|---|---|---|---|---|---|---|---|
| DQ-ORD-001 *(Example)* | `order_id` | uniqueness | library `duplicateValues` | No duplicate order identifiers | `mustBe: 0` | Critical | dbt test, every load | Block publish; page producer on-call |
| DQ-ORD-002 *(Example)* | `customer_id` | completeness | library `nullValues` | Customer identifier present | `mustBe: 0` | Critical | Every load | Block publish |
| DQ-ORD-003 *(Example)* | `currency` | conformity | library `invalidValues` | Code in the allowed ISO 4217 list | `mustBe: 0` | Major | Every load | Quarantine rows; alert |
| DQ-ORD-004 *(Example)* | `orders` | accuracy | sql | Daily sum of `net_amount` matches the order service | Difference `mustBeLessOrEqualTo: 0.1` percent | Major | Daily reconciliation at 06:00 UTC | Alert producer; mark day as provisional |
| DQ-ORD-005 *(Example)* | `orders` | completeness | library `rowCount` | Daily rows within expected range | `mustBeBetween: [40000, 160000]` | Minor | Every load | Alert; notify consumers |
| [DQ-ORD-nnn] | [element] | [dimension] | [type] | [Rule] | [Threshold] | [Severity] | [Check] | [Action] |

ODCS `severity` is free text (the schema examples are info, warning and error); this template uses the Data Quality Standard's Critical, Major and Minor so that contracts, tests and alerts share one scale. Tell any tool that expects the ODCS example values how to map them.

Write the `businessImpact` of each Critical and Major rule in one sentence, for example "Duplicate orders inflate reported revenue". Consumers read that field when they decide whether to wait for a fix or use the data with a caveat. When a rule fails, follow the Data Incident Postmortem (`data/data-incident-postmortem`) process for anything that reached consumers.

## 5. Service level properties

<!-- Guidance: Commit to numbers the producer can meet and measure. Each property says what is measured, the unit, the element it applies to, and the driver (regulatory, analytics or operational). Pitfall: copying a platform SLA (uptime of the warehouse) instead of a dataset SLA (freshness of this table). -->

ODCS expresses service levels as a list of `slaProperties`. Each entry has a `property` name, a `value` (and optionally `valueExt`), a `unit`, the `element` it is measured on, a `driver` and, for scheduled checks, a `scheduler` and `schedule`. ODCS recommends the property names `availability`, `throughput`, `errorRate`, `latency` (preferred to freshness), `frequency`, `retention`, `generalAvailability`, `endOfSupport`, `endOfLife`, `timeToDetect`, `timeToNotify` and `timeToRepair`; the documentation examples also use `timeOfAvailability`. Add others, such as `supportResponse` in the table below, only when no recommended name fits, and define them in the standards register so tools read them consistently. Version 3.2.0 adds `authoritativeDefinitions` and `customProperties` on each SLA entry, so you can link the formal SLA document or the measurement method. The older `slaDefaultElement` field is deprecated since v3.1.0; set `element` on each entry instead.

| ID | Property | Commitment | Unit | Element | Driver | Measured as | Period |
|---|---|---|---|---|---|---|---|
| SLA-ORD-001 *(Example)* | latency | 60 | minutes | `orders.order_ts` | analytics | Now minus latest `order_ts` loaded | Checked every 15 minutes |
| SLA-ORD-002 *(Example)* | frequency | 15 | minutes | `orders` | operational | Interval between successful loads | Daily |
| SLA-ORD-003 *(Example)* | timeOfAvailability | 06:00 UTC | time | `orders` | analytics | Previous day complete and reconciled | Daily |
| SLA-ORD-004 *(Example)* | retention | 7 | years | `orders.order_ts` | regulatory | Partition lifecycle policy | Continuous |
| SLA-ORD-005 *(Example)* | availability | 99.5 | percent | `orders` | operational | Successful synthetic query per hour | Monthly |
| SLA-ORD-006 *(Example)* | supportResponse | 1 | business hours | n/a | operational | Ticket timestamps | Monthly |
| [SLA-ORD-nnn] | [property] | [value] | [unit] | [element] | [driver] | [Measure] | [Period] |

Retention must agree with the Data Retention and Deletion Standard (`data/data-retention-deletion-standard`). If the contract promises longer retention than the standard allows, the standard wins and the contract is wrong.

## 6. Servers and environments

<!-- Guidance: List where the data is physically served in each environment, using ODCS server types. Keep hostnames, buckets and credentials out of the file with variables. Pitfall: committing a production connection string to the contract repository. -->

The ODCS `servers` list records each place the dataset can be read. Every entry has a `server` identifier and a `type`; v3.2.0 added eight server types (`hana`, `iceberg`, `exasol`, `teradata`, `ingres`, `vectorwise`, `versant`, `poet`) plus the synonyms `fastobjects` and `btrieve`. Each type has its own connection fields. For `iceberg` they are `catalog` and `catalogUrl` (required) plus `namespace` and `warehouse`. Add `environment` (for example `dev`, `uat`, `prod`), a `description` and the `roles` that may connect. File-based servers may also declare an `encoding` such as UTF-8.

ODCS v3.2.0 also defines variables: any string value may contain a `${VAR_NAME}` reference, with an optional default written `${VAR_NAME:-default}`, resolved by tooling at run time from environment variables, a secrets manager or CI variables. One contract file can then serve every environment, and no secret enters version control. The server `port` accepts a string so it can hold a variable. Tooling must report an unresolved variable instead of silently using an empty value.

| Server ID | Type | Environment | Connection fields (variables) | Roles | Notes |
|---|---|---|---|---|---|
| `lakehouse-prod` *(Example)* | iceberg | prod | `catalog: corvid`, `catalogUrl: ${ICEBERG_CATALOG_URL}`, `namespace: sales` | `orders_reader`, `orders_finance` | REST catalogue; Iceberg table format v2 |
| `lakehouse-dev` *(Example)* | iceberg | dev | `catalog: corvid`, `catalogUrl: ${ICEBERG_CATALOG_URL}`, `namespace: ${DEV_NAMESPACE:-sales_dev}` | `orders_dev` | Synthetic data only |
| `orders-events` *(Example)* | kafka | prod | `host: ${KAFKA_BOOTSTRAP}`, topic `orders.confirmed.v2` | `orders_stream_reader` | Same schema, event grain |
| [server] | [type] | [env] | [fields] | [roles] | [notes] |

## 7. Team, roles and access

<!-- Guidance: Name the people accountable for the data and the roles that grant access, with approvers. Pitfall: listing individuals as owners with no team, so ownership disappears when one person leaves. -->

Since v3.1.0, ODCS models `team` as an object with a `name`, a `description` and a `members` list; the older plain array of members is deprecated and will be removed in ODCS 4.0. Each member has a `username`, a `name`, a `role` (for example owner or data steward), and `dateIn` and `dateOut` so the history of ownership is kept. When someone leaves, set `dateOut` and record `replacedByUsername` rather than deleting the row.

| Username | Name | Role | Date in | Date out |
|---|---|---|---|---|
| `a.okafor` *(Example)* | Ada Okafor | Data product owner | 2025-03-01 | |
| `m.lindqvist` *(Example)* | Mats Lindqvist | Data steward | 2025-03-01 | |
| [username] | [Name] | [Role] | YYYY-MM-DD | [YYYY-MM-DD] |

The ODCS `roles` list names each access role with its `access` level and the `firstLevelApprovers` and `secondLevelApprovers` who grant it.

| Role | Access | First-level approver | Second-level approver | Scope |
|---|---|---|---|---|
| `orders_reader` *(Example)* | read | Data steward | n/a | All properties; `net_amount` masked |
| `orders_finance` *(Example)* | read | Data steward | Finance controller | All properties unmasked |
| `orders_dev` *(Example)* | read | Team lead | n/a | Dev server only |
| [role] | [read / write] | [approver] | [approver] | [scope] |

| Item | Rule |
|---|---|
| Access request | *Example:* Through [access portal]; approval recorded with the request ticket |
| Personal data handling | *Example:* `customer_id` is pseudonymous; joining it to identity tables requires a privacy-approved purpose |
| Masking | *Example:* `net_amount` masked for roles without finance approval |
| Legal basis and purposes | [Reference to the records of processing activities] |
| Audit | *Example:* Query logs retained for 1 year |

## 8. Support and communication

<!-- Guidance: Tell consumers where to ask questions, where change announcements appear and where to report problems. Pitfall: a single chat channel used for questions, incidents and announcements, so that announcements are missed. -->

ODCS `support` entries each name a `channel`, a `url`, a `tool` (for example email, slack, teams or ticket) and a `scope` (`interactive`, `announcements`, `issues` or `notifications`). Separate the scopes so that consumers can subscribe to announcements without following every question.

| Channel | Tool | Scope | URL | Hours |
|---|---|---|---|---|
| `#orders-data` *(Example)* | slack | interactive | [URL] | Business hours |
| `orders-data-announce` *(Example)* | email | announcements | [list address] | n/a |
| Orders data issues *(Example)* | ticket | issues | [URL] | Business hours; on-call for Critical |
| [channel] | [tool] | [scope] | [URL] | [hours] |

## 9. Pricing

<!-- Guidance: State the internal price, if any, so consumers see the cost of what they use. Leave a zero price explicit rather than deleting the section. Pitfall: a price unit nobody can measure. -->

The ODCS `price` object has `priceAmount`, `priceCurrency` and `priceUnit`. Many organisations charge nothing internally and still record a zero price, because it shows that the question was considered. Where the platform charges back query cost, name the unit the bill uses.

| Field | Value |
|---|---|
| `priceAmount` | *Example:* 0 |
| `priceCurrency` | *Example:* EUR |
| `priceUnit` | *Example:* gigabyte scanned |
| Chargeback note | *Example:* Query cost is billed to the consumer's cost centre by the platform, not by this contract |

## 10. AI usage context

<!-- Guidance: Tell AI agents, assistants and BI copilots how to use the dataset, which answers are verified, and what they must never do. This is the ODCS v3.2.0 `context` block. Pitfall: treating the constraints as access control; they guide well-behaved tools and do not replace roles and masking. -->

ODCS v3.2.0 adds an optional `context` block at contract level and at schema object level (not on individual properties; the schema rejects a `context` key on a property). It holds `instructions` (natural-language guidance scoped to that level), `verifiedStatements` (canonical questions, with verified answers where available) and `constraints` (negative guidance: what an agent must not do). Write instructions as you would brief a new analyst. Keep verified answers to the method, not to a number that will go stale.

| ID | Kind | Level | Text |
|---|---|---|---|
| CTX-ORD-001 *(Example)* | instruction | contract | Use `orders` for order-level questions; for daily counts and net revenue prefer the `orders_daily_agg` port of the Orders data product; exclude `status = cancelled` from net revenue |
| CTX-ORD-002 *(Example)* | verified statement | contract | Question: "What was net revenue last month?" Answer: sum `net_amount` by `currency` for confirmed orders in the month, using `order_ts` in UTC |
| CTX-ORD-003 *(Example)* | constraint | contract | Do not return individual orders together with `customer_id`; aggregate to at least country and day |
| CTX-ORD-004 *(Example)* | constraint | object `orders` | Do not convert `net_amount` between currencies; use the governed FX dataset |
| [CTX-ORD-nnn] | [kind] | [level] | [text] |

## 11. Consumers

<!-- Guidance: Register who depends on the contract so they can be notified of changes and asked to approve breaking ones. Pitfall: an empty register, which turns every change into a guess. -->

ODCS does not prescribe a consumer register; keep it here or in the data catalogue and reference it with a custom property. Record the properties each consumer uses, so that a change to an unused property can proceed with less ceremony.

| Consumer | Team | Use | Properties used | Contact | Criticality |
|---|---|---|---|---|---|
| *Example:* Revenue dashboard | Finance analytics | Daily revenue | All | [@name] | High |
| *Example:* Customer analytics | Customer analytics team | Cohort and repeat-purchase analysis | `order_id`, `customer_id`, `order_ts`, `status` | [@name] | Medium |
| *Example:* `orders_daily_agg` port (same product) | Sales Data Products | Source of the daily aggregate used by demand forecasting and the analytics assistant | `order_ts`, `channel`, `net_amount`, `currency`, `status` | [@name] | High |
| *Example:* Checkout data product | Checkout team | Conversion funnel join | `order_id`, `order_ts` | [@name] | Medium |
| [Consumer] | [Team] | [Use] | [Properties] | [@name] | [High / Medium / Low] |

## 12. Change management, versioning and deprecation

<!-- Guidance: Define what counts as breaking, the notice period, how versions are numbered and how properties are retired. Pitfall: deleting a column in the same release that announces its deprecation. -->

Contract versions follow semantic versioning:

- **Major** (breaking): removing or renaming a property, changing a type or meaning, changing the grain, making a nullable property required in a way that drops rows, lowering a service level.
- **Minor** (compatible): adding an optional property, adding an enumeration value with notice, improving a service level, marking a property deprecated.
- **Patch**: description fixes and documentation only.

ODCS v3.2.0 adds a `deprecated` flag on schema objects and properties. Set it when the replacement is available, keep the property populated during the notice period, and remove it only in the next major version. Record the replacement in the description and, if useful, as a deprecated synonym.

| Rule | Policy |
|---|---|
| Notice for breaking change | *Example:* 30 days to all registered consumers through the announcements channel |
| Parallel run | *Example:* Old and new major versions served side by side for at least 30 days |
| Deprecation | *Example:* Property flagged `deprecated: true` for at least one minor release before removal |
| Approval | *Example:* Producer owner and data steward; High criticality consumers must acknowledge |
| Emergency change | *Example:* Allowed for security or legal reasons; consumers notified within 1 hour |
| Status transitions | *Example:* proposed, draft, active, deprecated, retired; `retired` only after the last consumer has moved |

| Contract version | Date | Author | Change | Type |
|---|---|---|---|---|
| 1.0.0 *(Example)* | YYYY-MM-DD | [@name] | First active contract | n/a |
| 1.4.0 *(Example)* | YYYY-MM-DD | [@name] | Last 1.x release; served as deprecated port `orders` 1.4.0 until its sunset date (2026-12-31) | Minor |
| 2.0.0 *(Example)* | YYYY-MM-DD | [@name] | `net_amount` changed from gross to net of discounts | Major |
| 2.1.0 *(Example)* | YYYY-MM-DD | [@name] | Added `ship_country`; `region_code` deprecated | Minor |
| [x.y.z] | YYYY-MM-DD | [@name] | [Change] | [Major / Minor / Patch] |

## 13. Authoritative definitions and custom properties

<!-- Guidance: Link to the sources that define terms and implementations, and record organisation-specific metadata in custom properties instead of inventing top-level fields. Pitfall: adding unknown top-level keys, which strict validators reject. -->

`authoritativeDefinitions` may appear at contract, object, property and SLA level. Each entry has a `type` (recommended values in the ODCS documentation include `businessDefinition`, `glossary`, `implementation`, `transformationImplementation` and `canonicalUrl`) and a `url`. Use them to point to the glossary term, the dbt model and the metric definition.

`customProperties` is the extension point for anything ODCS does not define. Each entry has a `property` name in camel case and a `value`; v3.2.0 adds an optional `vendor` to show which tool owns the property; an optional `description` is also available.

| Level | Kind | Name or type | Value or URL |
|---|---|---|---|
| contract *(Example)* | authoritative definition | businessDefinition | [Glossary URL for "Order"] |
| `net_amount` *(Example)* | authoritative definition | implementation | [dbt model URL] |
| contract *(Example)* | custom property | `dataProductOutputPort` | `orders` |
| contract *(Example)* | custom property | `consumerRegister` | [catalogue URL] |
| [level] | [kind] | [name] | [value] |

The top-level `dataProduct` field is deprecated since ODCS v3.1.0. Link the contract to its data product from the product side (the ODPS output port's contract reference) and, if you need a back-link, with a custom property.

## 14. Traceability matrix

<!-- Guidance: Show that every promise in the contract is enforced and every consumer need is covered. Pitfall: rules that appear in the contract but in no job. -->

| Requirement or need | Contract element | Enforced by | Evidence | Consumer |
|---|---|---|---|---|
| *Example:* Revenue must not double count | DQ-ORD-001 | dbt test `unique_orders_order_id` | Test results in catalogue | Revenue dashboard |
| *Example:* Daily revenue ready before 06:00 UTC | SLA-ORD-003 | Orchestrator sensor | SLA report | Finance analytics |
| *Example:* No individual customer exposure in assistants | CTX-ORD-003, role `orders_reader` masking | Policy engine and context block | Access review | All |
| *Example:* Seven-year retention | SLA-ORD-004 | Iceberg snapshot and partition expiry job | Lifecycle job logs | Finance, audit |
| [Need] | [Element ID] | [Job or control] | [Evidence] | [Consumer] |

## 15. ODCS YAML skeleton

<!-- Guidance: Keep this file next to the prose and validate it in CI against the ODCS v3.2.0 JSON schema (schema/odcs-json-schema-v3.2.0.json in the Bitol repository). Field names below were checked against that schema on 2026-10-24; check the current release before relying on a validator. -->

The skeleton mirrors sections 1 to 13. Values are *Example* data for the fictional Corvid Commerce.

```yaml
apiVersion: v3.2.0
kind: DataContract
id: corvid-orders-orders
name: orders
version: 2.1.0
status: active
domain: sales
tenant: corvid-commerce
tags: [orders, sales, gold, PD, PSEUDO]
contractCreatedTs: "2026-10-24T09:00:00Z"
description:
  purpose: One row per confirmed customer order for revenue reporting and forecasting.
  usage: Daily revenue reporting, cohort analysis, source of the orders_daily_agg port.
  limitations: Marketplace orders may arrive up to 24 hours late.
context:
  instructions: >
    Use this dataset for order-level questions; prefer the orders_daily_agg port for daily
    counts and net revenue. Exclude cancelled orders from net revenue.
  verifiedStatements:
    - id: net-revenue-month
      question: "What was net revenue last month?"
      answer: "Sum net_amount by currency for non-cancelled orders, using order_ts in UTC."
  constraints:
    - id: no-customer-level-output
      constraint: "Do not return individual orders together with customer_id."
schema:
  - name: orders
    id: orders_obj
    physicalName: orders
    physicalType: table
    logicalType: object
    businessName: Customer orders
    dataGranularityDescription: One row per confirmed order
    synonyms:
      - synonym: purchases
        source: glossary
    properties:
      - name: order_id
        id: order_id_prop
        logicalType: string
        physicalType: VARCHAR(20)
        required: true
        primaryKey: true
        primaryKeyPosition: 1
        unique: true
        criticalDataElement: true
        classification: internal
        quality:
          - id: dq-ord-001
            type: library
            metric: duplicateValues
            mustBe: 0
            dimension: uniqueness
            severity: critical
            businessImpact: Duplicate orders inflate reported revenue.
      - name: net_amount
        id: net_amount_prop
        logicalType: number
        physicalType: DECIMAL(12,2)
        required: true
        classification: confidential
        synonyms:
          - synonym: Nettoumsatz
            locale: de-DE
            source: finance-team
      - name: total_net_amount
        id: total_net_amount_prop
        logicalType: number
        semanticType: measure
        transformLogic: SUM(net_amount)
        description: Sum of net_amount; aggregate per currency.
      - name: channel
        id: channel_prop
        logicalType: string
        semanticType: dimension
        enum:
          - value: web
            label: Web shop
          - value: app
            label: Mobile app
          - value: marketplace
            label: Marketplace
      - name: region_code
        id: region_code_prop
        logicalType: string
        deprecated: true
        description: Deprecated since 2.1.0; use ship_country.
    quality:
      - id: dq-ord-005
        type: library
        metric: rowCount
        mustBeBetween: [40000, 160000]
        dimension: completeness
        severity: minor
servers:
  - server: lakehouse-prod
    type: iceberg
    environment: prod
    catalog: corvid
    catalogUrl: ${ICEBERG_CATALOG_URL}
    namespace: sales
  - server: lakehouse-dev
    type: iceberg
    environment: dev
    catalog: corvid
    catalogUrl: ${ICEBERG_CATALOG_URL}
    namespace: ${DEV_NAMESPACE:-sales_dev}
slaProperties:
  - id: sla-ord-001
    property: latency
    value: 60
    unit: min
    element: orders.order_ts
    driver: analytics
  - id: sla-ord-004
    property: retention
    value: 7
    unit: y
    element: orders.order_ts
    driver: regulatory
team:
  name: sales-data-products
  description: Owns the Orders and Checkout data products.
  members:
    - username: a.okafor
      role: owner
      dateIn: "2025-03-01"
    - username: m.lindqvist
      role: data steward
      dateIn: "2025-03-01"
roles:
  - role: orders_reader
    access: read
    firstLevelApprovers: Data steward
  - role: orders_finance
    access: read
    firstLevelApprovers: Data steward
    secondLevelApprovers: Finance controller
support:
  - channel: "#orders-data"
    tool: slack
    scope: interactive
  - channel: orders-data-announce
    tool: email
    scope: announcements
price:
  priceAmount: 0
  priceCurrency: EUR
  priceUnit: gigabyte
authoritativeDefinitions:
  - type: businessDefinition
    url: https://catalog.example.com/glossary/order
customProperties:
  - property: dataProductOutputPort
    value: orders
```

## 16. Glossary and references

<!-- Guidance: Define the terms a consumer meets in this contract and list the sources the contract relies on. -->

| Term | Meaning in this contract |
|---|---|
| Data contract | The agreement in this document and its YAML file between producer and consumers of one dataset |
| Output port | The interface through which a data product serves a dataset; each output port has one contract |
| Critical data element (CDE) | A property whose errors have material business, financial or regulatory impact |
| Grain | What one row represents |
| Semantic type | ODCS role of a property: column, measure or dimension |
| Verified statement | A canonical question, with an approved answer method, for AI tools and analysts |
| Variable | A `${VAR_NAME}` reference resolved by tooling at run time |

References:

- Bitol, Open Data Contract Standard (ODCS) v3.2.0, released 2026-09-08, Apache-2.0: https://github.com/bitol-io/open-data-contract-standard and https://bitol-io.github.io/open-data-contract-standard/
- Bitol, Open Data Product Standard (ODPS) v1.1.0, released 2026-09-08, Apache-2.0: https://github.com/bitol-io/open-data-product-standard
- ISO/IEC 25012:2008, Data quality model (names only).
- Apache Iceberg table specification: https://iceberg.apache.org/spec/
- Related templates: Data Product Specification (`data/data-product-specification`), Data Quality Standard (`data/data-quality-standard`), Data Pipeline Specification (`data/data-pipeline-spec`), Metric Definition (`data/metric-definition`), Semantic Model Specification (`data/semantic-model-specification`), Data Classification Policy (`data/data-classification-policy`), Data Retention and Deletion Standard (`data/data-retention-deletion-standard`), Data Incident Postmortem (`data/data-incident-postmortem`), Data Requirements Specification (`requirements/data-requirements`).

## Standard and licence note

Based on the Open Data Contract Standard (ODCS) v3.2.0 published by the Bitol project (a Linux Foundation AI and Data project) under the Apache License 2.0, https://github.com/bitol-io/open-data-contract-standard. Field names and enumerated values are used as defined by the specification; all explanatory text is original and copies no prose from the specification. Check the current ODCS release and JSON schema before validating a contract, because field names and server types change between versions. Template licence: MIT.
