If you’ve spent much time working with NetSuite Advanced PDF/HTML templates, you’ve probably seen the same problems come up more than once.
A while back, I debugged a remittance PDF that had worked perfectly for weeks and then went blank for a single customer. Nothing in the template had changed. The difference was the customer’s name: it contained an ampersand that wasn’t being escaped correctly in the XML. The fix was one line. Finding it took about an hour.
That’s the frustrating part of Advanced PDF templates. A problem that looks like a NetSuite issue might actually be FreeMarker, XML, BFO rendering, the underlying record data, or SuiteScript. Here are 10 problems that come up regularly, and practical ways to troubleshoot them.
1. Blank fields on certain records
A template may work on most records but fail when an optional field, sublist value, or address line is empty. Account for the field possibly not existing at all:
<#if field?? && field?has_content>
${field}
<#else>
${field!""}
#if>
Test with a record where the field is genuinely empty. A template can look reliable when every test record happens to have data, especially custom fields that aren’t guaranteed to be populated.
2. FreeMarker data-type and comparison errors
FreeMarker can be particular about data types. A common symptom is an error like:
For example:
<#if record.custbody_priority_score?number gte 80>
Priority customer
#if>
When you see a comparison error, check the actual types of the values involved. Depending on how NetSuite exposes a field, you may need ?number or ?string.
Checkboxes can be tricky too. Some fields return a Boolean, others a “T”/”F” string. Check the actual value rather than assuming. Parentheses can also help when using < or > comparisons, so the expression isn’t misread.
3. Table headers don't repeat on every page
A long transaction can expose a problem that isn’t visible on a short one: the table header shows up on page one but disappears once the table spills onto page two. Use a proper <thead> and <tbody> structure:
Item
Qty
Amount
<#list lines as line>
${line.item}
${line.quantity}
${line.amount}
#list>
This works most of the time, though the renderer can be inconsistent with deeply nested or heavily styled tables. Don’t test with five rows; force enough lines to spill onto a second or third page. That’s when layout problems show up.
4. Page breaks and nested tables
Advanced PDFs can get unpredictable when tables get large or are nested inside other table cells. For sections that shouldn’t split unnecessarily, try:
...
This can help keep smaller sections together, though it isn’t guaranteed for every layout. Nested tables cause similar problems once they’re tall enough to span pages. If one keeps getting cut off, try restructuring into sibling tables instead. Several smaller tables are usually easier for the renderer to handle than one large one.
5. Broken logos and images
Something that works inside NetSuite doesn’t necessarily behave the same way during PDF rendering:
<#if subsidiary.logo?has_content>
<#else>
#if>
Use NetSuite’s built-in image fields where possible rather than hardcoding URLs that may differ between environments. If you’re referencing a File Cabinet image, check its availability and access settings. A file that works while you’re logged in isn’t always accessible when the PDF actually renders. This matters most in OneWorld accounts, where subsidiaries may use different logos.
6. XML errors caused by special characters
This one’s easy to misdiagnose, since a template can work for hundreds of transactions and then fail on one record:
${customer.companyname?xml}
${customer.email?xml}
Free-text fields can contain characters with special meaning in XML, like ampersands and angle brackets. Escaping values with ?xml keeps the resulting XML valid. When the template hasn’t changed but one record suddenly fails, check the data before assuming the template is broken.
7. Headers, footers, and page numbers
Repeating headers and footers use BFO’s rendering features rather than ordinary FreeMarker variables:
Page of
The height goes on the <body> tag as footer-height (or header-height for a header), not on the macro itself. Macros without a declared height simply won’t apply, and the resulting layout can be hard to diagnose.
Also remember that <pagenumber/> and <totalpages/> are BFO tags, not FreeMarker variables. Their values aren’t known until the page renders, so they can’t be used inside FreeMarker conditionals. Always test page numbering with a document that spans multiple pages.
8. Currency and date formatting
Hardcoded formatting breaks when transactions use different currencies or regional settings. For calculated or custom values, NetSuite’s formatting functions keep output consistent:
${nsformat_currency(subtotal)}
${nsformat_date(customDate)}
Compare that with a hardcoded format like ${subtotal?string(“#,##0.00”)}, which may not hold up when the same template handles multiple currencies or regions. If your account uses more than one currency, include a non-default currency in your testing.
9. Pulling custom or SuiteScript data into a template
Sometimes the data you need isn’t in standard transaction fields. SuiteScript can supply it through a custom data source:
renderer.addCustomDataSource({
format: render.DataSource.OBJECT,
alias: 'JSON',
data: someObject
});
The alias matters. If the script names the source JSON, that’s what the template references. For data that isn’t guaranteed to exist, protect the reference:
<#if JSON??>
${JSON.someValue!""}
#if>
Document what the script provides and what the template can safely expect, or a small SuiteScript change can break a template that quietly depends on an undocumented value.
10. Grouping, subtotals, and sorting
Grouping transaction lines takes more than sorting them. You need to detect when the group changes, calculate the subtotal, and make sure the final group gets processed:
<#list lines?sort_by("category") as line>
<#if line.category != currentGroup>
<#assign currentGroup = line.category>
<#assign subtotal = 0>
#if>
<#assign subtotal = subtotal + line.amount>
#list>
Sort the data first, then track the current group as you loop through it. The easiest thing to miss is the final group: since there’s no group change after the last line, code that only prints a subtotal when the next group starts will drop the last one. Process that remaining subtotal after the loop. If you need multiple sort criteria, verify the output rather than assuming chained sort_by calls behave like a SQL multi-column ORDER BY.
The bigger lesson: identify the layer first
When an Advanced PDF template breaks, figure out which layer is responsible first. A missing value points to record data. A comparison error points to FreeMarker. A malformed document is an XML problem. A layout issue likely comes from BFO rendering. That distinction alone saves a lot of troubleshooting time.
Before publishing, test more than the happy path: a normal transaction, missing fields, multiple pages, special characters, different currencies where applicable, and empty line data. If the template relies on SuiteScript-provided data, test with it present and absent.
A little defensive design up front saves hours of debugging later, and makes the template much easier for the next developer to maintain.
How TAC Can Help
Need help with a NetSuite Advanced PDF template?
If a template is failing because of complex transaction data, custom scripting, or rendering behavior, TAC Solutions Group can help diagnose the problem and build a more reliable solution.






