Skip to main content

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:

  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, 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_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 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-Schema header.