Block pull requests on data quality
Goal
You'll wire DQ Cloud into a dbt-driven CI pipeline so that pull requests cannot merge until the rule suites for the touched dbt models pass against the PR's staging schema. Two channels:
- GitHub Action — runs against the PR's freshly-built staging schema and posts a Check Run.
- dbt Cloud webhook — when a scheduled production dbt run finishes, DQ Cloud auto-fires the matching checkpoints.
Both rely on the same primitive: a checkpoint whose tags array contains dbt:<schema>.<model>. Tag once; both channels resolve their work from there.
Prereqs
- A workspace with at least one checkpoint authored against a dbt-built table.
- The dbt manifest already ingested into DQ Cloud (see Ingest dbt artifacts).
- A personal API key (
dqk_…) with at least workspace-editor role.
Step 1 — Tag your checkpoints
Add the dbt:<schema>.<model> tag to every checkpoint you want fired by the CI integration. Use the API or the checkpoint detail page:
curl -X PATCH \
-H "Authorization: Bearer ${DQK}" \
-H "Content-Type: application/json" \
-d '{"data": {"tags": ["dbt:analytics.orders", "ci-blocker"]}}' \
"${DQ}/api/v1/organizations/${ORG}/workspaces/${WS}/checkpoints/${CHECKPOINT_ID}"
The ci-blocker tag in the example is the optional secondary filter — useful when you also have nightly-monitoring checkpoints against the same model that you don't want firing on every PR.
You can resolve checkpoints back by tag:
curl -H "Authorization: Bearer ${DQK}" \
"${DQ}/api/v1/organizations/${ORG}/workspaces/${WS}/checkpoints?dbt_model=analytics.orders"
Step 2 — Wire the GitHub Action
Drop the following workflow into your dbt repository:
# .github/workflows/dq-cloud-check.yml
name: DQ 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: DQ 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 }}
checkpoint-tag-filter: ci-blocker
The action parses target/manifest.json, resolves the affected models, triggers the matching checkpoints against the staging schema, polls for completion, and exits non-zero on failure. 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 DQ 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 DQ 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. DQ Cloud verifies the signature before doing any work.
3d. Confirm
Trigger a production run. After it completes, DQ 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"}}
Any checkpoints tagged for the models in the run are enqueued automatically. Watch them land in the run-queue:
curl -H "Authorization: Bearer ${DQK}" \
"${DQ}/api/v1/organizations/${ORG}/workspaces/${WS}/run-queue?status=pending"
Verify
- Open a PR that changes a dbt model with a paired tagged checkpoint. The Check Run "DQ Cloud" should appear on the PR within a few minutes.
- Force a known-bad expectation (e.g. tighten a
expect_column_values_to_be_uniqueso 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_atadvances and one validation result lands per matched checkpoint.
Troubleshooting
- Webhook returns 401 "Invalid signature" — the secret in dbt Cloud and in DQ 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 checkpoints triggered — confirm the tag 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 the checkpoint'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-Schemaheader.