name: pdf-generation-patterns description: "Generating PDF documents from Salesforce using Visualforce renderAs='pdf', PageReference.getContentAsPDF(), and ContentVersion storage. Covers the Flying Saucer rendering engine constraints, inline CSS requirements, server-side data loading, LWC PDF limitations, and programmatic PDF attachment to records. NOT for Quote PDF template customization (use apex/quote-pdf-customization). NOT for OmniStudio DocGen document generation. NOT for Salesforce Reports exported as PDF." category: apex salesforce-version: "Spring '25+" well-architected-pillars:
- Security
- Reliability
- Performance triggers:
- "how do I generate a PDF from a Visualforce page in Salesforce and save it to Files"
- "my Visualforce PDF is blank or broken — JavaScript not running and external stylesheets not loading"
- "how to programmatically create a PDF and attach it to a record using Apex"
- "LWC component needs to output a PDF — what is the supported pattern"
- "how to store a generated PDF as a ContentVersion linked to a Salesforce record" tags:
- pdf-generation
- visualforce
- renderAs-pdf
- flying-saucer
- pageReference
- contentVersion
- contentDocumentLink
- inline-css
- apex inputs:
- "Source record Id (Opportunity, Account, custom object, etc.) whose data populates the PDF"
- "Delivery mechanism: on-demand browser download, programmatic attachment to ContentVersion, or email attachment"
- "Design assets: logos, fonts — whether hosted as static resources"
- "Whether the PDF must be generated by an LWC (requires VF wrapper pattern)"
- "Async vs. synchronous generation requirement — determines Queueable vs. direct controller approach" outputs:
- "Visualforce page with renderAs='pdf', showHeader='false', sidebar='false', and server-side controller"
- "Apex controller class loading all required data server-side with FLS-safe SOQL (WITH USER_MODE)"
- "Inline CSS using CSS 2.1 table-based layout (no Flexbox, Grid, or JS-dependent styles)"
- "Queueable Apex class calling PageReference.getContentAsPDF(), null-checking the blob, and inserting ContentVersion + ContentDocumentLink"
- "LWC-to-VF wrapper wiring if LWC-initiated PDF generation is required" dependencies: [] version: 1.0.0 author: Pranav Nagrecha updated: 2026-04-06
PDF Generation Patterns
This skill activates when a Salesforce implementation needs to generate PDF documents — either rendered on demand from a Visualforce page or programmatically stored as Files (ContentVersion) on Salesforce records. It covers the complete lifecycle: Flying Saucer engine constraints, CSS and JavaScript restrictions, Apex controller design, PageReference.getContentAsPDF() usage, ContentVersion storage, LWC limitations, and async generation patterns.
Before Starting
Gather this context before working on anything in this domain:
- Rendering engine constraints: Salesforce uses the Flying Saucer HTML-to-PDF engine for
renderAs='pdf'on<apex:page>. This engine executes no JavaScript and ignores external CDN stylesheets. All CSS must be inlined or referenced via a Salesforce-hosted static resource, and all data must be loaded server-side in the Apex controller before the page renders. - Data loading: There is no client-side execution during PDF rendering. Any SOQL, field lookups, or computed values must be resolved in the Apex controller's constructor or lazy-loaded properties before the page is returned to the renderer.
- LWC limitation: LWC cannot set
renderAs='pdf'and cannot produce a PDF natively. The documented pattern is a VF wrapper page that acts as the PDF template, invoked from an LWC via a navigation API or programmatically through a headless Apex Queueable. - Callout limit:
PageReference.getContentAsPDF()counts as one callout against the 100-callout-per-transaction limit. Bulk PDF generation must be scoped accordingly. - Delivery mechanism: Determine upfront whether the PDF is a browser download (VF page rendered directly), a programmatic attachment to a record (ContentVersion + ContentDocumentLink), or an email attachment (Messaging.EmailFileAttachment).
Core Concepts
Concept 1: Visualforce renderAs='pdf' and the Flying Saucer Engine
Setting renderAs="pdf" on <apex:page> directs Salesforce to pipe the rendered HTML through the Flying Saucer library (an iText-based HTML-to-PDF converter) before sending the response. Consequences that cause the majority of production bugs:
- JavaScript is completely ignored. There is no JS engine in the rendering pipeline. Scripts embedded in or referenced by the page produce no output and throw no error — they are silently skipped.
- External CDN stylesheets are not loaded.
<link href="https://cdn.example.com/style.css">references are ignored by the renderer because it does not make external authenticated calls on behalf of the browser session. All CSS must be inline<style>blocks or Salesforce-hosted static resources. - Only CSS 2.1 is supported. Flexbox (
display: flex), CSS Grid (display: grid), CSS custom properties (var(--color)), and CSS animations are silently ignored. Use table-based layout (display: table,display: table-cell) or floats for column alignment. - Page attributes required for clean PDF output:
showHeader="false" sidebar="false"on<apex:page>suppress the Salesforce chrome that would otherwise appear as page headers in the PDF. - Page breaks are controlled by the CSS properties
page-break-before,page-break-after, andpage-break-inside.
Concept 2: Server-Side Data Loading in the Apex Controller
Because the PDF renderer has no JavaScript engine, every piece of data shown in the PDF must be resolved in Apex before the page HTML is produced. Specific requirements:
- SOQL queries run in the constructor or in
@AuraEnabled getproperties — never deferred. - All conditional sections use
rendered="{!booleanProperty}"wherebooleanPropertyis a server-side Boolean on the controller. Do not use CSSdisplay:noneto hide sensitive content; hidden elements are still present in the HTML and visible if the raw HTML is inspected. - Use
WITH USER_MODEon all SOQL queries (available Summer '23+) to enforce FLS at the database level without manualSchemachecks. - Images and logos must be referenced by absolute URL (built in Apex using
URL.getSalesforceBaseUrl().toExternalForm()) or embedded as base64 data URIs. The{!$Resource.LogoName}expression resolves to a relative path that the Flying Saucer renderer cannot follow.
Concept 3: PageReference.getContentAsPDF() for Programmatic PDF Generation
PageReference.getContentAsPDF() renders a Visualforce page to a raw PDF Blob from within Apex server-side code, enabling automated PDF creation without user interaction. This method:
- Performs an internal HTTP callout to the VF page URL. It counts against the 100-callout-per-transaction limit.
- Returns
null(not an exception) when the VF page throws an unhandled exception. Code that skips a null check will silently insert a 0-byte file or throw a NullPointerException downstream. - Cannot be called inside a trigger context (callout-after-DML restriction). Must be invoked from a Queueable implementing
Database.AllowsCallouts, a@future(callout=true)method, or a Batch class implementingDatabase.AllowsCallouts. - Is subject to a 120-second timeout. Pages with large SOQL result sets or complex rendering must be optimized for server-side speed.
- The returned
Blobis stored as aContentVersion(fieldVersionData) and linked to the source record viaContentDocumentLink.
Concept 4: LWC PDF Limitation and the VF Wrapper Pattern
LWC does not support renderAs="pdf" and cannot produce a PDF natively. The documented Salesforce pattern for LWC-initiated PDF generation is:
- Author a Visualforce page as the PDF template with a server-side Apex controller.
- Either navigate to the VF page URL from the LWC (using
NavigationMixin.Navigatewithtype: 'standard__webPage') for a browser-download flow, or invoke a headless Apex method from the LWC that enqueues a Queueable to generate and store the PDF as a ContentVersion.
Common Patterns
Pattern 1: On-Demand PDF Rendered Directly in Browser
When to use: A user opens a record page, clicks "Download PDF," and the PDF downloads directly — no file storage needed.
How it works:
- Create a Visualforce page:
<apex:page controller="InvoicePdfController" renderAs="pdf" showHeader="false" sidebar="false">. - In
InvoicePdfController, query all required data in the constructor usingWITH USER_MODE. - Build the logo absolute URL in the constructor:
logoUrl = URL.getSalesforceBaseUrl().toExternalForm() + '/resource/' + RESOURCE_ID + '/logo.png';or embed as base64. - Use inline
<style>with CSS 2.1 table layout for columns. No<script>tags. - Expose the page via a button on the record page, passing
?id={!recordId}as a parameter.
Why not the alternative: Using {!$Resource.Logo} directly in an <img src> produces a relative URL the renderer cannot follow, resulting in a broken image. Loading data via AJAX/JS fails silently — the table renders empty.
Pattern 2: Programmatic PDF Attachment via Queueable
When to use: A PDF must be automatically generated and stored as a File on a record when triggered by a process (Flow, trigger, or platform event) — no user interaction.
How it works:
- An Apex trigger or Flow invokes a Queueable class (
PdfAttachmentJob) that implementsDatabase.AllowsCallouts, passing the source record Id. - Inside
execute(): build aPageReferenceto the VF page with?id=<recordId>, call.getContentAsPDF(), check for null, create aContentVersionwithVersionData = blob, then link it viaContentDocumentLinkwithLinkedEntityId = recordId. - Null-check pattern:
Blob pdf = pageRef.getContentAsPDF(); if (pdf == null) { /* log and return */ }.
Why not the alternative: Calling getContentAsPDF() synchronously in a trigger violates the callout-after-DML restriction and throws a System.CalloutException: Callout from triggers are currently not supported. The async Queueable pattern is the only safe path.
Pattern 3: LWC-Initiated PDF Generation
When to use: An LWC component on the record page has a "Generate Report" button and the PDF must be saved to the record's Files and surfaced back in the UI.
How it works:
- LWC calls an
@AuraEnabledApex method that enqueuesPdfAttachmentJob(Pattern 2). - The LWC displays a spinner while the job completes. On completion, it refreshes the Files related list using
refreshApexor LMS. - Alternatively, for a synchronous browser-download flow, the LWC uses
NavigationMixin.Navigateto open the VF page URL in a new tab.
Why not the alternative: Attempting to generate the PDF entirely in the LWC JavaScript layer is not possible — there is no Salesforce-native PDF library available in client-side JS. Third-party client-side libraries (jsPDF, etc.) are unofficial, unsupported, and unreliable for complex layouts.
Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| User-triggered browser download | VF page with renderAs='pdf', opened via button URL | Simplest path; no async complexity |
| Auto-attach PDF on record change | Queueable implementing Database.AllowsCallouts | Callouts prohibited in trigger context |
| LWC button → save PDF to Files | LWC calls @AuraEnabled method → enqueues Queueable | LWC cannot render PDF natively |
| Logo not appearing in PDF output | Absolute URL in Apex or base64 data URI in CSS | Renderer cannot follow relative $Resource paths |
| CSS layout broken in PDF | Replace Flexbox/Grid with CSS 2.1 table layout | Flying Saucer supports CSS 2.1 only |
| Bulk generation (100+ records) | Batch Apex with scope=1, each scope generates one PDF | getContentAsPDF() counts against 100-callout limit |
| Quote-specific PDF | Use apex/quote-pdf-customization skill instead | Quote line item and template concerns handled separately |
Recommended Workflow
Step-by-step instructions for an AI agent or practitioner working on this task:
- Clarify the trigger and delivery mechanism — Determine whether the PDF is user-initiated (browser download) or system-initiated (attached to a record). Identify the source record type and the data required. Confirm whether an LWC is involved.
- Design the Apex controller — Write a custom controller (not a standard controller extension unless the object is natively supported). Load all required data in the constructor with
WITH USER_MODESOQL. Build all computed values (logo URL, conditional flags, formatted dates) as server-side properties. - Author the Visualforce page — Set
renderAs="pdf" showHeader="false" sidebar="false"on<apex:page>. Include no<script>tags. Use<style>blocks with CSS 2.1 table-based layout only. Reference logos via absolute URL or base64. Userendered="{!property}"for conditional sections. - Test the VF page iteratively — Open the VF page URL in a browser with
?id=<recordId>. TogglerenderAs="pdf"on and off to compare HTML vs. PDF output. Confirm logo visibility, column alignment, and page breaks. - Implement programmatic generation if needed — Build a Queueable implementing
Database.AllowsCallouts. Inexecute(): callgetContentAsPDF(), null-check the blob, insert aContentVersion(withPathOnClient,Title,VersionData), query the resultingContentDocumentId, then insert aContentDocumentLinkto the source record. - Wire the delivery path — For trigger/Flow invocation: enqueue the Queueable from a trigger handler or an Apex action in Flow. For LWC: expose the enqueue call via an
@AuraEnabledmethod and handle spinner/refresh in the component. - Validate security — Run the full flow as a non-admin user with minimum read access. Confirm no FLS violations in debug logs. Confirm
ContentDocumentLinkShareTypeis set appropriately ('V'for Viewer,'I'for Inferred).
Review Checklist
Run through these before marking work in this area complete:
-
<apex:page>hasrenderAs="pdf" showHeader="false" sidebar="false" - No
<script>tags in the VF page — all logic is server-side Apex - SOQL uses
WITH USER_MODEor explicit FLS/CRUD checks - Logo/images use absolute URL built in Apex or are embedded as base64 data URIs — not
{!$Resource.X}directly in<img src> - CSS uses CSS 2.1 table layout — no Flexbox, Grid, or CSS custom properties
- Conditional sections use
rendered="{!boolProp}"— not CSSdisplay:none - If programmatic:
getContentAsPDF()is called from Queueable or Future method withDatabase.AllowsCallouts -
getContentAsPDF()return value is checked for null before inserting ContentVersion - ContentDocumentLink is inserted with correct
LinkedEntityIdandShareType - Tested as a non-admin user; no data exposure through hidden rendered sections
Salesforce-Specific Gotchas
Non-obvious platform behaviors that cause real production problems:
- JavaScript is silently ignored by the PDF renderer — The Flying Saucer engine has no JS execution context. Scripts and any layout they produce are absent in the PDF with no error. All layout and data must be resolved server-side.
- External CDN stylesheets are not fetched — A
<link>to an external stylesheet (Bootstrap, Tailwind, any CDN URL) is silently skipped by the renderer. CSS must be in an inline<style>block or a Salesforce-hosted static resource. {!$Resource.LogoName}in<img src>produces a broken image — The expression resolves to a relative URL. The renderer makes its own HTTP request and cannot resolve it. Build the absolute URL in Apex usingURL.getSalesforceBaseUrl()or embed as base64.getContentAsPDF()returns null on VF page error — not an exception — If the VF page throws, the method silently returns null. Omitting a null check leads to a NullPointerException when reading the blob or inserts a 0-byte ContentVersion.- Callout-after-DML restriction prevents use in triggers — If any DML occurred before
getContentAsPDF()in the same transaction, aCalloutExceptionis thrown. Always call it from a Queueable or@future(callout=true)method.
Output Artifacts
| Artifact | Description |
|---|---|
| Visualforce page | .page file with renderAs="pdf", CSS 2.1 table layout, no script tags, conditional rendered attributes |
| Apex controller class | Loads all data server-side, builds absolute logo URL, exposes Boolean flags for conditional sections |
| Queueable PDF attachment class | Calls getContentAsPDF(), null-checks blob, inserts ContentVersion + ContentDocumentLink |
| LWC wiring (if applicable) | @AuraEnabled Apex method + LWC spinner/refresh pattern |
| Checker script output | Issues list from check_pdf_generation_patterns.py |
Related Skills
apex/quote-pdf-customization— Quote-specific PDF generation including CPQ line items, multi-language quote templates, and standard controller extensions; use instead of this skill for Quote PDFsapex/visualforce-fundamentals— Core VF rendering concepts, controller types, view state, and LEX compatibility that underpin this skillomnistudio/document-generation-omnistudio— Alternative when OmniStudio is licensed; supports LWC-based document templates and server-side Word/PDF outputapex/apex-queueable-patterns— Queueable implementation patterns for async PDF generation and ContentVersion attachment