MISSING_PDF_PARAMETERS when the template was never set
The error names your parameters. The parameters are fine. setTemplateByScriptId failed four lines earlier and said nothing.
A batch pick-ticket process was rendering a custom Advanced PDF bound to an Item Fulfillment. It worked in Sandbox and failed in Production with:
UNEXPECTED_ERROR
{"type":"error.SuiteScriptError","name":"MISSING_PDF_PARAMETERS",
"message":"Missing PDF parameters."}
Thrown by renderAsPdf().
The wrong theory, and why it was reasonable
The name points at parameters, so that's where we looked. The renderer had three data sources attached — the fulfillment, the sales order, and an injected object — and any of them could plausibly be the missing piece:
renderer.addRecord({ templateName: 'record', record: ifRecord });
renderer.addRecord({ templateName: 'salesorder', record: soRec });
renderer.addCustomDataSource({
format: render.DataSource.OBJECT,
alias: 'binmap',
data: buildBinMap(soRec)
});
So we logged each data source, confirmed the fulfillment loaded, confirmed the object wasn't empty, and checked that every templateName matched what the FreeMarker template referenced. All correct. About ninety minutes gone.
The theory was wrong because the error is not about data sources at all.
The actual cause
The template was being set from a script parameter that could hold either a numeric internal ID or a CUSTTMPL_ script ID:
if (/^\d+$/.test(templateRef)) {
renderer.setTemplateById({ id: Number(templateRef) });
} else {
renderer.setTemplateByScriptId({ scriptId: templateRef });
}
The parameter held custtmpl_bsi_if_pickticket — lowercase, because that's how it had been typed into the deployment field.
NetSuite stores template script IDs uppercase internally. Given a case mismatch, setTemplateByScriptId does not throw, does not warn, and does not log. It simply doesn't set a template. The renderer is left in a valid-looking state with data sources attached and nothing to render them into.
Then renderAsPdf() runs, finds no template, and reports the most generic thing it can: parameters are missing. Which is technically true — the template is a parameter — and completely unhelpful, because it sends you to inspect the parameters that are there rather than the one that isn't.
The fix
if (/^\d+$/.test(templateRef)) {
renderer.setTemplateById({ id: Number(templateRef) });
} else {
/* setTemplateByScriptId fails SILENTLY on a case mismatch and
renderAsPdf then throws MISSING_PDF_PARAMETERS. Template script
IDs are stored uppercase internally, so normalize. */
renderer.setTemplateByScriptId({ scriptId: templateRef.toUpperCase() });
}
One .toUpperCase(). Also worth trimming the value — a trailing space pasted into a deployment field produces exactly the same silent failure.
What made it worse: the fallback
The pipeline had a deliberate safety net. If the custom fulfillment ticket failed to render, it fell back to NetSuite's native picking ticket so the warehouse always got paper:
let printed = false;
if (ifInfo) {
try {
printIFPickTicket(/* ... */);
printed = true;
} catch (rErr) {
result.warnings.push(
'IF pick ticket render failed, falling back to SO ticket: ' + rErr.message);
log.error(fn + 'ifTicket', rErr);
}
}
if (!printed) {
printNativePickTicket(soId, location, batchLabel, result);
}
That fallback is correct — a warehouse with no paper is a worse outcome than the wrong paper. But it meant nobody reported a failure. Tickets printed, orders shipped, and the only trace was a warning on a results record nobody was reading.
The lesson isn't to remove the fallback. It's that a fallback that nobody is notified about is a silence generator. If degraded mode is acceptable, it still has to be visible — surfaced on the results page, counted, and ideally emailed once a day. Otherwise you find out weeks later that the feature you built has never once run.
A fallback that nobody is notified about is a silence generator.
Checklist for this error
- Confirm a template was actually set before assuming the data sources are at fault.
- Normalize any template script ID to uppercase, and trim it.
- If the value comes from a script parameter or deployment field, log the exact string — including its case — rather than trusting what you think is in there.
- If it works in one account and not another, compare configuration before comparing code.
- Check whether a fallback path is hiding the failure from everyone but the log.
Related: reading a NetSuite stack trace when the error message is generic.