dbt-labs/dbt-core · error

Task should always operate on at least a single node

Error message

Task should always operate on at least a single node

What it means

The telemetry_request method assumes every task it serializes telemetry for operates on at least one dbt node; a task with an empty node list is a construction invariant violation, so the code panics instead of emitting an empty span-tree request. It exists to keep the span-tree telemetry contract (exactly one primary node for single-node tasks) well-defined.

Source

Thrown at crates/dbt-tasks-core/src/task.rs:226

    ///   Such tasks MUST create similar task-like spans for each underlying node themselves.
    ///
    /// If `skip_reason` is provided, the default implementation applies the same shared skip
    /// mapping used on span skip-close callbacks so `NodeEvaluated` starts with a skipped
    /// outcome already set.
    ///
    /// # Panics
    /// - If task reports zero nodes but doesn't override this method.
    /// - If task reports has no phase but doesn't override this method.
    fn telemetry_request(
        &self,
        in_dir: &Path,
        out_dir: &Path,
        skip_reason: Option<&SkipReason>,
    ) -> SpanTreeRequest<FsResult<NodeStatus>, SkipReason> {
        let mut nodes = self.dbt_nodes().into_iter();

        let Some(node) = nodes.next() else {
            unreachable!("Task should always operate on at least a single node");
        };

        if nodes.next().is_some() {
            // task for multiple nodes use current span as task span.
            // Task inner code should create actual spans for themselves.
            return SpanTreeRequest::use_current();
        }

        let Some(task_phase) = self.task_phase() else {
            unreachable!(
                "Task that doesn't belong to a phase should override telemetry_tree method"
            );
        };

        let phase = task_phase.into();
        let mut attrs = node.get_node_evaluated_event(phase, in_dir, out_dir);
        if let Some(skip_reason) = skip_reason {
            update_node_outcome_from_skip_reason(&mut attrs, skip_reason);

View on GitHub (pinned to 0267ce9170)

Solutions

  1. Fix the node-selection code so a task is never created with zero nodes, or skip telemetry_request when dbt_nodes() is empty.
  2. Guard the caller: check that the selection produced at least one node before building the task.
  3. Log and return a no-op SpanTreeRequest for empty tasks instead of reaching this method.
  4. Reproduce with the same selector flags and inspect why selection yielded nothing.

Example fix

// before
let node = nodes.next().expect("Task should always operate on at least a single node");

// after
let Some(node) = nodes.next() else { return SpanTreeRequest::use_current(); };
Defensive patterns

Strategy: validation

Validate before calling

if task.dbt_nodes().count() == 0 { return; } // skip telemetry for empty tasks

Type guard

fn has_nodes(task: &dyn AnyTask) -> bool { task.dbt_nodes().next().is_some() }

Try / catch

let Some(node) = nodes.next() else { return SpanTreeRequest::use_current(); };

Prevention

When it happens

Trigger: Calling telemetry_request on a task whose dbt_nodes() iterator yields no elements — i.e. the task was created/selected with zero nodes.

Common situations: An upstream selection filter matched nothing but the task was still instantiated; a bug in node selection (e.g. state:modified selectors returning empty while the task proceeds); running a single-node task type with a missing/misconfigured target resource.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07). Data as JSON: /api/errors/532ffeef391475b4. Report an issue: GitHub.