CI/CD and DBDeux triggers
Run dbt transformations as soon as a sync lands: a dbdeux scheduled job on success, a second job on failure, and a GitHub Actions dispatch guarded by a schedule window and concurrency group.
Once the agent writes fresh data, something downstream usually has to run: a dbt project, a warehouse refresh, a notification to the analytics team. Lifecycle actions let the agent start that step itself, so transformations follow each extraction instead of running on a clock that is either too early (stale data) or too late (wasted latency).
When to use it
- A dbt or SQL transformation project should run after every successful sync, not on its own schedule.
- Different things should happen on success and on failure (build the marts, or page the on-call channel and skip the build).
- Transformations live in a Git repository with a CI pipeline (GitHub Actions here; GitLab and Bitbucket work the same way) and you want the agent to dispatch it.
Design used here
- dbdeux scheduled job
Transform SalesDbruns on success. dbdeux is the DLH.io transformation and orchestration service; starting a job there is idempotent and cheap, so it is safe on a five-minute schedule. - dbdeux scheduled job
Sync failure triageruns on error, passing the agent run id so the job can look up the run report. - GitHub Actions dispatch runs a heavier full
dbt buildin the analytics repository, but only on weekdays between 06:00 and 20:00 Eastern, and the workflow uses a concurrency group so overlapping dispatches collapse into one build.
Configuration
agent_version: 1.9.4
state_db_path: C:/dlh/state/dlh_agent_state.duckdb
connection_information:
connection_type: sql_server
server_name_or_ip: sql01.corp.local
server_port: 1433
database_names: [SalesDb]
data_retrieval:
historical_load: false
output_format: delta
direct_cloud_write: true
storage_base_path: /data/delta_iceberg_tables
output_path: C:/dlh/output
parquet_compression: zstd
on_schema_drift_action: migrate_target
on_schema_drift_action_handling: fail_once
databases:
- name: SalesDb
sync_mode: ct
schema_refresh_mode: 1D
tables:
- name: dbo.*
all_columns: true
no_pk_strategy: full
dlh_ref:
org_guid: '<YOUR_ORG_GUID>'
prj_guid: '<YOUR_PROJECT_GUID>'
connection_guid: '<YOUR_CONNECTION_GUID>'
target_schema_prefix: Site003
api_key: '<YOUR_DLH_API_KEY>'
dlh_notifications:
enabled: true
platform_run_report: true
error_severity: error
webhooks:
- url_env_var: DLH_OPS_WEBHOOK_URL
format: teams
post_completion_actions:
enabled: true
dbdeux_jobs:
- name: 'Transform SalesDb'
job_guid: '<DBDEUX_TRANSFORM_JOB_GUID>'
credential_ref: 'dbdeux_svc'
run_condition: on_success
variables:
source_prefix: 'Site003'
- name: 'Sync failure triage'
job_guid: '<DBDEUX_TRIAGE_JOB_GUID>'
credential_ref: 'dbdeux_svc'
run_condition: on_error
cicd_dispatches:
- name: 'Full dbt build via GitHub Actions'
provider: github
owner: 'acme'
repo: 'acme-analytics-dbt'
event_type: 'dlh-agent-post-completed'
branch: 'main'
dbt_profile_target: 'prod'
variables:
source_db: 'SalesDb'
dbt_selector: 'source:sql_server_site003+'
credential_ref: 'github_dispatch'
wait_for_response: true
timeout_seconds: 30
schedule_window:
timezone: 'America/New_York'
days: [monday, tuesday, wednesday, thursday, friday]
start_time: '06:00'
end_time: '20:00'
dlh_credential__dbdeux_svc:
credential_type: api
token: '<YOUR_DBDEUX_SERVICE_ACCOUNT_TOKEN>'
is_encrypted_credential: false
dlh_credential__github_dispatch:
credential_type: scm
provider: github
token: '<YOUR_GITHUB_FINE_GRAINED_TOKEN>'
is_encrypted_credential: false| Block | Why |
|---|---|
Two dbdeux_jobs with different run_condition | on_success fires only when every table succeeded; on_error fires when any table failed or the run aborted. Routing them to different jobs means a broken table never triggers a marts build on partial data. |
variables.source_prefix | Merged into the job run request together with the values the agent always adds: triggered_by, agent_run_id and agent_run_status. The triage job can use agent_run_id to fetch the run report. |
cicd_dispatches with schedule_window | The GitHub build is skipped (not deferred) when a run finishes outside 06:00 to 20:00 Eastern on a weekday. dbdeux jobs have no window here, so they still follow every run. |
wait_for_response: true, timeout_seconds: 30 | The agent waits for GitHub's HTTP response and logs the status, so a rejected token or a missing workflow shows up in the agent log rather than silently doing nothing. |
dlh_credential__* sections | Tokens are encrypted in place on the first run. credential_ref values are the suffix after dlh_credential__. |
Lifecycle actions never fail the run: an action that errors is logged and shown in the run summary as a non-blocking action failure, and the data that was written stays written.
GitHub workflow
.github/workflows/dlh-agent-dispatch.yml in the acme-analytics-dbt repository:
name: dbt build on DLH agent completion
on:
repository_dispatch:
types: [dlh-agent-post-completed]
concurrency:
group: dlh-agent-${{ github.event.client_payload.source_db }}
cancel-in-progress: false
jobs:
dbt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.client_payload.branch }}
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements.txt
- run: dbt deps
- run: dbt build --select "${{ github.event.client_payload.dbt_selector }}" --target "${{ github.event.client_payload.dbt_profile_target }}"
env:
SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}The variables, branch and dbt_profile_target from the agent arrive in github.event.client_payload. The concurrency block is what makes a five-minute agent safe with CI: a dispatch that arrives while a build is running queues behind it, and GitHub keeps only the latest queued run per group.
The token in dlh_credential__github_dispatch needs permission to create repository_dispatch events: for a fine-grained token, Contents: Read and write on that repository; for a classic token, the repo scope.
Run behaviour
After the run summary the agent prints the post-completion phase:
[post_completion_actions] Triggering dbdeux scheduled job: Transform SalesDb (<DBDEUX_TRANSFORM_JOB_GUID>)
[post_completion_actions] dbdeux job 'Transform SalesDb' triggered successfully (412ms).
[post_completion_actions] Skipping dbdeux job 'Sync failure triage' - run_condition 'on_error' not met (run status: success).
[post_completion_actions] Dispatching CI/CD workflow: Full dbt build via GitHub Actions (github acme/acme-analytics-dbt)
[post_completion_actions] CI/CD dispatch 'Full dbt build via GitHub Actions' successful: OKWhen the run finishes at 22:15 Eastern the dispatch lines are replaced by Skipping 'Full dbt build via GitHub Actions' - outside schedule window. When a table fails, the transform job is skipped and the triage job is triggered with agent_run_status=error. In dbdeux, each triggered job run shows triggered_by and agent_run_id in its variables so you can trace it back to the agent log.
Why not just schedule dbt every 15 minutes?
A fixed dbt schedule either runs on stale data or wastes warehouse credits rebuilding unchanged models. Chaining from the agent means transformations run exactly once per delivery, and not at all when a delivery failed.
Variations
- GitLab or Bitbucket instead of GitHub: change
provider, useworkflow_reffor the branch to run on, and provide the appropriate token type. Thevariablesarrive as pipeline variables together withDLH_RUN_ID,DLH_EVENT_TYPE,DLH_BRANCHandDLH_DBT_PROFILE_TARGET. See Lifecycle actions. - Refresh Snowflake without CI: add a
sync_bridgesentry with the GUID of a DLH.io sync bridge that refreshes the Iceberg or Delta tables the agent writes. Sync bridges use thedlh_ref.api_keyand need no extra credential. - Pre-start actions:
pre_start_actionsaccepts the same action types and runs after preflight checks pass, for example to pause a downstream refresh while a large historical load runs. - Always run, then decide in the job: use
run_condition: alwayson a single dbdeux job and branch onagent_run_statusinside the job instead of configuring two entries.