Graph Construction

BioImageFlow pipelines are directed acyclic graphs (DAGs) built lazily by calling tool instances. No computation happens until compute() is called.

Nodes

Every tool call creates a Node:

segment = Segment()  # tool instance

with Workflow() as wf:
    masks = segment(image=raw["image"])
    # masks is a Node, not a DataFrame

Nodes store:

  • The tool that will execute

  • Upstream node references

  • Column bindings (which column from which upstream node)

  • Constant bindings (literal values)

Column references

Use node["column"] to create a ColumnRef:

raw = loader(folder="/data")
masks = segment(image=raw["image"])  # raw["image"] is a ColumnRef

This tells the engine: “when executing segment, resolve its image input from the image column of raw’s output DataFrame.”

The framework validates at construction time that:

  • The referenced column exists in the upstream node’s outputs

  • The type annotations are compatible (via check_compatibility())

Binding rules

Tool inputs can be bound in three ways:

Column reference — from an upstream node’s output:

masks = segment(image=raw["image"])

Node shorthand — passing a node directly resolves to the upstream column whose name equals the keyword argument. segment(image=raw) is exactly equivalent to segment(image=raw["image"]); if raw has no image column the call raises ColumnNotFoundError. This is a syntactic shortcut, not type-based matching — specs.md §4.6 is explicit that there is no implicit name-based or type-based column matching at the binding rule itself.

# Equivalent to segment(image=raw["image"]):
masks = segment(image=raw)

Constant — literal Python values:

masks = segment(image=raw["image"], threshold=0.5)

Missing required inputs (no default, no binding) raise BindingError.

Source nodes

Source nodes have no upstream — they produce the initial DataFrame from their parameters alone. There are two flavours:

DataFrameTool with accepts_upstream = False . The canonical shape for “load files” or “build a parameter sweep”. Specs.md §4.2 lists this as the recommended source pattern. The companion package bioimageflow_common_tools ships two examples:

from bioimageflow_common_tools import Files, Generate

files = Files()
sigmas = Generate()

with Workflow() as wf:
    images = files(path="/data", pattern="*.tif")
    sweep = sigmas(column_name="sigma", values=[0.5, 1.0, 2.0])

Constructing a source tool with positional upstream arguments raises SourceToolUpstreamError.

ProcessingTool with no column references. Useful when the loader itself needs an isolated environment — for example a DICOM or HDF5 reader that depends on a heavy native library. The tool reads from constants, runs in its own environment, and emits a one-row output DataFrame:

class LoadDicom(ProcessingTool):
    display_name = "Load DICOM"
    environment = EnvironmentSpec(name="dicom", dependencies={"pydicom": "*"})

    class Inputs:
        folder: str

    class Outputs:
        image: Annotated[Path, ImageSpec()] = Template("out.tif")

    def process_row(self, arguments: Arguments) -> "LoadDicom.Outputs":
        ...

Both flavours are valid sources; pick the one that matches the environment requirements of the loader.

Branching

Branches are just two nodes that read the same upstream column:

raw = files(path="/data", pattern="*.tif")
blurred = blur(image=raw["path"])
thresholded = threshold(image=raw["path"])  # second branch

Reusing two refs in one downstream tool creates a fan-in:

overlay = compose(image=raw["path"], mask=thresholded["mask"])

Combining branches with explicit join semantics is covered in Merge Strategies.

Named nodes

By default, nodes derive their storage name from the tool class name and a numeric disambiguator. Use the call-time name keyword to assign stable node identities or disambiguate multiple uses of the same tool:

blur = GaussianBlur()

smooth_1 = blur(image=raw["image"], sigma=1.0, name="blur_fine")
smooth_5 = blur(image=raw["image"], sigma=5.0, name="blur_coarse")

Node names must be unique within a workflow.

DAG validation

Construction-time errors (cycles, missing bindings, type mismatches, duplicate names, …) are reported through the validation surface; the full ValidationErrorKind table lives in Reference under the errors page once it lands. For now the exhaustive contract is in BioImageFlow Library Specifications §6.6.