Bulk-by-tag task collapse (overrides[tag.*]: {mode: group})¶
When a whole layer of dbt models shares one runner and one set of
options, running each model as its own Airflow task wastes cold-starts.
Add mode: group to a tag.<name> entry under overrides: and every
tagged model collapses into ONE Airflow task per (name, runner)
bucket, running dbt run --select A B C ... on the assigned runner.
dbt-core handles internal execution order.
Legacy shape
Older configs used a separate top-level tag_groups: YAML key.
That key is a hard error at load time. Fold each entry into
overrides[tag.<t>]: {mode: group, name: ..., ...} with the same
field schema — no other behaviour change. See
YAML config schema → removed keys
for a side-by-side migration example.
Minimum viable example¶
# runners.yml
runner:
type: glue_spark
job_name: glue-etl
mode: attach
aws_conn_id: aws_default
overrides:
# 8 models tagged "bronze" -> 1 Airflow task running
# `dbt run --select b1 b2 b3 b4 b5 b6 b7 b8`
tag.bronze:
mode: group
name: bronze_batch # optional -- defaults to "bronze"
from dbt_aws.common import load_runner_config
from dbt_aws.common.builder import DbtDag
cfg = load_runner_config("runners.yml")
dag = DbtDag(
dag_id="daily",
project=...,
project_archive_s3="s3://...",
config=cfg,
)
Produces one task named dbt__bronze_batch__spark. Untagged models
stay one-task-per-node.
Rich form: group-level overrides¶
Any field the selected runner's OVERRIDE_TYPE accepts is valid inside
a mode: group entry. Set command, target, profile_name,
full_refresh, vars_json, etc. for the whole group at once:
overrides:
tag.gold:
mode: group
name: gold_batch
runner: spark
command: build # dbt build instead of dbt run
target: prod
profile_name: gold_prof
full_refresh: true
Every non-pulled-out member of gold_batch gets those overrides
injected at task-build time.
Multi-runner sub-grouping¶
If a tag's nodes route to more than one runner (via another
overrides[tag.<t>]: {runner: ...} entry pointing a subset elsewhere,
or via meta.stratus.runner on individual nodes), the group splits
into one Airflow task per runner:
runners:
spark: {type: glue_spark, job_name: glue-etl}
shell: {type: glue_python_shell, job_name: glue-pyshell}
default_runner: spark
overrides:
# Every bronze model joins the group. Routing (which runner) is
# decided independently per node -- e.g. models also tagged
# "landing" go to shell:
tag.landing:
runner: shell # dispatch-only override; no mode -> stays single by default
tag.bronze:
mode: group
name: bronze_batch
If out of 5 bronze-tagged models, 3 stay on spark and 2 also carry
tag landing (→ shell), you get two grouped tasks:
dbt__bronze_batch__spark(3 members)dbt__bronze_batch__shell(2 members)
Pull-out rule — preserve per-model behaviour¶
A node carrying ANY per-model override (in overrides[<uid>] or
meta.stratus, beyond the dispatch-only runner key) is pulled OUT of
the group and stays a singleton task. This means declaring a
group-level command: build never silently ignores a model's own
command: snapshot — the model stays its own task and its own config
wins.
overrides:
tag.bronze:
mode: group
name: bronze_batch
command: build
# `hotfix_customer` overrides one field -> pulled out as a singleton.
model.proj.hotfix_customer:
command: run # forces `dbt run`, not `dbt build`
target: hotfix_prod
Result: hotfix_customer runs on its own task with the caller's
config; the remaining bronze models still batch under
bronze_batch__spark.
The builder INFO-logs which nodes get pulled out:
INFO dbt_aws.common.builder tag_groups: 1 node(s) pulled out as singletons
due to per-model overrides: ['model.proj.hotfix_customer']
mode: single with a name: (task-id prefix)¶
The default mode: single keeps one Airflow task per node. In
a name: on a mode: single entry becomes a task-id PREFIX:
overrides:
tag.bronze:
mode: single # (default; can be omitted)
name: bronze # -> task-ids ``bronze__model__proj__b1`` etc.
runner: spark
worker_type: G.2X
Every bronze-tagged node gets bronze__ prepended to its task-id, so
siblings sort together in the Airflow Graph view. Non-bronze nodes are
untouched. Multi-tag nodes whose applying tags disagree on name:
raise ValueError at DAG-parse (tag-conflict rule).
Difference from mode: group:
| Mode | Airflow tasks per tag | name: semantics |
|---|---|---|
single (default) |
one per node | task-id prefix (<name>__<uid>) |
group |
one per (name, runner) bucket |
collapsed task-id (defaults to tag) |
Resolution ladder¶
Precedence for a node's final --command / --target /
--profile-name value, last layer wins:
1. overrides[<uid>][field]
2. meta.stratus[field]
3. overrides[tag.<t>][field] (both modes; group-level defaults)
4. runner.<field> (runner constructor default)
5. DbtDag(target=...) (DAG-level default; target only)
Composition with collapse_strategy¶
collapse_strategy="view_chain" / "aggressive" runs FIRST (structural
collapse). Then mode: group runs on top, merging what came out
further:
DbtDag(
...,
collapse_strategy="view_chain", # step 1: fold view+consumer chains
config=cfg, # step 2: bulk-by-tag on top
)
Parse-time logging¶
Whenever a mode: group entry has at least one member:
INFO dbt_aws.common.builder tag_groups distribution:
bronze_batch__spark=8, bronze_batch__shell=2
INFO dbt_aws.common.builder tag_groups: 1 node(s) pulled out as singletons
due to per-model overrides: ['model.proj.hotfix']
A WARNING fires when a mode: group entry names a tag no selected
node carries (typo guard).
What doesn't compose (yet)¶
mode: groupdoesn't currently expose anon_failure_callback:at the group level. If a member fails inside a groupeddbt run, the whole Airflow task retries.airflow_kwargs_per_task=(retries,execution_timeout,pool) applies to every task including grouped ones — you can't yet set different Airflow-side kwargs per group. If you need this, raise an issue.