So far in our Advanced PDF Templates series we’ve covered branding, FreeMarker, and debugging common errors. In this post we go one step further: using NetSuite saved searches as a data source inside Advanced PDF Templates to build truly report-style documents β think aging summaries on a statement, top-selling items on an invoice, or a vendor performance scorecard on a purchase order.
Why Combine Saved Searches with PDF Templates?
By default, an Advanced PDF Template only has access to the record it is printing. Saved searches let you pull related data β historical transactions, aggregated KPIs, or filtered lists β and embed them directly into the document. The result is a polished, branded PDF that reads more like a dashboard than a static form.
- Add an A/R aging table to every customer statement.
- Show the customer’s last 5 orders on an invoice.
- Embed a project task summary on a project status report.
- Print a vendor’s on-time delivery rate on a purchase order.
The Architecture: How It Works
Advanced PDF Templates cannot run saved searches directly. The standard pattern is:
- Create a saved search with the data you want to expose.
- Write a small User Event or Suitelet script that runs the search and attaches the results to the record’s
context(or to a custom field rendered as JSON). - Override the print button or use a custom Print Template that passes the enriched context into the PDF.
- In the template, iterate over the new data using FreeMarker.
Step 1 β Build the Saved Search
Create a Transaction saved search filtered to the relevant customer and grouped by aging bucket. Make sure to:
- Use an available filter for Customer (Internal ID) so the script can pass it at runtime.
- Limit the result columns to what you’ll display β fewer columns means faster rendering.
- Set the search to Public so server scripts can access it.
- Note the search’s internal ID (e.g.,
customsearch_ar_aging_by_customer).
Step 2 β Expose the Data with SuiteScript
A lightweight 2.1 User Event script can attach the search results to the record before the PDF is generated:
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/search'], (search) => {
const beforeLoad = (ctx) => {
if (ctx.type !== ctx.UserEventType.PRINT) return;
const customerId = ctx.newRecord.getValue({ fieldId: 'entity' });
const results = search.load({ id: 'customsearch_ar_aging_by_customer' })
.runPaged({ pageSize: 100 })
.fetch({ index: 0 }).data
.map(r => ({
bucket: r.getText({ name: 'agingbucket' }),
amount: r.getValue({ name: 'amount' })
}));
ctx.newRecord.setValue({
fieldId: 'custbody_ar_aging_json',
value: JSON.stringify(results)
});
};
return { beforeLoad };
});
Create a body custom field custbody_ar_aging_json (Long Text, hidden) on the Statement to receive the JSON payload.
Step 3 β Render the Data in the Template
Inside the Advanced PDF Template, parse the JSON and loop through it using FreeMarker:
<#assign aging = record.custbody_ar_aging_json?eval />
<table class="aging" style="width:100%; border-collapse:collapse;">
<thead>
<tr style="background:#eee;">
<th style="text-align:left;">Aging Bucket</th>
<th style="text-align:right;">Amount</th>
</tr>
</thead>
<tbody>
<#list aging as row>
<tr>
<td>${row.bucket}</td>
<td style="text-align:right;">${row.amount?number?string.currency}</td>
</tr>
</#list>
</tbody>
</table>
Alternative: Use a Custom Print Suitelet
If you need richer data or don’t want to persist JSON on the record, write a Suitelet that:
- Loads the source record.
- Runs one or more saved searches.
- Builds a context object with
record.load+ the search results. - Calls
render.create()with the Advanced PDF template ID and renders the merged result.
const renderer = render.create();
renderer.templateContent = template.content;
renderer.addRecord({ templateName: 'record', record: invoice });
renderer.addCustomDataSource({
format: render.DataSource.OBJECT,
alias: 'recentOrders',
data: { items: recentOrdersArray }
});
return renderer.renderAsPdf();
In the template you can then reference ${recentOrders.items[0].tranid} directly.
Performance and Governance Tips
- Cap result sizes. Set a search row limit (e.g., 100) β Advanced PDF rendering happens synchronously and slow searches block the user.
- Cache where possible. If the data changes rarely, store the JSON on the record and refresh it nightly with a scheduled script.
- Watch governance units. A 2.1 User Event has 1,000 units; a typical paged saved search uses about 10 per page.
- Avoid
?evalon untrusted input. The JSON should only ever come from your own scripts.
Common Pitfalls
- Forgetting to make the saved search Public β the template will silently skip the section.
- Storing the JSON in a Free-Form Text field (limited to 300 characters). Use Long Text instead.
- Embedding currency symbols in the JSON rather than formatting in FreeMarker, which breaks multi-currency printing.
- Hard-coding the search internal ID instead of using a script parameter β makes promotion between sandbox and production painful.
Final Thoughts
Combining saved searches with Advanced PDF Templates unlocks an entirely new class of document β branded, dynamic, data-rich PDFs that put real business context in front of customers and vendors. With a small SuiteScript helper and a few lines of FreeMarker, your statements, invoices, and purchase orders can carry the same insight as a NetSuite dashboard. In the next post, we’ll close out this series by looking at localization and multi-language Advanced PDF Templates.
Discover more from The NetSuite Pro
Subscribe to get the latest posts sent to your email.
Leave a Reply