> ## Documentation Index
> Fetch the complete documentation index at: https://docs.anomalyarmor.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Data Contracts (ODCS export/import)

> Export and import AnomalyArmor monitoring config as portable ODCS YAML

<div aria-hidden="true" style={{position:"absolute",width:"1px",height:"1px",overflow:"hidden",clip:"rect(0,0,0,0)",whiteSpace:"nowrap"}}>For LLM agents: documentation index at <a href="/llms.txt" tabIndex={-1}>/llms.txt</a>, full text at <a href="/llms-full.txt" tabIndex={-1}>/llms-full.txt</a>. Append .md to any page URL for plain markdown.</div>
AnomalyArmor speaks the [Open Data Contract Standard (ODCS) v3.1.0](https://bitol-io.github.io/open-data-contract-standard/latest/). Every asset's monitoring config (freshness SLAs, validity rules, schema, metrics, drift monitors, alert rules, destinations, blackouts) exports as ODCS YAML and imports back on demand.

<Frame>
  <img src="https://mintcdn.com/anomalyarmor/8mY_BmQQn-Ql-kSr/images/diagrams/contracts-round-trip-flow-light.svg?fit=max&auto=format&n=8mY_BmQQn-Ql-kSr&q=85&s=71e6d049c6197eee1b01d769936f8098" alt="Export and import round-trip for ODCS contracts" className="block dark:hidden" width="760" height="360" data-path="images/diagrams/contracts-round-trip-flow-light.svg" />

  <img src="https://mintcdn.com/anomalyarmor/8mY_BmQQn-Ql-kSr/images/diagrams/contracts-round-trip-flow-dark.svg?fit=max&auto=format&n=8mY_BmQQn-Ql-kSr&q=85&s=42bedd063cff39bef4b387cee6e31fcf" alt="Export and import round-trip for ODCS contracts" className="hidden dark:block" width="760" height="360" data-path="images/diagrams/contracts-round-trip-flow-dark.svg" />
</Frame>

Three things this gives you:

1. **Interoperability.** ODCS is a Linux Foundation project. Soda, Great Expectations, and dbt speak it. Your contract is not a proprietary blob.
2. **No lock-in.** Click Export, get a directory of YAML. Import it into whichever tool you pick next. No rewrite required.
3. **Version control.** Contracts diff cleanly in Git. Reviewers see which validity rule changed in a PR, not a screenshot of a settings page.

## ODCS-native vs ODCS-extended

ODCS covers the core concepts every tool agrees on: schema, column quality rules, freshness SLAs, ownership. We use its sanctioned `customProperties` extension mechanism for the AnomalyArmor-specific concepts ODCS does not have a native slot for.

| Native ODCS (any tool reads these)                                                        | Extended (under `customProperties.anomalyarmor`)            |
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `apiVersion`, `id`, `name`, `version`, `status`, `domain`, `tags`                         | Schema drift monitoring (ODCS has no schema-change concept) |
| `schema[]` with `properties[]` + `physicalType`                                           | ML distribution drift (PSI, KS, chi-squared)                |
| `quality[]` metric library (`nullValues`, `invalidValues`, `duplicateValues`, `rowCount`) | Row-count anomaly detection with rolling baselines          |
| `quality[]` custom SQL                                                                    | Alert rules and routing                                     |
| `slaProperties[]` (latency, frequency)                                                    | Destinations (Slack, email, PagerDuty, Linear, webhooks)    |
| `team[]`                                                                                  | Operating schedules and blackout windows                    |

Our exported YAML is **fully ODCS-compliant**: it passes the [official v3.1.0 JSON Schema validator](https://github.com/bitol-io/open-data-contract-standard/tree/main/schema). Using `customProperties` is exactly how Soda and Great Expectations handle the same tension: industry-standard, not a workaround.

ODCS models `customProperties` as a **list of `{property, value}` entries**, not a dict. Our AnomalyArmor extensions all ride inside a single entry whose `property` is `anomalyarmor`:

```yaml theme={null}
customProperties:
  - property: anomalyarmor
    value:
      freshness: [...]
      validity: [...]
      drift_monitors: [...]
```

## Deviations from ODCS v3.1.0

Small, documented places where our exported YAML does more or less than the spec:

* **`schema[].properties[].id` (extension).** ODCS does not define an `id` at the column level. We write a stable UUID5 per column so re-import can match columns across renames. `odcs-pure` mode strips it; other ODCS tools read the column by `name` and ignore `id`.
* **Unmodeled optional sections.** These ODCS sections are recognized but we do not produce or consume them: `servers`, `roles`, `price`, `support`, `authoritativeDefinitions`, `contractCreatedTs`. They round-trip opaquely if present on import and are never emitted on export.

## Portability modes

Two export modes control what gets written to the YAML.

### `extended` (default)

Full round-trip fidelity. Includes `customProperties.anomalyarmor.<domain>` blocks with every AA-specific configuration knob (drift thresholds, monitoring modes, alert routing, etc.). Use this for version control, backup, and re-import into AnomalyArmor.

### `odcs-pure`

Strips every `customProperties.anomalyarmor` block. The resulting YAML contains only the ODCS-native subset that any ODCS tool can read and act on: schema, quality rules, freshness SLAs, team. You lose the AA-extended features (drift, alert routing, blackouts) but gain maximum portability for the interop case.

Pick `odcs-pure` when you want to hand the contract to a Soda or Great Expectations customer. Pick `extended` everywhere else.

## CLI

```bash theme={null}
pip install anomalyarmor-cli
armor auth login --key aa_live_your_key_here
```

The package installs two console scripts, `armor` and `anomalyarmor`. They are aliases, use whichever you prefer. Examples below use `armor` for brevity.

### Export: one asset to stdout

```bash theme={null}
armor contract pull --asset <asset-uuid>
```

Writes ODCS YAML to stdout. Pipe to `yq` or redirect to a file.

### Export: one asset to a file

```bash theme={null}
armor contract pull --asset <asset-uuid> -o orders.yaml
```

### Export: whole warehouse as a zip

```bash theme={null}
armor contract pull --warehouse analytics -o analytics.zip
```

Submits an async job, polls to completion, and writes the zip. Layout inside the zip:

```
analytics/
  contract/
    orders.yaml
    customers.yaml
    invoices.yaml
```

### Export: every contract in your account

```bash theme={null}
armor contract pull --all -o everything.zip
```

### Filtering (export or import)

Include or exclude specific config domains:

```bash theme={null}
# Only freshness + validity
armor contract pull --asset <uuid> --include freshness,validity -o partial.yaml

# Everything except alert routing and destinations
armor contract pull --asset <uuid> --exclude alert_rules,destinations -o no-alerts.yaml
```

Registered domain names: `schema`, `freshness`, `validity`, `metrics`, `row_count`, `drift_monitors`, `schema_drift`, `alert_rules`, `destinations`, `blackouts`.

### Export: ODCS-pure mode

```bash theme={null}
armor contract pull --asset <uuid> --mode odcs-pure -o orders.odcs.yaml
```

### Validate a YAML file (no DB, no auth required beyond the API key)

```bash theme={null}
armor contract validate -f orders.yaml
```

Exits 0 on a valid document, non-zero on parse or schema errors. Errors are printed one-per-line with file path, YAML path, and line number, so editors and CI can parse the output. Cheap: a single HTTP round-trip regardless of contract size.

### Plan an import (dry-run diff)

```bash theme={null}
armor contract plan --asset <asset-uuid> -f orders.yaml
```

Shows a per-domain diff without applying. Output:

```
Plan for asset 53581432-...:
  freshness         +2 ~1 -0
  validity          +0 ~3 -1
  drift_monitors    +0 ~0 -0
```

`+` is additions, `~` is modifications, `-` is deletions.

### Apply an import

```bash theme={null}
armor contract apply --asset <asset-uuid> -f orders.yaml
```

Transactional per asset. Any domain failure rolls back the whole contract. By default, configs present in the live asset but missing from the YAML are **not deleted** (warn-only). To prune, pass `--prune`:

```bash theme={null}
armor contract apply --asset <asset-uuid> -f orders.yaml --prune
```

`--prune` deletes configs that are absent from the YAML. Treat this the way you would treat `rm -rf`, review the `plan` output first.

### Diff two local YAML files

```bash theme={null}
armor contract diff baseline.yaml pr.yaml
```

Pure client-side, no server call. Uses the same identity-matching logic as the server-side plan, so the summary line-up is identical. Useful in CI to compare `contracts/main.yaml` to `contracts/pr.yaml` before calling `plan` or `apply`.

## In-product UI

The same primitives ship in the app.

* **Export.** Every asset detail page has an **Export as YAML** action. Opens a modal with three controls: asset scope (this asset / schema / warehouse / all), feature-domain checkboxes, and the `extended` / `odcs-pure` toggle. Multi-asset scopes download as a zip.
* **Import.** Same page, **Import YAML** action next to Export. Drag-drop or click-to-browse a `.yaml` file. The modal runs server-side validation, then shows a plan preview (`+2 freshness, ~1 validity, -0 drift`). Apply button is disabled until the plan is clean. Prune has an explicit confirmation dialog that lists what will be removed.
* **Bulk import.** Asset list supports checkbox selection; the bulk action bar exposes **Import YAML** which applies one contract to many selected assets (async job, polled in the modal).

## REST API

All endpoints authenticated via your API key. Single-asset endpoints are synchronous; bulk endpoints run as async jobs.

### Export, single asset (synchronous)

```
GET /api/v1/contracts/{asset_public_id}/export
    ?mode=extended|odcs-pure     (default: extended)
    &include=freshness,validity  (comma-separated, default: all)
    &exclude=alert_rules         (applied after include)
```

Returns `application/x-yaml` with a `Content-Disposition: attachment` header.

### Export, bulk (asynchronous)

```
POST /api/v1/contracts/export-jobs
  { "scope": "warehouse"|"all",
    "scope_name": "analytics",        // required for scope=warehouse
    "mode": "extended",
    "include": ["freshness"],
    "exclude": ["alert_rules"] }
```

Returns `{"job_id": "<uuid>"}`. Poll with:

```
GET /api/v1/contracts/export-jobs/{job_id}
```

Response includes `status`, `progress_percent`, and `asset_count`. When status is `completed`, download:

```
GET /api/v1/contracts/export-jobs/{job_id}/download
```

Returns `application/zip` with the laid-out `{warehouse}/contract/{table}.yaml` archive.

### Validate (no DB, no asset context)

```
POST /api/v1/contracts/validate
Body: raw ODCS YAML
```

Returns `{ valid: bool, errors: [{line, yaml_path, code, message}], contract_summary: {...} }`.

### Plan, single asset (synchronous)

```
POST /api/v1/contracts/{asset_public_id}/plan
Body: raw ODCS YAML
```

Returns `{ valid, errors, diffs: { <domain>: { added, modified, removed } } }`. Read-only, no mutations.

### Apply, single asset (synchronous, transactional)

```
POST /api/v1/contracts/{asset_public_id}/apply?prune=false
Body: raw ODCS YAML
```

Returns `{ valid, applied: { <domain>: {added, modified, removed} }, unsupported_domains: [], pruned: bool }`. Partial failures roll back the entire contract.

### Apply, bulk (asynchronous)

```
POST /api/v1/contracts/apply-jobs
  { "asset_public_ids": ["...", "..."],
    "yaml": "apiVersion: v3.1.0\n...",
    "prune": false }
```

Returns `{"job_id": "<uuid>"}`. Poll `GET /api/v1/contracts/apply-jobs/{job_id}` for per-asset status.

## Round-trip example

```bash theme={null}
# Export
armor contract pull --asset <uuid> -o orders.yaml

# Edit the freshness threshold in orders.yaml
vim orders.yaml

# Commit to Git
git add orders.yaml && git commit -m "tighten freshness SLA on orders"

# Preview the change without applying
armor contract plan --asset <uuid> -f orders.yaml

# Apply once the diff looks right
armor contract apply --asset <uuid> -f orders.yaml
```

## Adapter coverage at a glance

Three adapters ship today. The grid shows which kinds of rules each adapter converts automatically, which convert with caveats, and which you rewrite as custom rules in AnomalyArmor.

<Frame>
  <img src="https://mintcdn.com/anomalyarmor/C0RY1-Hrje4r-_Nv/images/diagrams/contracts-adapter-coverage-matrix-light.svg?fit=max&auto=format&n=C0RY1-Hrje4r-_Nv&q=85&s=6bf514e3bee8677263f78e722636c5ce" alt="Coverage matrix across Soda, dbt, and Great Expectations adapters" className="block dark:hidden" width="980" height="380" data-path="images/diagrams/contracts-adapter-coverage-matrix-light.svg" />

  <img src="https://mintcdn.com/anomalyarmor/C0RY1-Hrje4r-_Nv/images/diagrams/contracts-adapter-coverage-matrix-dark.svg?fit=max&auto=format&n=C0RY1-Hrje4r-_Nv&q=85&s=79d7ac1a394b7513951357f787013923" alt="Coverage matrix across Soda, dbt, and Great Expectations adapters" className="hidden dark:block" width="980" height="380" data-path="images/diagrams/contracts-adapter-coverage-matrix-dark.svg" />
</Frame>

Every skipped check surfaces a warning, never a silent drop. See the per-adapter guides below for the exact mapping tables.

## Coming from Soda?

[Soda ships ODCS export](https://docs.soda.io/data-testing/git-managed-data-contracts) as of 2024. Run `soda export --odcs` on your existing Soda project and the resulting YAML imports directly into AnomalyArmor:

```bash theme={null}
soda export --odcs > contracts/
armor contract apply --asset <uuid> -f contracts/orders.yaml
```

<Frame>
  <img src="https://mintcdn.com/anomalyarmor/8mY_BmQQn-Ql-kSr/images/diagrams/contracts-soda-migration-flow-light.svg?fit=max&auto=format&n=8mY_BmQQn-Ql-kSr&q=85&s=d1171257026f476fd8f15f3de68f3a03" alt="Two-command Soda to AnomalyArmor migration" className="block dark:hidden" width="820" height="320" data-path="images/diagrams/contracts-soda-migration-flow-light.svg" />

  <img src="https://mintcdn.com/anomalyarmor/8mY_BmQQn-Ql-kSr/images/diagrams/contracts-soda-migration-flow-dark.svg?fit=max&auto=format&n=8mY_BmQQn-Ql-kSr&q=85&s=a63db68047c866fa5881f2f7954a90a9" alt="Two-command Soda to AnomalyArmor migration" className="hidden dark:block" width="820" height="320" data-path="images/diagrams/contracts-soda-migration-flow-dark.svg" />
</Frame>

If you want to preview the migration before signing up, paste your Soda YAML into [anomalyarmor.ai/migrate](https://www.anomalyarmor.ai/migrate) for a structural summary without an account.

## Coming from dbt or Great Expectations?

Those tools do not emit ODCS directly. We ship adapters that translate their config into ODCS YAML, which then feeds the same `contract apply` pipeline:

```bash theme={null}
# dbt: translate schema.yml tests and contracts into ODCS
armor migrate-from dbt ./my-dbt-project/ > contracts/

# Great Expectations: translate expectation suites into ODCS
armor migrate-from great-expectations ./great_expectations/ > contracts/
```

See the per-adapter guides for mapping rules, unsupported expectations, and CLI options:

* [Migrate from dbt](/guides/migrate-from-dbt)
* [Migrate from Great Expectations](/guides/migrate-from-great-expectations)

## Limits and current gaps

* **No S3 artifact storage for huge bulk exports.** Jobs today inline the zip bytes in the status response, which is fine for hundreds of tables. Company-wide exports with thousands of tables should use multiple `--warehouse` jobs.
* **Single-asset apply only in `contract apply`.** Multi-asset apply ships through the bulk-apply REST/UI path, not through the CLI flag surface yet.
* **Some domains are `unsupported` on import.** The `apply` response lists any domains whose `import_` is not yet implemented in `unsupported_domains`. Pair with `plan` first to see whether they would have changed before committing.

## Common Questions

### What is ODCS and why does AnomalyArmor use it?

ODCS is the [Open Data Contract Standard](https://bitol-io.github.io/open-data-contract-standard/latest/), a Linux Foundation project that defines a vendor-neutral YAML format for data contracts. Using it means your AnomalyArmor config is portable: Soda, Great Expectations, and dbt also speak it, so you keep optionality if you ever switch tools.

### What is the difference between `extended` and `odcs-pure` export modes?

`extended` (default) includes the full `customProperties.anomalyarmor` block so the contract round-trips with every AA-specific feature intact (drift, alert routing, blackouts). `odcs-pure` strips those blocks and emits only the ODCS-native subset (schema, quality, freshness, team). Use `odcs-pure` when handing the contract to a Soda or Great Expectations user, `extended` everywhere else.

### How do I preview contract changes before applying them?

Run `armor contract plan --asset <uuid> -f contract.yaml`. It returns a per-domain diff showing additions, modifications, and deletions without mutating anything. Apply with `armor contract apply` once the diff looks right. See [Plan an import](#plan-an-import-dry-run-diff).

### Does `contract apply` delete configs that aren't in my YAML?

No, not by default. Configs present in the live asset but missing from the YAML are warn-only, so you won't accidentally wipe rules by applying a partial contract. Pass `--prune` to explicitly delete them, and treat that flag the way you would `rm -rf`, always review the `plan` output first.

### Can I put AnomalyArmor contracts in Git?

Yes, that's one of the main reasons to use them. The YAML diffs cleanly in PRs so reviewers see which validity rule or freshness SLA changed, not a screenshot of a settings page. Export with `armor contract pull`, commit the file, and use `armor contract diff` in CI to compare branches.

### Can I validate a contract YAML without applying it?

Yes. `armor contract validate -f contract.yaml` runs the document against the official ODCS v3.1.0 JSON Schema and exits non-zero on parse or schema errors. Errors print with file path, YAML path, and line number so editors and CI can surface them inline. No DB connection required.
