Repeats and conditional content
Two blocks make a template respond to the data it is rendering: Repeat for each loops over a record’s children, and Show if hides or shows content based on the record’s values. This page covers both.
Repeat for each
Section titled “Repeat for each”A Repeat for each block loops over a child relationship of the base object. The block’s children render once per row: one row per Opportunity Line Item, one row per Contact, and so on.
- Source relationship: pick from the dropdown of available child relationships. The list only shows relationships you have access to. You can also insert a related list straight from the Merge fields panel, which wraps the selection in a Repeat for you.
- Filter (optional): narrow the rows that render, for example
Opportunities.StageName != 'Closed Lost'. See What a filter can read below. - Render mode: a repeat can render as stacked blocks or as table rows, one row per child record, which is the layout invoices, statements and order summaries reach for.
- Inside a Repeat block, the Merge fields panel flips to show the child object’s fields. Insert chips like
{{Opportunities.Amount}}; they resolve per row at runtime. - Five-hop parent lookups also work inside Repeat blocks, for example
Opportunities.Account.Owner.Profile.Name. - Row order: child rows print oldest first, and the preview and the generated document agree on that order. If you need a different order, sort within the record’s own data using a formula field, or narrow the set with the repeat’s filter.
- Nested repeats are supported to arbitrary depth, for example an Account that repeats over its Opportunities, each of which repeats over its line items. Sections, callouts and conditional blocks can sit inside a repeat. The same relationship can be repeated at two levels (Cases at the top, and Cases inside each Contact), and a condition inside a nested repeat can read the outer row.
- A signature block inside a repeat prints the row’s value in its signer label.
What a filter can read
Section titled “What a filter can read”A filter is evaluated once per row, and it can compare:
- a field of the row, such as
Contacts.LastName; - a field through the row’s parent, such as
Contacts.ReportsTo.LastNameorContacts.Account.Industry; - a lookup’s id, such as
Contacts.OwnerId; - a field of the main record, such as
Account.Industry; - a field of the repeat around it, when this repeat sits inside another one.
This works the same in the PDF, in Word and in the editor preview. The Filter picker lists the fields around the repeat rather than the row’s parents, so type a parent path by hand. A filter the document cannot read (an unknown field, a path past five hops, a field you cannot read) is refused when you save, with a message naming the field and where it can be read from.
After upgrading to 3.0, some repeats can print different rows. Before 3.0, a filter on anything other than a plain field of the row read that field as empty, so an
=filter printed no rows and a!=filter printed every row. 3.0 reads the real value, with no republish needed, so review filters that compare a parent, main-record, enclosing-repeat or lookup-id field. A repeat over Approvals also applies its filter now.
Show if
Section titled “Show if”A Show if block conditionally renders its children based on parent-record data. Expressions are parsed at save time, so any syntax errors surface in the right rail as plain-English messages while you author.
| Construct | Example |
|---|---|
| Comparison | Account.AnnualRevenue > 1000000 |
| Equality | Account.Industry = 'Technology' |
| Inequality | Account.Industry != 'Healthcare' |
| Contains | Account.Name contains 'Acme' |
| Logical AND | Account.IsActive = true AND Account.AnnualRevenue > 1000000 |
| Logical OR | Account.Industry = 'Technology' OR Account.Industry = 'Software' |
| Negation | NOT Account.Industry = 'Healthcare' |
| Grouping | (Account.Industry = 'A' OR Account.Industry = 'B') AND Account.Amount > 1000000 |
| Null check | Account.Description = null |
| Child count | COUNT:Opportunities > 0 |
| Other aggregates| SUM:Opportunities.Amount > 10000 (also AVG, MIN, MAX) |
Operators are =, !=, >, >=, <, <= and contains (case-insensitive substring). Literals are single-quoted strings, decimals including negative ones, true, false and null.
A condition reads the main record, its parents (Account.Owner.Name), lookup ids and, inside a repeat, the current row and the rows of any repeat around it.
A related list only inside a repeat over it
Section titled “A related list only inside a repeat over it”A condition such as Contacts.LastName = 'Smith' placed outside a repeat over Contacts has no single contact to compare, so it is refused when you save:
Change the condition: Contacts.LastName is on the related list Contacts, and a condition can only read a related list inside a repeat over it. Move the conditional block inside a repeat over Contacts, or test the list itself with COUNT:Contacts > 0.
Before 3.0 such a condition saved without complaint and was always false (or, written negatively, always true). A release already published with one keeps generating as it did, but a draft carrying one reads as not valid until you fix it.
Approval History is the exception to the COUNT advice: totals over approvals are not supported, so COUNT:Approvals and the other aggregates over Approvals are refused. Show approvals with a repeat over Approvals instead.
The else branch
Section titled “The else branch”A Show if block carries an optional else branch, so a single block renders one set of children when its condition is met and a different set when it is not, instead of two separate blocks kept in sync with inverted expressions. Editing inside the else branch works as it does anywhere else: duplicate, add a block after, and move all act on the branch, and merge fields there get the same checks as the rest of the document.
Note: the same expression grammar decides whether an item is included in a bundle. See Multi-template bundles.