Skip to content

GitHub Action

detectkit ships a composite GitHub Action — a thin wrapper around the dtk CLI — so you can run dtk run (or autotune / clean) as a CI check or a scheduled job without hand-rolling the “install Python, pip install detectkit, run the command, gate on the exit code” boilerplate yourself.

It installs detectkit from PyPI (pinned by the version input, or the latest release if you leave it unset) — it does not run this repository’s own checked-out library code. Point it at your own project with project-dir (a directory containing detectkit_project.yml and profiles.yml, exactly like running dtk locally).

.github/workflows/detectkit.yml
name: detectkit
on:
schedule:
- cron: "*/10 * * * *" # every 10 minutes
workflow_dispatch:
jobs:
monitor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run detectkit
uses: alexeiveselov92/detectkit@v0.63.0 # pin to a released tag
env:
CLICKHOUSE_PASSWORD: ${{ secrets.CLICKHOUSE_PASSWORD }}
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK_URL }}
with:
extras: clickhouse
select: "*"

That’s it: the job fails whenever dtk run would — a failed metric, a selector matching nothing, a dead database — because the action preserves dtk’s own exit code as the step’s outcome. Check the Releases page for the latest tag; the action versions ride the library’s own vX.Y.Z tags (see Environment Variables below for how ${{ secrets.* }} reaches your profiles.yml).

InputDefaultNotes
commandrunrun, autotune, or clean. Anything else fails the step (exit 2) before dtk is invoked.
select*Passed as --select. run/autotune require a selector; clean accepts either a selector (drift mode) or --orphaned-metrics via extra-args — set select: "" to use the latter.
steps""Passed as --steps for command: run (e.g. load,detect). Empty uses dtk run’s own default (load,detect,alert). Ignored (with a warning) for autotune/clean.
profile""Passed as --profile. Empty uses the project’s default_profile.
project-dir.Directory containing detectkit_project.yml / profiles.yml, relative to the checkout.
version""detectkit PyPI version spec. A bare version (0.61.0) is pinned exactly; a string starting with a pip operator (>=0.60,<0.62, ~=0.61) is used as-is. Empty installs the latest release.
extras""Comma-separated pip extras, e.g. clickhouse or duckdb,mysql — match whatever backend your profiles.yml points at. See the Databases guide.
python-version3.12Passed to actions/setup-python (detectkit requires 3.10+).
json-summarytrueFor command: run only: pass --json and capture the summary into the summary/summary-path outputs. No effect on autotune/clean (they have no --json flag).
extra-args""Extra raw arguments appended to the dtk command line, e.g. --full-refresh --force. Parsed with real shell quoting, so a quoted value with spaces round-trips correctly (extra-args: '--from "2026-01-01 00:00:00"').
OutputDescription
exit-codedtk’s exit code as a string ("0" / "1" / "2").
summaryThe raw dtk run --json document, when command: run and json-summary: true; empty otherwise.
summary-pathPath to the saved summary file on the runner, under the same condition.

dtk run / dtk autotune / dtk clean return a reliable exit code — 0 success, 1 failure, 2 usage error — documented in full in the CLI reference’s Exit Codes section. The action’s own final step re-exits with that exact code, so the job already fails on a real pipeline failure with no extra configuration — the whole point of the exit-code contract is that a failing metric fails your job, not just prints red text into a log nobody reads.

If you want a softer gate (e.g. one metric’s failure should page on-call without failing the whole workflow, or you want to branch on something finer than “did anything fail”), read the summary output instead of relying on the step’s own pass/fail:

- name: Run detectkit
id: dtk
uses: alexeiveselov92/detectkit@v0.63.0
continue-on-error: true # don't fail the job here — we'll decide below
with:
select: "tag:critical"
- name: Only page on-call for specific failures
if: steps.dtk.outputs.exit-code != '0'
run: |
echo "detectkit run failed (exit ${{ steps.dtk.outputs.exit-code }})"
# page-oncall.sh ...
exit 1 # still fail the job — just after your own logic ran

See also the CLI reference’s Scheduling section, which covers orchestrator recipes (Airflow, Dagster, Prefect) alongside GitHub Actions for non-Actions schedulers.

With the default json-summary: true (and command: run), the action’s summary output carries the same schema_version: 1 document described in the CLI reference — per-metric status, counters, timing, and the exit code, as one JSON document. Pull it apart with jq in a follow-up step:

- name: Run detectkit
id: dtk
uses: alexeiveselov92/detectkit@v0.63.0
with:
select: "*"
- name: Fail only if a specific metric errored
if: always()
run: |
echo '${{ steps.dtk.outputs.summary }}' > summary.json
jq -e '.metrics[] | select(.name == "checkout_errors" and .status == "failed")' summary.json \
&& exit 1 || true
- name: Post a run digest
if: always()
run: |
jq -r '.totals | "metrics=\(.metrics) failed=\(.failed) anomalies=\(.anomalies_detected)"' \
"${{ steps.dtk.outputs.summary-path }}"

Both summary (the value directly) and summary-path (the file on disk) are available — use whichever is more convenient for the step.

Nothing about the action changes how detectkit resolves secrets: profiles.yml and alert_channels support environment-variable interpolation via ${VAR} or {{ env_var('VAR') }} (see Environment Variables), and those variables just need to be present in the process environment when dtk actually runs.

GitHub Actions applies a step’s env: context to a composite action’s internal steps too, so set your secrets on the step that calls this action:

- name: Run detectkit
uses: alexeiveselov92/detectkit@v0.63.0
env:
CLICKHOUSE_PASSWORD: ${{ secrets.CLICKHOUSE_PASSWORD }}
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK_URL }}
with:
extras: clickhouse
profiles.yml
profiles:
prod:
type: clickhouse
host: "{{ env_var('CLICKHOUSE_HOST') }}"
password: "${CLICKHOUSE_PASSWORD}"
alert_channels:
mattermost_ops:
type: mattermost
webhook_url: "{{ env_var('MATTERMOST_WEBHOOK_URL') }}"

Never hardcode a webhook URL or database password directly in profiles.yml — commit the ${VAR} / env_var(...) placeholder and store the real value as a GitHub Actions repository or environment secret.

Hybrid mode on CI: warehouse source, ephemeral state caveat

Section titled “Hybrid mode on CI: warehouse source, ephemeral state caveat”

If your metrics read from a cloud warehouse, hybrid mode (source_profile) lets the query run against the warehouse while detectkit’s own _dtk_* bookkeeping lives in a cheap separate database — often DuckDB, a single local file with no server to provision.

On a GitHub-hosted runner, the filesystem is thrown away at the end of every job — a DuckDB state file at ./detectkit.duckdb starts empty on every run unless you do something to persist it. That breaks detectkit’s resume-from-last-timestamp idempotency: instead of “load what’s new since the last run,” every scheduled run becomes “load everything since loading_start_time, from scratch.” For a smoke test or an ad hoc backfill that’s fine (it’s what the example project in examples/action-smoke/ does). For scheduled, ongoing monitoring, it defeats the purpose:

  • Recommended: point the state profile at a real, always-on database — a small managed PostgreSQL/MySQL instance, or a self-hosted one — so state actually persists between runs the way it would on a long-lived server or container. This is the pattern hybrid mode is designed for: cheap persistent state, expensive warehouse queried only for the metric’s own SQL.
  • Best-effort alternative: actions/cache the DuckDB file between runs, keyed on something stable (the metric name, not the commit SHA). This can work for light, non-critical schedules, but the cache is explicitly best-effort — GitHub evicts entries under storage pressure or after ~7 days of no access, and a cache miss silently resets your state to empty (the next run just reloads from loading_start_time, no error, no alert). Don’t rely on it for anything where a silent state reset would be a problem — a real database removes the failure mode entirely.

Either way, dtk run in the action is oblivious to how the state database persists — persistence is entirely a property of your workflow (or your infrastructure), not something the action or hybrid mode manage for you.

examples/action-smoke/ is a complete, self-contained detectkit project used by this repository’s own action smoke test (.github/workflows/action-smoke.yml) — and doubles as a runnable example you can copy: a DuckDB profile, no external database, and one metric whose query synthesizes its own series with DuckDB’s generate_series() instead of reading from a real table. Point the action at a copy of it (extras: duckdb) to try the action end-to-end with nothing to provision.

  • CLI reference — every dtk command and flag, including Exit Codes, --json, and Scheduling (Airflow/Dagster/Prefect recipes for non-Actions orchestrators).
  • Hybrid mode — splitting warehouse source queries from detectkit’s own state.
  • DuckDB — the file-based backend used by the example project and a common choice for detectkit state.
  • Alerting channels — configuring the channels your scheduled runs will actually notify.