========================================
Parameterize a template for many tables
========================================
.. raw:: html
.. meta::
:description: Go past a one-parameter template — declare multiple Lakehouse Plumber template parameters with defaults, override a different one per table, and layer a flowgroup preset over the template's own preset.
A template shares an action sequence across flowgroups. It declares parameters
under ``parameters`` and uses ``{{ parameter }}`` placeholders in its actions.
A flowgroup names it with ``use_template`` and supplies ``template_parameters``.
This guide builds a bronze-ingest template for two tables: orders as clustered
CSV and customers as JSON. Start with a working :doc:`pipeline `
and read :doc:`presets` if you want to share reader and table settings.
Create ``templates/`` and ``presets/`` directories if they do not exist.
See the :ref:`worked reuse diagram ` for the resulting orders
and customers flowgroups and their preset values.
Declare the parameters
======================
A template's ``parameters`` block declares what varies per table. Create
``templates/bronze_ingest.yaml`` with three of them:
.. literalinclude:: ../../_fixtures/guide_reuse_templates/templates/bronze_ingest.yaml
:language: yaml
:caption: templates/bronze_ingest.yaml
:emphasize-lines: 12-13,15-26
Each parameter is a name plus a few rules. ``entity`` is marked ``required:
true`` — every flowgroup must supply it, and omitting it stops generation with a
clear error before any Python is written. ``file_format`` and ``cluster_columns``
each carry a ``default``, which makes them optional: a flowgroup that says
nothing about them gets ``csv`` and no clustering. A parameter is optional unless ``required: true`` is set. Required parameters
must be supplied explicitly even if they also declare a default.
You do not need to declare a parameter's type; the engine does not enforce
``type`` metadata. Lists and objects render as native collections, integers can
become integers, and ``true``/``false`` becomes a boolean. Decimal results remain
strings, and numeric-looking strings can become integers. That is why ``cluster_columns`` can default
to an empty list ``[]`` and render into a real ``cluster_by`` list downstream,
not the string ``"[]"``.
The template also carries ``presets: [bronze_defaults]``, so every table it
stamps inherits the same Auto Loader reader options and streaming-table
properties. You set the bronze standard once, in the template, for all of them.
.. note::
Templating fills ``{{ }}`` first; the ``${...}`` tokens are left untouched
until later. So ``{{ entity }}`` and ``{{ file_format }}`` resolve during
template expansion, and ``${catalog}`` / ``${bronze_schema}`` /
``${landing_path}`` resolve afterwards, per environment — the same template
ships unchanged to dev, staging, and prod.
Stamp tables that differ
========================
Each table is a flowgroup that names the template and supplies its parameters.
``orders`` overrides ``cluster_columns`` — a native YAML list — and relies on the
``file_format`` default:
.. literalinclude:: ../../_fixtures/guide_reuse_templates/pipelines/orders_ingest.yaml
:language: yaml
:caption: pipelines/orders_ingest.yaml
:emphasize-lines: 7-13
``customers`` overrides a *different* parameter — ``file_format`` — and relies on
the ``cluster_columns`` default, so it needs no list at all:
.. literalinclude:: ../../_fixtures/guide_reuse_templates/pipelines/customers_ingest.yaml
:language: yaml
:caption: pipelines/customers_ingest.yaml
:emphasize-lines: 8-9
Note the two different keys. The template *declares* parameters under
``parameters``; a flowgroup *supplies* them under ``template_parameters``. Each
flowgroup names only what makes its table different — one word for the entity,
plus the one or two parameters it wants to change from the defaults.
Layer a preset on one table
===========================
Look again at ``orders_ingest.yaml``: alongside ``use_template`` it lists its own
``presets: [high_throughput]``. Template-level presets apply first; flowgroup-level
presets apply second and win on conflicts. So ``high_throughput`` can raise a
value the template's ``bronze_defaults`` already set — for ``orders`` only:
.. literalinclude:: ../../_fixtures/guide_reuse_templates/presets/high_throughput.yaml
:language: yaml
:caption: presets/high_throughput.yaml
:emphasize-lines: 11
``bronze_defaults`` sets ``cloudFiles.maxFilesPerTrigger`` to 200 for every table
the template stamps. ``orders`` layers ``high_throughput`` on top, which sets it
to 1000. ``customers`` layers nothing, so it keeps 200. The template holds the
shared standard; a flowgroup tunes one table without editing the template or
touching the other tables.
Generate and compare the output
===============================
Validate first, then generate the Lakeflow code:
.. code-block:: console
$ lhp validate --env dev
✓ discover (0.01s)
✓ preflight (0.00s)
✓ bronze 0 files
✓ validate (0.36s)
1 validated · 0.4s
$ lhp generate --env dev
✓ discover (0.01s)
✓ preflight (0.00s)
✓ bronze 2 files
✓ generate (0.40s)
✓ format (0.05s)
✓ monitoring (0.00s)
1 pipeline generated · 2 files · 0.5s
Two flowgroups in, two complete Lakeflow modules out. Open ``orders`` — it is the
full pipeline, with the layered preset and the clustering baked in, and not a
``{{ }}`` in sight:
.. literalinclude:: ../../_fixtures/guide_reuse_templates/generated/dev/bronze/orders_ingest.py
:language: python
:caption: generated/dev/bronze/orders_ingest.py
:emphasize-lines: 24-25,41
``customers`` came from the same template, yet the module differs everywhere its
parameters did:
.. literalinclude:: ../../_fixtures/guide_reuse_templates/generated/dev/bronze/customers_ingest.py
:language: python
:caption: generated/dev/bronze/customers_ingest.py
:emphasize-lines: 24-25
Read the three lines that moved. ``orders`` has
``cloudFiles.maxFilesPerTrigger`` at 1000 — the layered ``high_throughput``
preset overriding the template's 200 — while ``customers`` keeps 200. ``orders``
reads ``cloudFiles.format`` ``"csv"`` (the default) and ``customers`` reads
``"json"`` (its override). And ``orders`` carries
``cluster_by=["order_id", "order_date"]`` while ``customers``, on the empty-list
default, has no ``cluster_by`` at all. One template produced two genuinely
different modules; the only lines that changed are the ones each flowgroup chose
to change.
What you just did
=================
Two flowgroups — sixteen lines of parameters between them — generated **111 lines
of Lakeflow Python across two modules** from one template. The load-and-write
shape is written once. Defaults keep the common case terse — ``customers`` is six
lines of parameters — while per-flowgroup overrides and a layered preset handle
the exceptions without forking the template.
A third table is another short flowgroup: override the one or two parameters that
make it different, not another fifty-odd lines of Python you would keep in sync by
hand. That is how one template scales across a schema's worth of tables.
What's next
===========
- **Make a scalar expression vary.** An eligible action string containing
``{{ ... }}`` may also use Jinja2 conditions, loops and filters. The engine
evaluates individual string values; it does not expand whole-document loops
or conditionally add/remove action entries. See the templates reference for
the rendering and conversion rules.
- **See every field.** The templates reference lists the full parameter and
template file schema, and how to organize templates in subdirectories
(``use_template: ingestion/bronze_ingest``).
- **Scale past one pipeline.** A template stamps tables into one pipeline; a
blueprint stamps whole pipelines — across regions, tenants, or environments —
from one definition.