dbt-labs/dbt-core · error · minijinja::Error (InvalidOperation)

Macros and sources cannot be written to disk

Error message

Macros and sources cannot be written to disk

What it means

write_file refuses to write artifacts for resource types 'macro' and 'source'. Macros and source definitions are not models with compiled artifacts, so writing them to the run artifacts directory is rejected up front rather than producing meaningless files.

Source

Thrown at crates/dbt-jinja-utils/src/phases/run/run_node_context.rs:727

            Ok(_) => {}
            Err(e) => {
                return Err(Error::new(
                    ErrorKind::InvalidOperation,
                    format!("Failed to write file: {e}"),
                ));
            }
        }

        // Return empty string on success
        Ok(MinijinjaValue::from(""))
    }
}

/// Write a file to disk
fn write_file(full_path: &Path, resource_type: &str, payload: &str) -> Result<(), Error> {
    // Check if model is a Macro or SourceDefinition
    if resource_type == "macro" || resource_type == "source" {
        return Err(Error::new(
            ErrorKind::InvalidOperation,
            "Macros and sources cannot be written to disk",
        ));
    }

    // Reconcile a target/ left behind by a different artifact layout — e.g.
    // dbt Core v1, or a pre-#14125 Fusion that wrote snapshot run artifacts as
    // a flat file where we now write a nested directory (or vice versa). Without
    // this, a stale file sitting where we now need a directory fails the write
    // below with ENOTDIR ("Not a directory"), and a stale directory sitting
    // where we now write a flat file fails with EISDIR ("Is a directory").
    // See https://github.com/dbt-labs/dbt-core/issues/15692.
    if let Some(parent) = full_path.parent()
        && !parent.is_dir()
    {
        // Parent is missing, or a stale file on the parent chain (from an older
        // layout) sits where we now need a directory. Remove the deepest such
        // file so create_dir_all can rebuild the tree, then create it.

View on GitHub (pinned to 0267ce9170)

Solutions

  1. Remove the write() call from macro or source contexts; only call it in model/seed/snapshot node contexts.
  2. Guard the call: {% if resource_type not in ['macro', 'source'] %}{{ write(payload) }}{% endif %}.
  3. Move the write into a node-level hook or the model body instead of a shared macro.

Example fix

-- before (inside a macro)
{{ write(rendered_sql) }}
-- after
{% if resource_type not in ['macro', 'source'] %}{{ write(rendered_sql) }}{% endif %}
Defensive patterns

Strategy: validation

Validate before calling

-- Jinja
{% if resource_type not in ['macro', 'source'] %}
  {{ write(payload) }}
{% endif %}

Try / catch

match write_file(&path, resource_type, payload) {
    Err(e) if e.to_string().contains("Macros and sources cannot be written") => {
        // skip artifact write for macro/source contexts instead of failing
    }
    other => other?,
}

Prevention

When it happens

Trigger: The write() context function is invoked from within a macro or a source node's rendering context (resource_type is 'macro' or 'source'), e.g. calling {{ write(payload) }} inside a macro body or during source rendering.

Common situations: Adding write() calls to shared macro helpers that also run for macros/sources; dispatch macros that render in macro context; accidental reuse of a run-node write pattern in source-parsing hooks.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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