How to use templates
Creating a Template
- Prepare an Excel workbook (
.xlsx) with your desired layout, labels, and formatting. - Insert placeholders using
{{field_name}}syntax in cells where dynamic data should appear. - Upload the workbook via the Templates page in the application, choosing the appropriate template type (Batch, Formula, or Production Quote).
Generating a Report
- Navigate to a batch, formula, or production quote detail page.
- Click one of the available template buttons.
- The system renders the template against that resource and stores the generated report.
- Download the output
.xlsxfrom the reports list.
Field Reference
Templates use a simple inline syntax with the following constructs:
Placeholders
Insert a value from the resource data:
{{field_name}}
{{nested.field_name}}
Dot paths drill into nested hashes. For example, {{formula.name}} resolves to data["formula"]["name"].
Formatting
By default, placeholders are rendered as strings or decimals depending on their type. {{numberFormat}} controls the numeric type and display: the cell is stored as a number (so it can be summed and used in formulas) with an Excel number format applied for display. When a {{numberFormat}} is mixed with static text in the same cell, the cell is stored as text instead.
Each function takes optional arguments, which are passed as key/value pairs after the field name.
Dates
| Code | Value | Output |
|---|---|---|
{{field_name}} | 2024-06-01 12:34:56 UTC | 2024-06-01 12:34:56 UTC |
{{dateFormat field_name}} | 2024-06-01 12:34:56 UTC | 2024-06-01 |
{{dateFormat field_name format="full"}} | 2024-06-01 12:34:56 UTC | Saturday, June 1, 2024 |
{{dateFormat field_name format="long"}} | 2024-06-01 12:34:56 UTC | June 1, 2024 |
{{dateFormat field_name format="medium"}} | 2024-06-01 12:34:56 UTC | Jun 1, 2024 |
{{dateFormat field_name format="short"}} | 2024-06-01 12:34:56 UTC | 6/1/24 |
{{dateFormat field_name format="%m/%d/%Y"}} | 2024-06-01 12:34:56 UTC | 06/01/2024 |
Numbers
| Code | Value | Output |
|---|---|---|
{{field_name}} | 1234.56 | 1234.56 |
{{numberFormat field_name "integer"}} | 1234.56 | 1,235 |
{{numberFormat field_name "integer" roundingMode="up"}} | 1234.50 | 1,235 |
{{numberFormat field_name "integer" roundingMode="down"}} | 1234.50 | 1,234 |
{{numberFormat field_name "decimal" precision=2}} | 1234.56 | 1,234.56 |
{{numberFormat field_name "decimal" precision=2 roundingMode="up"}} | 1234.567 | 1,234.57 |
Currency
| Code | Value | Output |
|---|---|---|
{{field_name}} | 1234.56 | 1234.56 |
{{numberFormat field_name "currency"}} | 1234.56 | $1,234.56 |
{{numberFormat field_name "currency" currency="EUR"}} | 1234.56 | €1,234.56 |
{{numberFormat field_name "currency" currency="EUR" precision=0}} | 1234.56 | €1,235 |
{{numberFormat field_name "currency" currency="EUR" precision=0 roundingMode="down"}} | 1234.50 | €1,234 |
{{numberFormat field_name "currency" currency="EUR" precision=3}} | 1234.50 | €1,234.500 |
Percentages
| Code | Value | Output |
|---|---|---|
{{field_name}} | 0.1234 | 0.1234 |
{{numberFormat field_name "percentage"}} | 0.1234 | 12.34% |
{{numberFormat field_name "percentage" precision=2}} | 0.1234 | 12.34% |
{{numberFormat field_name "percentage" precision=2 roundingMode="up"}} | 0.12345 | 12.35% |
Measurements
| Code | Value | Output |
|---|---|---|
{{field_name}} | 125.5 g | 125.5 g |
{{numberFormat field_name "integer"}} | 125 g | 125 |
{{numberFormat field_name "decimal" precision=2}} | 125 g | 125.00 |
{{measurementFormat field_name}} | 125.5 g | 125.5 g |
{{measurementFormat field_name precision=2}} | 125 g | 125.00 g |
{{measurementFormat field_name precision=2 unit="kg"}} | 125 g | 0.13 kg |
Loops
Repeat a block of rows for each item in a collection:
[[#each collection_name]]
...rows to repeat...
[[/each]]
The collection name must match a top-level key in the data that resolves to an array. Inside the block, placeholders resolve against each item first, then fall back to the outer scope.
Empty collections: At the top level, body rows are preserved (for a "no items" message). Nested (inside another [[#each]]), body rows are removed.
Inline loops
To render all items of a collection into a single cell (rather than expanding rows), wrap the body in an inline {{#each}} block inside the cell. Items are joined with an optional join separator:
{{#each orders}}{{size}}{{/each}} -> "1 kg2 kg3 kg"
{{#each orders join=", "}}{{size}}{{/each}} -> "1 kg, 2 kg, 3 kg"
Orders: {{#each orders join=" / "}}{{size}}{{/each}} -> "Orders: 1 kg / 2 kg / 3 kg"
The body can mix placeholders, functions, and static text, and inline {{#each}} blocks can be nested. Placeholders resolve against each item first, then fall back to the outer scope, so an inline block placed inside a row-level [[#each]] can reference the row item's collections. An empty collection renders as an empty string.
Inline conditionals
To render part of a cell only when a condition resolves to a truthy value (anything other than nil or false), wrap it in an inline {{#if}} block:
{{#if completed_at}}{{dateFormat completed_at}}{{/if}}
An optional {{else}} branch renders when the condition is falsey:
{{#if completed_at}}{{dateFormat completed_at}}{{else}}Not completed{{/if}}
The condition is a data path and is resolved against the current scope with fallback to the outer scope, so inline {{#if}} blocks work inside [[#each]] and inline {{#each}} blocks. Blocks can be nested.
Conditionals
Include a block of rows conditionally:
[[#if condition]]
...rows...
[[/if]]
Currently the body rows are always included. The condition field is reserved for future use.
Unresolved Placeholders
Any placeholder that does not match a key in the data resolves to an empty string.
Template Types
There are three template types, each with a distinct set of available fields.
Batch Templates
Used to generate reports for batch resources.
Available fields:
| Field | Type | Description |
|---|---|---|
brand_name | string | Brand name (constant: "Brand Inc") |
formula_name | string | Full formula name: "RepositoryName / BranchName" |
distribution_type | string | Distribution channel label |
formula_sku | string | SKU string: "EXT_ID - branch_num" |
authored_at | datetime | Branch authorship timestamp |
quantity | decimal | Batch quantity |
unit | string | Unit of measurement |
total_weight | measurement | Formatted total batch weight, e.g. "100 kg" |
total_percentage | percentage | Formatted total batch percentage, e.g. "100.00000000%" |
position | integer | Batch position number |
started_at | datetime | When production started |
completed_at | datetime | When production completed (nullable) |
notes | text | Batch notes |
procedure | text | Procedure text |
initial_appearance | string | Initial appearance observation |
initial_color | string | Initial color observation |
initial_ph | string | Initial pH reading |
initial_gravity | string | Initial specific gravity |
initial_odor | string | Initial odor observation |
initial_viscosity_cps | string | Initial viscosity in cP |
hour24_appearance | string | 24-hour appearance observation |
hour24_color | string | 24-hour color observation |
hour24_ph | string | 24-hour pH reading |
hour24_gravity | string | 24-hour specific gravity |
hour24_odor | string | 24-hour odor observation |
hour24_viscosity_cps | string | 24-hour viscosity in cP |
range_ph_min | string | pH specification minimum |
range_ph_max | string | pH specification maximum |
range_gravity_min | string | Gravity specification minimum |
range_gravity_max | string | Gravity specification maximum |
range_viscosity_cps_min | string | Viscosity spec minimum (cP) |
range_viscosity_cps_max | string | Viscosity spec maximum (cP) |
total_plate_count_specification | string | TPC specification limit |
total_plate_count_test_method | string | TPC test method |
total_plate_count_test_result | text | TPC test result |
yeast_mold_count_specification | string | YMC specification limit |
yeast_mold_count_test_method | string | YMC test method |
yeast_mold_count_test_result | text | YMC test result |
viscosity_procedure | string | Viscosity test procedure |
viscosity_tool | string | Viscosity measurement tool |
phases | array | Array of phase objects (see below) |
created_at | datetime | Record creation timestamp |
updated_at | datetime | Record update timestamp |
Phases and Ingredients
The phases field is an array intended for use with [[#each phases]]. Each phase object contains:
| Field | Type | Description |
|---|---|---|
name | string | Phase name (e.g. "Oil Phase") |
ingredients | array | Array of ingredient objects (for nested [[#each ingredients]]) |
Each ingredient object contains:
| Field | Type | Description |
|---|---|---|
name | string | Material trade name |
inci_name | string | INCI name |
supplier_name | string | Supplier name |
weight_percentage | percentage | Formatted percentage, e.g. "25.00000000%" |
weight | measurement | Formatted weight, e.g. "25 kg" |
lot_number | string | Material lot number |
position | integer | Ingredient position in phase |
phase | string | Phase name (e.g. "Oil Phase") |
part_number | string | The part number of the material |
Formula Templates
Used to generate reports for formula resources.
Available fields
| Field | Type | Description |
|---|---|---|
uuid | string | UUID |
external_id | string | External identifier |
is_master | boolean | Is this the master branch? |
version_name | string | Version name |
distribution_type | string | Distribution channel label |
authored_at | datetime | When the branch was authored |
created_at | datetime | Record creation timestamp |
updated_at | datetime | When the branch was last updated |
brand_name | string | Brand name (constant: "Brand Inc") |
formula_name | string | Full formula name: "RepositoryName / BranchName" |
formula_sku | string | SKU string: "EXT_ID - branch_num" |
order_fill_size | measurement | Formatted fill size of a single unit, e.g. "100 kg" |
order_quantity | decimal | Order quantity |
order_total_size | measurement | Formatted total order size, e.g. "100 kg" |
total_cost | money | Formatted total order cost, e.g. "$500.00" |
total_cost_per_unit | money | Formatted cost per unit, e.g. "$5.00" |
total_cost_per_kg | money | Formatted cost per kg, e.g. "$5.00" |
total_excess_cost_per_unit | money | Formatted excess cost per unit, e.g. "$0.00" |
finalized_by | string | Email of the user who finalized the formula |
procedure | text | Formula procedure text |
notes | text | Formula notes text |
phases | array | Array of phase objects (see below) |
Phases and Ingredients
The phases field is an array intended for use with [[#each phases]]. Each phase object contains:
| Field | Type | Description |
|---|---|---|
name | string | Phase name (e.g. "Oil Phase") |
ingredients | array | Array of ingredient objects (for nested [[#each ingredients]]) |
Each ingredient object contains:
| Field | Type | Description |
|---|---|---|
name | string | Material trade name |
part_number | string | The part number of the selected material |
inci_name | string | INCI name |
supplier_name | string | Supplier name |
weight_percentage | percentage | Formatted percentage, e.g. "25.00000000%" |
weight | measurement | Formatted weight, e.g. "25 kg" |
cost_per_kg | money | Formatted cost, e.g. "$5.00" |
cost_percentage | percentage | Cost as a percentage of total, e.g. "25%" |
pack_size | measurement | Smallest order pack size, e.g. "1 kg" |
lead_time | string | Formatted lead time, e.g. "7 days" |
total_cost | money | Total ingredient cost, e.g. "$5.00" |
used_up_cost | money | The cost of the used up amount of the ingredient |
leftover_cost | money | Cost of unused portion, e.g. "$0.00" |
in_stock | boolean | Is the ingredient in stock? |
orders | array | Array of order objects (see below) |
Each order object contains:
| Field | Type | Description |
|---|---|---|
quantity | float | Order quantity |
size | measurement | Formatted order size, e.g. "1 kg" |
price | money | Formatted order price, e.g. "$5.00" |
Production Quote Templates
Used to generate reports for production quote resources.
Available fields
| Field | Type | Description |
|---|---|---|
uuid | string | UUID of the quoted branch |
external_id | string | External identifier |
is_master | boolean | Is this the master branch? |
version_name | string | Version name |
distribution_type | string | Distribution channel label |
created_at | datetime | Record creation timestamp |
authored_at | datetime | When the branch was authored |
updated_at | datetime | When the record was last updated |
brand_name | string | Brand name (constant: "Brand Inc") |
formula_name | string | Full formula name: "RepositoryName / BranchName" |
formula_sku | string | SKU string: "EXT_ID - branch_num" |
fill_size | measurement | Fill size of a single unit, e.g. "1 kg" |
quantity | integer | Number of units quoted |
overage | percentage | Overage applied, e.g. "5%" |
overage_cost | money | Cost added by the overage, e.g. "$5.00" |
total_order_size | measurement | Formatted total order size, e.g. "10 kg" |
total_cost | money | Formatted total order cost, e.g. "$55.00" |
total_cost_per_unit | money | Formatted cost per unit, e.g. "$5.00" |
total_cost_per_kg | money | Formatted cost per kg, e.g. "$5.00" |
total_excess_cost_per_unit | money | Formatted excess cost per unit, e.g. "$0.25" |
total_used_up | money | Formatted cost of the used up amount, e.g. "$52.50" |
total_leftover_cost | money | Formatted leftover cost, e.g. "$2.50" |
total_percentage | percentage | Total ingredient percentage, e.g. "100%" |
finalized_by | string | Email of the user who finalized the formula |
procedure | text | Formula procedure text |
notes | text | Formula notes text |
phases | array | Array of phase objects (see below) |
Phases and Ingredients
The phases field is an array intended for use with [[#each phases]]. Each phase object contains:
| Field | Type | Description |
|---|---|---|
name | string | Phase name (e.g. "Oil Phase") |
ingredients | array | Array of ingredient objects (for nested [[#each ingredients]]) |
Each ingredient object contains:
| Field | Type | Description |
|---|---|---|
name | string | Material trade name |
part_number | string | The part number of the selected material |
inci_name | string | INCI name |
supplier_name | string | Supplier name |
weight_percentage | percentage | Formatted percentage, e.g. "25.00000000%" |
weight | measurement | Formatted weight, e.g. "2.5 kg" |
cost_per_kg | money | Formatted cost, e.g. "$5.00" |
cost_percentage | percentage | Cost as a percentage of total, e.g. "25%" |
pack_size | measurement | Smallest order pack size, e.g. "1 kg" |
lead_time | string | Formatted lead time, e.g. "7 days" |
total_cost | money | Total ingredient cost, e.g. "$5.00" |
used_up_cost | money | The cost of the used up amount of the ingredient |
leftover_cost | money | Cost of unused portion, e.g. "$0.00" |
in_stock | boolean | Is the ingredient in stock? |
orders | array | Array of order objects (see below) |
Each order object contains:
| Field | Type | Description |
|---|---|---|
quantity | float | Order quantity |
size | measurement | Formatted order size, e.g. "1 kg" |
price | money | Formatted order price, e.g. "$5.00" |
Output
Generated reports are stored as a report with the output .xlsx file attached for download.
Notes
- Cell formatting (font, colors, borders, alignment, text wrap) is preserved from the template in the generated output.
- Merged cell regions and column widths are carried over.
- Placeholders can be mixed with static text in a single cell (e.g.
"Product: {{name}} as of {{formatDate updated_at}}"). {{numberFormat}}cells are stored as numbers with an Excel number format (e.g. percentages store the fraction with a0.00%format), so they can be used inSUMand other formulas.- Multiple worksheets in a single template workbook are each transformed independently.
- Any field that resolves to
nilrenders as an empty string.