Skip to main content

Block pull requests on data quality

Goal​

You'll wire Measured Cloud into a dbt-driven CI pipeline so that pull requests cannot merge until the validations for the touched dbt models pass against the PR's staging schema. Two channels:

  1. GitHub Action — runs against the PR's freshly-built staging schema and posts a Check Run.
  2. dbt Cloud webhook — when a scheduled production dbt run finishes, Measured Cloud auto-runs the matching validations.

Both rely on the same primitive: a validation whose tags array contains dbt:<schema>.<model>. Tag once; both channels resolve their work from there.

Coming from checkpoints? Checkpoints were retired (see Checkpoints (retired)). Move your dbt: tags onto validations. For one release, a model that no validation is tagged for still falls back to a checkpoint carrying the tag, and runs the validation that checkpoint maps to.

Prereqs​

  • A workspace with at least one validation covering a dbt-built table.
  • The dbt manifest already ingested into Measured Cloud (see Ingest dbt artifacts).
  • A personal API key (dqk_…) with at least workspace-editor role.

Step 1 — Tag your validations​

Add the dbt:<schema>.<model> tag to every validation you want run by the CI integration. Use the validation's Edit action, or the API:

curl -X PATCH \
-H "Authorization: Bearer ${DQK}" \
-H "Content-Type: application/json" \
-d '{"data": {"tags": ["dbt:analytics.orders", "ci-blocker"]}}' \
"${DQ}/api/v2/organizations/${ORG}/workspaces/${WS}/validations/${VALIDATION_ID}"

The ci-blocker tag in the example is the optional secondary filter — useful when you also have nightly-monitoring validations against the same model that you don't want running on every PR.

To check which validations carry a tag, list the workspace's validations:

curl -H "Authorization: Bearer ${DQK}" \
"${DQ}/api/v2/organizations/${ORG}/workspaces/${WS}/validations"

Step 2 — Wire the GitHub Action​

Drop the following workflow into your dbt repository:

# .github/workflows/dq-cloud-check.yml
name: Measured Cloud
on:
pull_request:
branches: [main]

jobs:
data-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: dbt build
run: |
dbt deps
dbt build --target ci \
--vars '{"pr_schema": "dbt_pr_${{ github.event.pull_request.number }}"}'

- name: Measured Cloud pre-merge check
uses: tomplace/DQ-Cloud/providers/github-action@main
with:
dq-cloud-url: https://dq.example.com
dq-cloud-api-key: ${{ secrets.DQ_CLOUD_API_KEY }}
dq-cloud-org-id: ${{ vars.DQ_CLOUD_ORG_ID }}
dq-cloud-workspace-id: ${{ vars.DQ_CLOUD_WORKSPACE_ID }}
staging-schema: dbt_pr_${{ github.event.pull_request.number }}
tag-filter: ci-blocker
# validation-ids: <uuid>,<uuid> # optional: always run these too

The action parses target/manifest.json, resolves the affected models, runs the matching validations against the staging schema, polls each execution run until it finishes, and exits non-zero on failure. validation-ids runs specific validations on every PR, in addition to the tagged ones — prefer it when you know exactly which validations gate a repository. Its outputs are validations-triggered and validations-failed. The old checkpoint-tag-filter, fail-on-no-checkpoints, checkpoints-triggered and checkpoints-failed names still work as deprecated aliases, and include-legacy-checkpoints (default true) controls the checkpoint-tag fallback. Add it as a required check on main to actually block merges.

Full input reference: providers/github-action/README.md.

Step 3 — Wire the dbt Cloud webhook​

If your team runs dbt Cloud rather than (or in addition to) local CI, you can have dbt Cloud notify Measured Cloud directly when a production run completes.

3a. Generate a signing secret​

SECRET=$(openssl rand -hex 32)
echo "$SECRET" # save this; you need it for dbt Cloud below

The HMAC over this secret is the only thing authorizing the inbound webhook, so the PATCH below rejects a secret shorter than 32 characters with a 400. openssl rand -hex 32 produces a 64-character value — comfortably above the floor.

3b. Configure Measured Cloud​

curl -X PATCH \
-H "Authorization: Bearer ${DQK}" \
-H "Content-Type: application/json" \
-d "{\"signing_secret\": \"${SECRET}\"}" \
"${DQ}/api/v1/organizations/${ORG}/dbt/webhook-config"

Verify:

curl -H "Authorization: Bearer ${DQK}" \
"${DQ}/api/v1/organizations/${ORG}/dbt/webhook-config"
# {"data": {"signing_secret": "configured", "last_received_at": null}}

3c. Configure dbt Cloud​

In dbt Cloud, navigate to Account settings → Webhooks → Create new webhook:

  • Endpoint: https://dq.example.com/api/v1/organizations/<ORG_ID>/dbt/webhook
  • Secret: the value you generated in 3a.
  • Subscribe to: Run completed (production environment only).

dbt Cloud signs every POST with the secret as an HMAC-SHA256 over the raw body, sent in the Authorization: Token <hex> header. Measured Cloud verifies the signature before doing any work.

3d. Confirm​

Trigger a production run. After it completes, Measured Cloud's last_received_at should advance:

curl -H "Authorization: Bearer ${DQK}" \
"${DQ}/api/v1/organizations/${ORG}/dbt/webhook-config"
# {"data": {"signing_secret": "configured", "last_received_at": "2026-05-23T14:02:19+00:00"}}

Every validation tagged for the models in the run starts an execution run automatically. Watch them on each validation's Execution runs card, or list what's in flight:

curl -H "Authorization: Bearer ${DQK}" \
"${DQ}/api/v1/organizations/${ORG}/workspaces/${WS}/active-runs"

Verify​

  • Open a PR that changes a dbt model with a paired tagged validation. The Check Run "Measured Cloud" should appear on the PR within a few minutes.
  • Force a known-bad expectation (e.g. tighten a expect_column_values_to_be_unique so the staging data fails) and confirm the Check Run goes red.
  • Trigger a production dbt Cloud run and verify the auto-fire path: last_received_at advances and one execution run starts per matched validation.

Troubleshooting​

  • Webhook returns 401 "Invalid signature" — the secret in dbt Cloud and in Measured Cloud have diverged. Rotate via the PATCH in 3b and update dbt Cloud.
  • Webhook returns 404 "dbt webhook not configured" — the org has no secret stored. Run the PATCH in 3b.
  • Check Run posts but no validations ran — confirm the tag is on the validation itself and check its spelling: dbt:<schema>.<model> (the prefix and colon are required). The action's Job Summary lists the models it tried to resolve.
  • Production runs fire but staging schemas don't match — the worker uses each asset's configured datasource by default. For the GitHub Action's per-PR schema override to take effect, your worker must be on the API version that honours the X-DQ-Staging-Schema header.