Branching a PDAL Pipeline with Tags
TL;DR: Give a stage a "tag", then start each branch with a stage whose "inputs" lists that tag. One reader tagged cleaned can feed a ground branch ending in a DTM writer, a first-return branch ending in a DSM writer and a pass-through branch ending in a LAZ writer — all in one pipeline, reading and cleaning the tile once.
# Context and Motivation
This guide is part of PDAL Stage Chaining. Most pipelines are written as a straight list, and PDAL treats that list as a chain where each stage’s input is the stage before it. But a pipeline is really a directed acyclic graph, and the tag and inputs options let you write that graph explicitly. The practical payoff is avoiding repeated work: a production DTM run usually also needs a DSM, a classified point cloud and maybe an intensity image, and reading, decompressing and noise-filtering the same 2 GB tile four times is wasted time and I/O.
Branching also keeps related outputs consistent. When the DTM and DSM come from one pipeline, they are guaranteed to share the same cleaning, the same CRS handling and the same PDAL version.
# Prerequisites and Assumptions
- PDAL 2.x;
tagandinputshave been part of the pipeline format for many releases. - A tile with ground classified (for the DTM branch) and return numbers populated (for the DSM branch).
- Enough memory for the tile in standard mode. Branching pipelines do not stream, because more than one downstream consumer needs the same points.
# Step-by-Step Implementation
# Step 1 — Tag the shared stage
Add "tag": "cleaned" to the last stage whose output every branch needs — here, the noise filter after the reader.
# Step 2 — Start each branch with inputs
The first stage of each branch sets "inputs": ["cleaned"]. Every following stage in that branch chains implicitly from the one before it, exactly as in a linear pipeline.
# Step 3 — End each branch with a writer
Each branch terminates in its own writer. PDAL executes every branch that ends in a writer.
# Step 4 — Tag writers for command-line overrides
Tagging the writers (dtm_out, dsm_out, laz_out) lets you override each filename independently with --stage.<tag>.filename.
# Step 5 — Validate the graph
pdal pipeline --validate reports unknown tags and cycles; run it before the first real execution.
# Complete Working Example
{
"pipeline": [
{ "type": "readers.las", "filename": "tiles/t_0431.laz", "tag": "raw" },
{ "type": "filters.range", "limits": "Classification![7:7],Classification![18:18]",
"inputs": ["raw"], "tag": "cleaned" },
{ "type": "filters.range", "limits": "Classification[2:2]",
"inputs": ["cleaned"], "tag": "ground" },
{ "type": "writers.gdal", "filename": "out/t_0431_dtm.tif", "inputs": ["ground"],
"resolution": 1.0, "output_type": "idw", "window_size": 6,
"data_type": "float32", "tag": "dtm_out" },
{ "type": "filters.range", "limits": "ReturnNumber[1:1]",
"inputs": ["cleaned"], "tag": "first" },
{ "type": "writers.gdal", "filename": "out/t_0431_dsm.tif", "inputs": ["first"],
"resolution": 1.0, "output_type": "max", "data_type": "float32", "tag": "dsm_out" },
{ "type": "writers.las", "filename": "out/t_0431_clean.laz", "inputs": ["cleaned"],
"minor_version": 4, "dataformat_id": 6, "forward": "all", "tag": "laz_out" }
]
}Running it from Python and checking all three outputs exist:
import json
from pathlib import Path
import pdal
spec = json.loads(Path("branch.json").read_text())
p = pdal.Pipeline(json.dumps(spec))
n = p.execute()
print("points processed:", n)
for f in ("out/t_0431_dtm.tif", "out/t_0431_dsm.tif", "out/t_0431_clean.laz"):
assert Path(f).exists(), f"missing {f}"Explicit inputs on every stage is verbose but unambiguous; you can omit inputs on a stage whose input is simply the stage before it, but writing them all out makes the graph readable at a glance.
# Key Parameter Table
| Option | On | Meaning |
|---|---|---|
tag |
any stage | Names the stage’s output so other stages can refer to it |
inputs |
any non-reader stage | List of tags this stage consumes; several tags merge their views |
--stage.<tag>.<option> |
command line | Override one tagged stage’s option |
filters.merge |
after several inputs | Explicitly merges views into one before further processing |
writers.* |
branch end | Every branch ending in a writer is executed |
# Verification
- All outputs written. Each writer’s file exists after execution; a branch whose input tag is misspelled fails validation rather than silently being skipped.
- Consistent extents. The DTM and DSM rasters should share their origin and size when the same
resolutionand bounds apply; compare withgdalinfo. - Point counts per branch. Run with
--verbose 4to see how many points each stage received.
# Gotchas and Edge Cases
Branches receive copies. When two stages consume the same tag, each branch works on its own view. A filter in one branch that modifies a dimension — filters.assign changing classes, say — does not affect the other branch. That is usually what you want, but it also means memory holds more than one view at a time for stages that copy.
Multiple inputs merge. A stage with "inputs": ["a", "b"] receives both views. For writers that means both sets of points are written together — useful for merging, surprising if you meant to pick one.
No streaming. A tag consumed by more than one stage forces standard execution. If memory is the constraint, split the work into a streaming cleaning pass that writes an intermediate LAZ, then a branched pass; see splitting a blocking pipeline into two passes.
Readers do not take inputs. Tags on readers name them for later stages; inputs on a reader is meaningless. Multiple readers feeding one stage is the merge pattern covered in merging multiple readers in one pipeline.
# Frequently Asked Questions
How do I write several outputs from one PDAL pipeline?
Tag the stage whose output all outputs share, then start each output’s branch with a stage that lists that tag in inputs and end it with its own writer. PDAL runs every branch that ends in a writer.
Do branches share memory or copy the points?
Each consumer of a tag gets its own view of the points, so changes in one branch do not leak into another. Depending on the stages involved, that can mean extra memory for the duration of the run.
Can a branching pipeline run in streaming mode?
No. A stage output consumed by more than one downstream stage requires standard mode. Split the work into a streaming pass that writes an intermediate file and a branched pass over that file if memory is tight.
What happens if I reference a tag that does not exist?
Validation fails with an error naming the unknown input. Run pdal pipeline with the validate flag before executing new branched pipelines.
# Related
- PDAL Stage Chaining — how stages pass views to one another
- Merging Multiple Readers in One Pipeline — the opposite shape: many inputs, one output
- Reordering PDAL Stages for Speed — where to put shared work
- Building a DSM from First Returns — the DSM branch in depth
- Overriding Stage Options from the Command Line — tagged overrides for branch outputs