Running dbt on GitHub Actions

Self-hosted dbt job runner using GitHub Actions, replacing dbt Cloud / Fivetran Transformations for automated builds, scheduled runs, and API-triggered orchestration.

A complete how-to for running your dbt project on GitHub Actions instead of dbt Cloud (now Fivetran Transformations). Covers scheduled runs, push triggers, Slack notifications, and API-triggered orchestration.

For the why, see the blog post: Replace dbt Cloud with a Free GitHub Actions Runner.

What you get

  • Push triggers — every merge to master runs dbt build.
  • Scheduled runs — nightly (or hourly, every 15 min, whatever you want) via cron.
  • Manual triggers — kick off a run from the GitHub UI with a target / command picker.
  • API triggersPOST to GitHub's workflow_dispatch endpoint to trigger runs from your app, Convex, Lambda, or any other system that can speak HTTP.
  • Slack notifications — success and failure messages with a link to the run.
  • Run artifactsmanifest.json, run_results.json, and the full dbt log saved on every run for debugging.

Architecture

your-dbt-repo/
├── .github/workflows/
│   └── dbt.yml              # The workflow
├── profiles/
│   └── profiles.yml         # CI profile (uses env vars for credentials)
├── models/
├── dbt_project.yml
└── packages.yml

One workflow file, one profiles file. That's it. Everything else is your existing dbt project.

Prerequisites

  • A GitHub repo containing your dbt project.
  • A service account / user for your warehouse with the right permissions (BigQuery service account JSON, or Snowflake user with key-pair auth).
  • A Slack incoming webhook URL (optional but recommended).

1. Create the workflow file

Create .github/workflows/dbt.yml:

name: dbt

on:
  push:
    branches:
      - main
      - master

  schedule:
    - cron: '0 6 * * *' # 6 AM UTC daily

  workflow_dispatch:
    inputs:
      target:
        description: 'dbt target'
        required: false
        default: 'prod'
        type: choice
        options:
          - dev
          - prod
      dbt_command:
        description: 'dbt command'
        required: false
        default: 'build'
        type: choice
        options:
          - build
          - run
          - test
          - seed
      selector:
        description: 'dbt selector (e.g., --select tag:nightly)'
        required: false
        default: ''
        type: string

env:
  DBT_PROFILES_DIR: ${{ github.workspace }}/profiles

jobs:
  dbt-run:
    name: dbt ${{ inputs.dbt_command || 'build' }} (${{ inputs.target || 'prod' }})
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dbt
        run: |
          pip install --upgrade pip
          pip install dbt-bigquery # or dbt-snowflake, dbt-postgres, etc.

      - name: Set up BigQuery credentials
        run: |
          mkdir -p ~/.dbt
          echo "${{ secrets.BIGQUERY_KEYFILE }}" | base64 -d > ~/.dbt/keyfile.json
          echo "BIGQUERY_KEYFILE=$HOME/.dbt/keyfile.json" >> $GITHUB_ENV

      - name: Install dbt packages
        run: dbt deps

      - name: Run dbt
        run: |
          set -o pipefail
          dbt ${{ inputs.dbt_command || 'build' }} \
            --target ${{ inputs.target || 'prod' }} \
            ${{ inputs.selector }} \
            2>&1 | tee dbt_output.log

      - name: Upload dbt artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: dbt-artifacts-${{ github.run_id }}
          path: |
            target/manifest.json
            target/run_results.json
            dbt_output.log
          retention-days: 7

      - name: Notify Slack on failure
        if: failure()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: |
            {
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plain_text",
                    "text": "dbt Job Failed",
                    "emoji": true
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {"type": "mrkdwn", "text": "*Target:*\n${{ inputs.target || 'prod' }}"},
                    {"type": "mrkdwn", "text": "*Command:*\ndbt ${{ inputs.dbt_command || 'build' }}"},
                    {"type": "mrkdwn", "text": "*Trigger:*\n${{ github.event_name }}"}
                  ]
                },
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run Details>"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

      - name: Notify Slack on success
        if: success()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: |
            {
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plain_text",
                    "text": "dbt Job Succeeded",
                    "emoji": true
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {"type": "mrkdwn", "text": "*Target:*\n${{ inputs.target || 'prod' }}"},
                    {"type": "mrkdwn", "text": "*Command:*\ndbt ${{ inputs.dbt_command || 'build' }}"}
                  ]
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

2. Create CI profiles

Create profiles/profiles.yml in the repo root. This is a CI-only profile — your local dev profile in ~/.dbt/profiles.yml is unaffected.

BigQuery

my_project:
  target: prod
  outputs:
    dev:
      type: bigquery
      method: service-account
      keyfile: "{{ env_var('BIGQUERY_KEYFILE') }}"
      project: my-gcp-project
      dataset: development
      location: US
      threads: 4
      job_execution_timeout_seconds: 300

    prod:
      type: bigquery
      method: service-account
      keyfile: "{{ env_var('BIGQUERY_KEYFILE') }}"
      project: my-gcp-project
      dataset: production
      location: US
      threads: 4
      job_execution_timeout_seconds: 300

The profile name (my_project here) must match the profile: field in your dbt_project.yml.

Snowflake

For Snowflake, swap the Install dbt step to pip install dbt-snowflake and the credentials step:

- name: Set up Snowflake credentials
  run: |
    mkdir -p ~/.dbt
    echo "${{ secrets.SNOWFLAKE_PRIVATE_KEY }}" | base64 -d > ~/.dbt/snowflake_key.p8
    echo "SNOWFLAKE_PRIVATE_KEY_PATH=$HOME/.dbt/snowflake_key.p8" >> $GITHUB_ENV
    echo "SNOWFLAKE_ACCOUNT=${{ secrets.SNOWFLAKE_ACCOUNT }}" >> $GITHUB_ENV
    echo "SNOWFLAKE_USER=${{ secrets.SNOWFLAKE_USER }}" >> $GITHUB_ENV

Profiles:

my_project:
  target: prod
  outputs:
    prod:
      type: snowflake
      account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
      user: "{{ env_var('SNOWFLAKE_USER') }}"
      private_key_path: "{{ env_var('SNOWFLAKE_PRIVATE_KEY_PATH') }}"
      role: TRANSFORMER
      database: ANALYTICS
      warehouse: TRANSFORMING
      schema: production
      threads: 4

3. Add GitHub secrets

Go to your repo → Settings → Secrets and variables → Actions.

BigQuery

Secret Description
BIGQUERY_KEYFILE Base64-encoded service account JSON
SLACK_WEBHOOK_URL Slack incoming webhook URL (optional)

To encode the keyfile:

base64 -i /path/to/keyfile.json | pbcopy

Then paste in the secret value field.

Snowflake

Secret Description
SNOWFLAKE_ACCOUNT Account identifier (e.g. xy12345.us-east-1)
SNOWFLAKE_USER Service user name
SNOWFLAKE_PRIVATE_KEY Base64-encoded PKCS8 private key
SLACK_WEBHOOK_URL Slack incoming webhook URL (optional)

4. Commit, push, and merge

The workflow only appears in the GitHub Actions UI once the workflow file is on the default branch. Open a PR, get it green, and merge.

Triggering runs

From the GitHub UI

  1. Go to the Actions tab in your repo.
  2. Select the dbt workflow.
  3. Click Run workflow.
  4. Pick target, dbt_command, and an optional selector. Click Run.

From the CLI

# Trigger a default build on prod
gh workflow run dbt.yml

# Trigger with options
gh workflow run dbt.yml \
  -f target=dev \
  -f dbt_command=run \
  -f selector='--select tag:nightly'

# List recent runs
gh run list --workflow=dbt.yml

# View logs for a specific run
gh run view <run-id> --log

From an API call

This is what makes the runner useful as an orchestration target. Any system that can POST to GitHub can trigger a dbt run:

await fetch(
  'https://api.github.com/repos/<owner>/<repo>/actions/workflows/dbt.yml/dispatches',
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${githubToken}`,
      Accept: 'application/vnd.github+json',
    },
    body: JSON.stringify({
      ref: 'master',
      inputs: { target: 'prod', dbt_command: 'build' },
    }),
  },
)

The githubToken needs actions:write permission on the repo. A fine-grained personal access token or a GitHub App installation token both work.

Triggers reference

Trigger When Default target
push Any commit to main / master prod
schedule 6 AM UTC daily (configurable cron) prod
workflow_dispatch Manual or API trigger configurable

To run more often than nightly, change the cron. GitHub Actions cron supports down to 5-minute intervals — */15 * * * * runs every 15 minutes.

To skip runs when only docs change, add a paths-ignore filter to the push trigger:

on:
  push:
    branches: [main, master]
    paths-ignore:
      - '**.md'
      - 'docs/**'

Notifications

Slack messages are posted on both success and failure with the target, command, trigger source, and a link to the run.

To send only failures, delete the Notify Slack on success step.

To send to multiple channels, add additional Slack steps with different webhook secrets, or use Slack's routing in the webhook itself.

Troubleshooting

Authentication errors

Check that BIGQUERY_KEYFILE is correctly base64-encoded and that the service account has BigQuery Job User and BigQuery Data Editor (or equivalent) on the target datasets.

# Sanity check the encoding round-trips
base64 -i keyfile.json | base64 -d | jq .

dbt deps failure

Make sure every package in packages.yml is publicly accessible, or that the runner has SSH credentials configured if you depend on a private GitHub repo.

For a private dbt package on GitHub, add a deploy key with read access on the package repo and add an SSH agent step before dbt deps:

- name: Set up SSH for private packages
  uses: webfactory/ssh-agent@v0.9.0
  with:
    ssh-private-key: ${{ secrets.DBT_PACKAGE_DEPLOY_KEY }}

Workflow not visible in the Actions tab

The workflow file has to be on the default branch (usually main or master). Open a PR and merge it. After that, all subsequent triggers — including manual runs from any branch — work normally.

A test fails and blocks the build

You can disable a single dbt test temporarily by adding {{ config(enabled=false) }} to the top of its .sql file (for singular tests), or by setting the test to severity: warn in YAML.

Cost

GitHub Actions on public repos is unlimited. On private repos, the free tier includes 2,000 minutes/month for the Free plan, 3,000 for Pro, and tiered allowances for Team and Enterprise.

A typical dbt build on a small-to-medium project takes 3–8 minutes. Even at 8 minutes per run, hourly all day every day is ~5,800 minutes/month — fits in Team plan ($4/user/month) or you bump to a paid runner overage at $0.008/minute.

What this replaces

This setup replaces dbt Cloud / Fivetran Transformations as the orchestration and scheduling layer. You keep your dbt project exactly as-is. What changes:

  • Schedules move from the dbt Cloud UI to cron in dbt.yml.
  • API triggers move from dbt Cloud's job API to GitHub's workflow_dispatch.
  • Webhooks/notifications move from dbt Cloud to Slack via this workflow.
  • The "IDE" and lineage browser go away. Use dbt docs with dbt docs generate && dbt docs serve locally, or host the generated static site on GitHub Pages / Cloudflare Pages.

For more on the why, see Replace dbt Cloud with a Free GitHub Actions Runner.