Skip to main content
A factory is a function that generates Python nodes instead of authoring them one at a time. Use @factory when you want to create many similar tasks, assets, loaders, or checks from a list, a config, or a loop, rather than hand-writing each one. Factories are an advanced feature. Most projects author nodes directly; reach for a factory when you find yourself copy-pasting near-identical node definitions.

Defining a factory

A @factory function takes no arguments and returns one or more decorated node functions:
# factories/exports.py
from sqlbuild.factories import factory
from sqlbuild.assets import asset

TABLES = ["orders", "customers", "payments"]


@factory
def export_assets():
    nodes = []
    for table in TABLES:
        @asset(name=f"export_{table}", tags=("export",))
        def export(ctx, table=table):
            return ctx.result(metadata={"table": table}, materialized=True)
        nodes.append(export)
    return nodes
The returned nodes are discovered and added to the graph exactly as if you had written them by hand. They participate in selection, lifecycle, the DAG artifact, and integrations like any other node. A factory may return a single node or a list, tuple, or set of nodes. Every returned item must be a decorated @task, @asset, @loader, or @check function.

Folder rules

Where a factory lives determines what it is allowed to emit:
LocationMay emit
loaders/loaders only
tasks/tasks only
assets/assets only
checks/checks only
factories/any kind, including a mix
  • A single-kind factory can live in that kind’s folder (e.g. an asset-only factory in assets/), keeping it next to the nodes it generates. It can also live in factories/.
  • A factory that emits more than one kind must live in factories/.
This keeps each kind folder honest: everything in assets/, whether hand-written or factory-generated, is an asset. SQLBuild enforces this at discovery time. A factory in a kind folder that returns a foreign kind raises an error pointing you to factories/:
Factory export_pipeline in assets/ returned a loader 'raw_orders';
mixed-kind factories must live in factories/.
The factories/ folder is not created by sqb init - add it when you need it.

Mixed-kind factories

A factory in factories/ can generate a whole related pipeline at once - a loader, the asset that reads it, and a check on the result:
# factories/orders.py
from sqlbuild.factories import factory
from sqlbuild.loaders import loader
from sqlbuild.assets import asset
from sqlbuild.checks import check


@factory
def orders_pipeline():
    @loader(name="raw_orders")
    def load(ctx):
        return fetch_orders()

    @asset(name="orders_export", depends_on=load)
    def export(ctx):
        return ctx.result(materialized=True)

    @check(depends_on=export)
    def orders_export_check(ctx):
        return ctx.pass_("export ready")

    return [load, export, orders_export_check]
Generated nodes follow the same rules as directly-authored ones, including the SQL boundary: a factory-generated loader still binds to a managed source, and a factory-generated check still may not depend on SQL models.

Naming

Generated nodes need unique names across the project. Pass an explicit name= to each node a factory creates (factories almost always generate names from a loop variable or config), since relying on the function’s own name would produce duplicates.