pdf-generation-patterns
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.
Best use case
pdf-generation-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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.
Teams using pdf-generation-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/pdf-generation-patterns/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How pdf-generation-patterns Compares
| Feature / Agent | pdf-generation-patterns | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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`, and `page-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 get` properties — never deferred.
- All conditional sections use `rendered="{!booleanProperty}"` where `booleanProperty` is a server-side Boolean on the controller. Do not use CSS `display:none` to hide sensitive content; hidden elements are still present in the HTML and visible if the raw HTML is inspected.
- Use `WITH USER_MODE` on all SOQL queries (available Summer '23+) to enforce FLS at the database level without manual `Schema` checks.
- 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 implementing `Database.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 `Blob` is stored as a `ContentVersion` (field `VersionData`) and linked to the source record via `ContentDocumentLink`.
### 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:
1. Author a Visualforce page as the PDF template with a server-side Apex controller.
2. Either navigate to the VF page URL from the LWC (using `NavigationMixin.Navigate` with `type: '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:**
1. Create a Visualforce page: `<apex:page controller="InvoicePdfController" renderAs="pdf" showHeader="false" sidebar="false">`.
2. In `InvoicePdfController`, query all required data in the constructor using `WITH USER_MODE`.
3. Build the logo absolute URL in the constructor: `logoUrl = URL.getSalesforceBaseUrl().toExternalForm() + '/resource/' + RESOURCE_ID + '/logo.png';` or embed as base64.
4. Use inline `<style>` with CSS 2.1 table layout for columns. No `<script>` tags.
5. 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:**
1. An Apex trigger or Flow invokes a Queueable class (`PdfAttachmentJob`) that implements `Database.AllowsCallouts`, passing the source record Id.
2. Inside `execute()`: build a `PageReference` to the VF page with `?id=<recordId>`, call `.getContentAsPDF()`, check for null, create a `ContentVersion` with `VersionData = blob`, then link it via `ContentDocumentLink` with `LinkedEntityId = recordId`.
3. 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:**
1. LWC calls an `@AuraEnabled` Apex method that enqueues `PdfAttachmentJob` (Pattern 2).
2. The LWC displays a spinner while the job completes. On completion, it refreshes the Files related list using `refreshApex` or LMS.
3. Alternatively, for a synchronous browser-download flow, the LWC uses `NavigationMixin.Navigate` to 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:
1. **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.
2. **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_MODE` SOQL. Build all computed values (logo URL, conditional flags, formatted dates) as server-side properties.
3. **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. Use `rendered="{!property}"` for conditional sections.
4. **Test the VF page iteratively** — Open the VF page URL in a browser with `?id=<recordId>`. Toggle `renderAs="pdf"` on and off to compare HTML vs. PDF output. Confirm logo visibility, column alignment, and page breaks.
5. **Implement programmatic generation if needed** — Build a Queueable implementing `Database.AllowsCallouts`. In `execute()`: call `getContentAsPDF()`, null-check the blob, insert a `ContentVersion` (with `PathOnClient`, `Title`, `VersionData`), query the resulting `ContentDocumentId`, then insert a `ContentDocumentLink` to the source record.
6. **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 `@AuraEnabled` method and handle spinner/refresh in the component.
7. **Validate security** — Run the full flow as a non-admin user with minimum read access. Confirm no FLS violations in debug logs. Confirm `ContentDocumentLink` `ShareType` is set appropriately (`'V'` for Viewer, `'I'` for Inferred).
---
## Review Checklist
Run through these before marking work in this area complete:
- [ ] `<apex:page>` has `renderAs="pdf" showHeader="false" sidebar="false"`
- [ ] No `<script>` tags in the VF page — all logic is server-side Apex
- [ ] SOQL uses `WITH USER_MODE` or 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 CSS `display:none`
- [ ] If programmatic: `getContentAsPDF()` is called from Queueable or Future method with `Database.AllowsCallouts`
- [ ] `getContentAsPDF()` return value is checked for null before inserting ContentVersion
- [ ] ContentDocumentLink is inserted with correct `LinkedEntityId` and `ShareType`
- [ ] 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:
1. **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.
2. **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.
3. **`{!$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 using `URL.getSalesforceBaseUrl()` or embed as base64.
4. **`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.
5. **Callout-after-DML restriction prevents use in triggers** — If any DML occurred before `getContentAsPDF()` in the same transaction, a `CalloutException` is 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 PDFs
- `apex/visualforce-fundamentals` — Core VF rendering concepts, controller types, view state, and LEX compatibility that underpin this skill
- `omnistudio/document-generation-omnistudio` — Alternative when OmniStudio is licensed; supports LWC-based document templates and server-side Word/PDF output
- `apex/apex-queueable-patterns` — Queueable implementation patterns for async PDF generation and ContentVersion attachmentRelated Skills
mfa-enforcement-patterns
Design MFA enforcement: auto-enablement, Salesforce Authenticator rollout, exceptions, service accounts, API-only users, SSO interop, and audit. Trigger keywords: MFA, multi-factor, two-factor, Salesforce Authenticator, MFA exception, MFA SSO, api-only MFA. Does NOT cover: end-user password policies, device-trust posture, or non-Salesforce IdP configuration.
encrypted-field-query-patterns
Design SOQL, filters, reporting, and indexes against Shield Platform Encryption fields. Trigger keywords: Shield Platform Encryption, encrypted field query, probabilistic vs deterministic encryption, encrypted SOQL filter, encrypted field index. Does NOT cover: Classic Encryption (deprecated), field-level security policy, or tenant secret key rotation.
apex-managed-sharing-patterns
Grant row-level access programmatically via __Share records when declarative sharing rules cannot express the policy. NOT for OWD, role hierarchy, or criteria-based sharing rule design.
omnistudio-testing-patterns
Use when testing or validating OmniStudio components — OmniScript preview, Integration Procedure step debugging, DataRaptor field-mapping validation, and end-to-end UTAM-based automation. NOT for Apex unit testing or standard Flow debugging.
omnistudio-error-handling-patterns
Use when designing fault behavior across Integration Procedures, DataRaptors, OmniScripts, and FlexCards — error routing, user-facing messaging, retry semantics, and idempotency. Triggers: 'omnistudio error', 'integration procedure fault', 'dataraptor error handling', 'omniscript retry', 'flexcard action failure'. NOT for general Apex exception design or Flow fault paths.
omnistudio-ci-cd-patterns
Use when designing or implementing CI/CD pipelines for OmniStudio components — DataPack export/import, versioning, environment promotion, and automated deployment. NOT for standard Salesforce metadata CI/CD or Apex-only pipelines.
omniscript-design-patterns
Use when designing or reviewing OmniScripts for guided experiences, step structure, branching, save/resume, and the boundary between OmniScript, Integration Procedures, DataRaptors, and custom LWCs. Triggers: 'omniscript design', 'too many steps in omniscript', 'save and resume omniscript', 'branching in omniscript', 'when should this be an integration procedure'. NOT for deep Integration Procedure or DataRaptor design when the guided interaction layer is not the main concern.
integration-procedure-cacheable-patterns
Use when designing Integration Procedures (IPs) with platform cache to cut latency and callout load. Covers cache key design, TTL selection, per-user vs org-wide partitions, invalidation on data changes, and safe fallback on cache miss/stale. Does NOT cover general IP authoring (see omnistudio-error-handling-patterns) or LWC client-side caching.
flexcard-design-patterns
Use when designing, building, or reviewing OmniStudio FlexCards — including data source selection, card states, actions, conditional visibility, flyout configuration, and child card iteration. Triggers: 'FlexCard', 'card template', 'flyout', 'card action', 'card state', 'data source', 'child card', 'conditional visibility'. NOT for OmniScript design, standalone LWC development, or Apex controller architecture outside the FlexCard context.
document-generation-omnistudio
Generating documents (PDF, DOCX, PPTX) from OmniStudio using Document Templates, OmniDataTransform token mapping, and OmniScript or Integration Procedure orchestration. Use when building client-side interactive or server-side batch document generation flows. NOT for Salesforce CPQ document generation. NOT for standard Salesforce mail merge or Lightning email templates. NOT for Contract Lifecycle Management (CLM) native document generation.
dataraptor-patterns
Use when designing or reviewing OmniStudio DataRaptors, especially Extract versus Turbo Extract versus Transform versus Load, field mapping strategy, performance tradeoffs, and when to move work into Integration Procedures or Apex. Triggers: 'DataRaptor Extract', 'Turbo Extract', 'DataRaptor Load', 'DataRaptor Transform', 'OmniStudio data mapping'. NOT for overall OmniScript journey design or Integration Procedure sequencing when the main question is not the DataRaptor shape itself.
wire-service-patterns
Use when designing or reviewing Lightning Web Components that use `@wire`, Lightning Data Service, UI API, or the GraphQL wire adapter, especially for reactive parameters, cache behavior, and refresh strategy. Triggers: 'wire service', 'refreshApex', 'reactive parameter', 'getRecord', 'wire vs imperative Apex'. NOT for component communication or generic lifecycle issues when data provisioning is not the main concern.