# Promote checks Dev to Prod

Source: https://userguide.qualytics.io/cli/examples/promote-checks-dev-prod/

## Goal

Move quality checks from one Qualytics environment to another, with Git as the system of record. Author and review checks in Dev, commit the YAML to a Git repo, and apply them to Prod through a controlled, repeatable process. The same flow scales to Test, Staging, or any other environment.

## Permissions

| Step | Endpoint | Role | Team permission |
| --- | --- | --- | --- |
| Export from Dev | `GET /api/quality-checks` (paginated) | `Member` | `Reporter` on the source datastore's team |
| Read containers (for name resolution) | `GET /api/containers` | `Member` | `Reporter` |
| Import to Prod (create) | `POST /api/quality-checks` | `Member` | `Author` (or `Drafter` for `Draft` status) |
| Import to Prod (update) | `PUT /api/quality-checks/{id}` | `Member` | `Author` |

> **Warning: Use a separate token per environment**
>
> Don't reuse a Dev token in Prod. Each environment should have its own service account with the minimum permissions it needs.

## Prerequisites

- The CLI is installed and authenticated for both environments (or you swap `QUALYTICS_URL` and `QUALYTICS_TOKEN` between steps).
- The target datastore in Prod has been synced. Container names referenced in the YAML must resolve in Prod.
- A Git repository to hold the exported YAML (this is what makes the workflow auditable).

## CLI workflow

```mermaid
graph LR
    Dev[(Dev datastore)] -->|export| FS[checks/ folder]
    FS -->|git add + commit| Repo[(Git repo)]
    Repo -->|git pull| CI[CI / local]
    CI -->|import --dry-run| Preview[Preview diff]
    Preview --> Apply[import without --dry-run]
    Apply --> Prod[(Prod datastore)]
```

### 1. Export from Dev

```bash
export QUALYTICS_URL=https://dev.qualytics.io
export QUALYTICS_TOKEN=$DEV_TOKEN

qualytics checks export --datastore-id 1 --output ./checks/
```

The output directory is one YAML file per check, organized by container:

```text
checks/
  orders/
    isNotNull_customer_id.yaml
    satisfiesExpression_total_positive.yaml
  customers/
    isUnique_email.yaml
```

### 2. Commit and review

```bash
git add checks/
git commit -m "Update quality checks from dev"
git push
```

A reviewer can see exactly which checks changed in the PR diff.

### 3. Preview import to Prod

```bash
export QUALYTICS_URL=https://prod.qualytics.io
export QUALYTICS_TOKEN=$PROD_TOKEN

qualytics checks import --datastore-id 99 --input ./checks/ --dry-run
```

```text
Loaded 15 check definitions from ./checks/
[DRY RUN] Importing to datastore 99...
          Import Summary
┏━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┓
┃ Datastore ID ┃ Created ┃ Updated ┃ Failed ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━┩
│ 99           │ 10      │ 5       │ 0      │
└──────────────┴─────────┴─────────┴────────┘
```

### 4. Apply

```bash
qualytics checks import --datastore-id 99 --input ./checks/
```

### 5. Promote to multiple datastores in one shot

```bash
qualytics checks import \
    --datastore-id 99 \
    --datastore-id 100 \
    --datastore-id 101 \
    --input ./checks/
```

## Behind the scenes

| CLI step | Method | Path | Notes |
| --- | --- | --- | --- |
| `checks export` (list) | GET | `/api/quality-checks?datastore_id={id}` | Paginated. The CLI walks all pages. |
| `checks export` (file write) | (local) | — | One YAML per check, with `_qualytics_check_uid` for stable identity. |
| `checks import` (resolve containers) | GET | `/api/containers?datastore_id={id}` | Maps container names to IDs in the target. |
| `checks import` (existing checks) | GET | `/api/quality-checks?datastore_id={id}` | Used to match by `_qualytics_check_uid`. |
| `checks import` (create new) | POST | `/api/quality-checks` | Per check that didn't exist in target. |
| `checks import` (update existing) | PUT | `/api/quality-checks/{id}` | Per check matched on UID. |

Match-by-UID is what makes the import idempotent. Re-running the same import is always safe.

## Python equivalent

The promote workflow has too many moving parts (paginated export, file layout, idempotent matching) to replicate fully in a few lines. For automation that's already in Python, the simplest pattern is to `subprocess` the CLI:

```python
import os
import subprocess

env = {**os.environ, "QUALYTICS_URL": "https://dev.qualytics.io",
                     "QUALYTICS_TOKEN": os.environ["DEV_TOKEN"]}
subprocess.run(
    ["qualytics", "checks", "export", "--datastore-id", "1", "--output", "./checks/"],
    env=env, check=True,
)

env = {**os.environ, "QUALYTICS_URL": "https://prod.qualytics.io",
                     "QUALYTICS_TOKEN": os.environ["PROD_TOKEN"]}
subprocess.run(
    ["qualytics", "checks", "import", "--datastore-id", "99", "--input", "./checks/", "--dry-run"],
    env=env, check=True,
)
subprocess.run(
    ["qualytics", "checks", "import", "--datastore-id", "99", "--input", "./checks/"],
    env=env, check=True,
)
```

If you genuinely need pure-Python, the rough sketch is: paginate `GET /api/quality-checks`, write per-check YAML with stable UIDs, then for each YAML in the target environment, look up by UID via `GET /api/quality-checks?_qualytics_check_uid={uid}` and PUT or POST accordingly.

## Variations and advanced usage

### Filter the export

Promote only a subset:

```bash
# Active checks for two specific containers, tagged production
qualytics checks export \
    --datastore-id 1 \
    --containers "10,20" \
    --tags "production" \
    --status Active \
    --output ./checks/
```

### Dry-run gate in CI

Make `dry-run` the PR check, and the real apply the merge step. See [GitHub Actions pipelines](https://userguide.qualytics.io/cli/examples/github-actions-pipelines/).

### Promote full configuration, not just checks

If connections, datastores, computed containers, and computed fields need to move too, use `config export` / `config import` instead. See [Export and import full configuration](https://userguide.qualytics.io/cli/examples/export-import-config/).

### Roll back

Roll back is `git revert` on the YAML, then re-import. Because import is upsert, the previous values are restored from the file.

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| `Container 'X' not found in target datastore` | Target hasn't been synced, or table doesn't exist there yet | `qualytics operations sync --datastore-id 99` first. |
| Some checks fail to update with 403 | Token has `Drafter` permission but check status is `Active` | Re-import with `Author` permission, or change status to `Draft` in the YAML. |
| Drift after import: checks have different IDs | That's expected; IDs differ per environment. UIDs are what matter. | Confirm with `additional_metadata._qualytics_check_uid` matches in source and target YAML. |
| Many "Updated" entries on a fresh Prod | Some checks already existed in Prod from previous experiments | Decide whether to keep or replace; a clean dry-run should show this clearly. |
| Re-import shows changes when nothing was edited | A timestamp or auto-generated field is being persisted | File a bug with the sample diff; export should be deterministic. |

## Related

- [Quality Checks command reference](https://userguide.qualytics.io/cli/quality-checks/)
- [Bulk-create quality checks](https://userguide.qualytics.io/cli/examples/bulk-create-quality-checks/): the precursor before there's anything to promote.
- [Export and import full configuration](https://userguide.qualytics.io/cli/examples/export-import-config/): for promoting more than just checks.
- [GitHub Actions pipelines](https://userguide.qualytics.io/cli/examples/github-actions-pipelines/): automate this whole flow.

*Last updated: May 12, 2026.*
