======================================== 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.