Generate PDFs and Word docs from any Salesforce record. Merge PDFs, add barcodes and QR codes, compute totals — 100% native, zero external dependencies, 100% free forever. All features, all users, no paid tiers. PowerPoint and Excel coming soon.
Join the Community Channel | Website | Roadmap
sf package install --package 04tal000006PhBBAA0 --wait 10 --target-org <your-org>Install in Production | Install in Sandbox
Then: Assign DocGen Admin permission set | Enable Blob.toPdf() Release Update | Open the DocGen app
- Create a template — pick Word, Excel, or PowerPoint. Choose your Salesforce object.
- Select your fields — use the visual query builder, or paste a full SOQL statement for complex nested relationships.
- Add tags and upload — type
{Name}where you want data. Upload the file. - Generate — from any record page, in bulk, or from a Flow.
Download example templates from portwoodglobalsolutions.com.
| Format | Template | Output Options | Best For |
|---|---|---|---|
| Word | .docx |
PDF or DOCX | Contracts, proposals, invoices, letters |
| Excel | .xlsx |
XLSX | Data exports, reports, financial summaries |
| PowerPoint | .pptx |
PPTX | Presentations, slide decks |
Word is the most capable — it's the only format that supports images, barcodes, QR codes, rich text, and PDF output.
| Tag | What It Does | Example |
|---|---|---|
{FieldName} |
Insert a field value | {Name}, {Email}, {Phone} |
{Parent.Field} |
Pull from a related record | {Account.Name}, {Owner.Email} |
{#ChildList}...{/ChildList} |
Repeat for each child record | {#Contacts}{FirstName}{/Contacts} |
{#BoolField}...{/BoolField} |
Show/hide based on checkbox | {#IsActive}Active member{/IsActive} |
{RichTextField} |
Rich text with formatting and images | {Description} renders bold, italic, lists |
| Tag | Output |
|---|---|
{CloseDate:MM/dd/yyyy} |
03/18/2026 |
{Amount:currency} |
$500,000.00 |
{Rate:percent} |
15.5% |
{Quantity:number} |
1,234 |
Place these outside the loop to compute totals from child records:
| Tag | Example |
|---|---|
{SUM:List.Field} |
{SUM:QuoteLineItems.TotalPrice} |
{COUNT:List} |
{COUNT:Contacts} |
{AVG:List.Field} |
{AVG:OpportunityLineItems.UnitPrice} |
{MIN:List.Field} / {MAX:List.Field} |
{MIN:QuoteLineItems.Quantity} |
Store a ContentVersion ID (starts with 068) in a text field, then use {%FieldName} in your template:
| Tag | What It Does |
|---|---|
{%Logo__c} |
Insert image at original size |
{%Logo__c:200x60} |
Fixed size: 200px wide, 60px tall |
{%Logo__c:100%x} |
Full page width, keep aspect ratio |
{%Logo__c:m100%xm50%} |
Shrink to fit within page width and 50% height |
Images work in both PDF and DOCX output. You can also embed images directly in your Word template — they render in PDFs automatically.
Rich text fields render with full formatting (bold, italic, lists, images) in PDF output. Images inside rich text fields work in PDFs. For DOCX output, use {%FieldName} image tags instead of rich text images.
PDF output only. No external services required.
| Tag | What You Get |
|---|---|
{*ProductCode} |
Code 128 barcode |
{*ProductCode:code128:300x80} |
Barcode at 300px wide, 80px tall |
{*Website:qr} |
QR code (150px default) |
{*TrackingUrl:qr:200} |
QR code at 200px square |
To repeat rows inside a table (not the whole table), put the loop tags in the data row:
| Name | Title | |
|---|---|---|
{#Contacts}{FirstName} {LastName} |
{Title} |
{Email}{/Contacts} |
The {#Contacts} goes in the first cell and {/Contacts} goes in the last cell of the same row. The header row stays fixed, and the data row repeats for each record.
- Title pages — If your Word template has "Different First Page" enabled, the PDF will suppress headers and footers on page 1. Your cover page stays clean.
- Section breaks — Section breaks in your Word template create proper page breaks in the PDF.
Put a page break inside a loop to give each child record its own page:
{#Opportunities}
Customer: {Account.Name}
Amount: {Amount:currency}
← page break here (Insert → Page Break in Word)
{/Opportunities}
Five ways to combine PDFs:
| Mode | What It Does |
|---|---|
| Generate & Merge | Generate a doc, then append existing PDFs from the record |
| Document Packets | Generate from multiple templates, merge into one PDF |
| Merge Only | Combine existing PDFs on the record with drag-and-drop ordering |
| Child Record PDFs | Pull PDFs from child records (e.g., all Opportunity PDFs under an Account) |
| Bulk Merge | After bulk generation, merge all generated PDFs into one download |
Records with 2,000 to 50,000+ child records are detected automatically. Same template, same button — the engine handles pagination and async processing behind the scenes.
The query builder accepts full SOQL statements with unlimited nesting depth. Paste a query like:
SELECT Name, Industry,
(SELECT FirstName, LastName, Account.Name FROM Contacts),
(SELECT Name, Amount,
(SELECT Quantity, Product2.Name FROM OpportunityLineItems)
FROM Opportunities WHERE StageName = 'Closed Won')
FROM AccountThe builder parses the query, displays the field tree with parent lookups highlighted, and generates copy-paste merge tags for your template. Outer SELECT / FROM clauses are stripped automatically — DocGen always runs against a specific record.
Tips:
- Test your query in Developer Console or tools like Salesforce Inspector before pasting.
- Use AI to help build complex queries — Agentforce, ChatGPT, Gemini, and Claude can all generate valid SOQL with nested relationships.
- Subqueries support WHERE, ORDER BY, and LIMIT clauses.
- Parent lookups (e.g.,
Account.Name,Product2.Family) work at every nesting level.
| Action | Inputs | Use In |
|---|---|---|
DocGenFlowAction |
templateId, recordId | Record-Triggered Flows, Screen Flows |
DocGenBulkFlowAction |
templateId, queryCondition | Scheduled Flows, Bulk Processing |
Generate documents for hundreds of records at once. Enter a filter condition, click Submit. Real-time progress tracking in the app.
| Feature | DOCX | |
|---|---|---|
| All merge tags and formatting | Yes | Yes |
| Bold, italic, underline, colors, font sizes | Yes | Yes |
| Tables with borders, shading, column widths | Yes | Yes |
| Template-embedded images | Yes | Yes |
Dynamic images from record fields ({%Field}) |
Yes | Yes |
| Rich text field formatting | Yes | Yes |
| Rich text images | Yes | No — use {%Field} image tags |
| Barcodes and QR codes | Yes | No |
| Page numbers in headers/footers | Yes | N/A (Word handles natively) |
| Cover page (no header on page 1) | Yes | N/A (Word handles natively) |
| Custom fonts (Calibri, branded, etc.) | No — falls back to Helvetica | Yes — preserves original fonts |
| Clickable hyperlinks | No — rendered as styled text | Yes |
Salesforce's PDF engine supports these fonts:
| Font | CSS Name | When It's Used |
|---|---|---|
| Helvetica | sans-serif |
Default for all text |
| Times | serif |
If explicitly set in template |
| Courier | monospace |
Fixed-width text |
| Arial Unicode MS | (automatic) | Chinese, Japanese, Korean, Thai, Arabic, Hebrew |
Custom fonts from your Word template (Calibri, Cambria, branded typefaces) fall back to Helvetica in PDF output. If custom fonts matter, generate as DOCX — Word preserves the original fonts.
Starting with Spring '26, the renderer supports expanded multibyte character rendering for international scripts.
These are Salesforce platform limitations, not DocGen bugs:
| Not Supported | Why | Workaround |
|---|---|---|
| Custom fonts | Blob.toPdf() only has 4 built-in fonts |
Generate as DOCX |
@font-face CSS |
Not supported by the PDF renderer | Generate as DOCX |
| Text boxes and shapes | Word drawing objects aren't converted to HTML | Use tables for layout |
| SmartArt and charts | Not rendered in the HTML conversion | Insert as images in your template |
| Clickable hyperlinks | PDF renderer outputs styled text, not links | Links work in DOCX |
| CSS Grid / Flexbox | The PDF renderer supports CSS 2.1 only | Use tables |
| JavaScript | Ignored by the renderer | N/A |
| Even/odd page headers | Not currently supported | Same header on all pages |
| Multiple section headers | One header/footer set per document | Use page breaks, not section-specific headers |
| E-signatures | Intentionally excluded | Use DocuSign, Adobe Sign after generation |
| Limit | Details | How DocGen Handles It |
|---|---|---|
| 6 MB heap (sync) | Single document generation | DOCX uses client-side assembly; PDF uses zero-heap image pipeline |
| 12 MB heap (async) | Bulk batch generation | Batch size 1 = fresh heap per record |
| ~3 MB PDF save | Saving PDF to a record | Download has no size limit |
| 4 MB Aura payload | Saving DOCX to a record | Download works for any size |
| 100 SOQL queries | Per transaction | Multi-level queries use 1 SOQL per relationship depth |
| 50,000+ child records | Giant datasets | Auto-detected, processed async with cursor pagination |
Template (.docx/.xlsx/.pptx)
↓
Decompress → Merge XML tags → Recompress
↓ ↓
DOCX/XLSX/PPTX PDF path:
(client-side ZIP) DocGenHtmlRenderer → Blob.toPdf()
| Class | Role |
|---|---|
DocGenService |
Core merge engine — tags, loops, images, aggregates, barcodes |
DocGenHtmlRenderer |
DOCX XML → HTML for PDF rendering |
DocGenDataRetriever |
Multi-level SOQL with query tree stitching |
BarcodeGenerator |
Code 128 + QR code generation (pure Apex) |
DocGenController |
LWC controller — template CRUD, generation endpoints |
DocGenBatch |
Batch Apex for bulk document generation |
docGenPdfMerger.js |
Client-side PDF merge engine (pure JS) |
docGenZipWriter.js |
Client-side DOCX/XLSX assembly (pure JS) |
DocGen ships on a biweekly release cycle. Next release: April 17, 2026.
See CHANGELOG.md for full version history.
DocGen is 100% free, open source, and community-driven. Published through Portwood Global Solutions.
| Channel | What It's For |
|---|---|
| Community Channel | Real-time help, feature requests, template sharing |
| GitHub Issues | Bug reports and tracked feature requests |
| Roadmap | What's shipped and what's coming next |
| Website | Install links, feature overview |
Need dedicated support? Contact us at [email protected].
We welcome contributions — see CONTRIBUTING.md for setup instructions.
We run the Salesforce Code Analyzer with Security + AppExchange rule selectors on every release. The current scan reports 51 High-severity SFGE ApexFlsViolation findings — all on package-internal custom objects. We believe in full transparency, so here they are:
Why these exist: DocGen is a managed package with the portwoodglobal namespace. Salesforce's USER_MODE keyword (which SFGE requires for compliance) breaks namespace resolution during managed package builds — field names like Query_Config__c fail with "No such column" because the SOQL engine requires the fully-qualified portwoodglobal__Query_Config__c. Since namespace-qualified field names don't compile in development scratch orgs, SYSTEM_MODE is the only viable option for package-internal objects. This is standard practice for managed packages on the Salesforce platform.
Salesforce's own AppExchange security review guidance explicitly states that bypassing CRUD/FLS is acceptable for "custom objects or fields like logs or system metadata that shouldn't be directly accessible to the user via CRUD/FLS." All DocGen custom objects fall into this category.
How access is actually enforced:
- Object-level CRUD: Enforced by
DocGen_AdminandDocGen_Userpermission sets (platform-level) - Field-level security: Enforced by the same permission sets (platform-level)
- Record-level sharing: All Apex classes use
with sharing(platform-enforced) - Standard objects (ContentVersion, ContentDocumentLink): Use
USER_MODE+Security.stripInaccessible()(code-enforced)
No user can read, create, update, or delete any DocGen data without an explicitly assigned permission set.
Full SFGE violation log (51 findings across 3 files)
DocGenBulkController.cls (9 findings):
| Line | Operation | Object | Fields |
|---|---|---|---|
| 9 | READ | DocGen_Template__c | Base_Object_API__c, Description__c, Name, Output_Format__c, Query_Config__c, Test_Record_Id__c |
| 336 | INSERT | DocGen_Job__c | Label__c, Merge_Only__c, Query_Condition__c, Status__c, Template__c |
| 336 | INSERT | DocGen_Job__c | (duplicate path) |
| 356 | READ | DocGen_Job__c | Error_Count__c, Name, Status__c, Success_Count__c, Total_Records__c |
| 366 | READ | DocGen_Job__c | CreatedDate, Error_Count__c, Label__c, Name, Status__c, Success_Count__c, Total_Records__c |
| 382 | READ | DocGen_Job__c | CreatedDate, Query_Condition__c, Template__c |
| 438 | READ | DocGen_Saved_Query__c | CreatedDate, Description__c, DocGen_Template__c, Name, Query_Condition__c |
| 461 | INSERT | DocGen_Saved_Query__c | Description__c, DocGen_Template__c, Name, Query_Condition__c |
DocGenController.cls (41 findings):
| Line | Operation | Object | Fields |
|---|---|---|---|
| 16 | READ | DocGen_Template__c | Base_Object_API__c, Document_Title_Format__c, Output_Format__c, Query_Config__c, Type__c |
| 448 | READ | DocGen_Job__c | Label__c, Status__c, Success_Count__c, Total_Records__c |
| 637 | READ | DocGen_Template__c | Base_Object_API__c, Query_Config__c |
| 787 | READ | DocGen_Template__c | Base_Object_API__c, Description__c, Is_Default__c, Name, Output_Format__c, Query_Config__c, Type__c |
| 800 | READ | DocGen_Template__c | Base_Object_API__c, Category__c, Description__c, Document_Title_Format__c, Is_Default__c, Name, Output_Format__c, Query_Config__c, Test_Record_Id__c, Type__c |
| 800 | READ | ContentDocumentLinks | ContentDocument.CreatedDate, ContentDocumentId |
| 839 | READ | DocGen_Template__c | Base_Object_API__c, Is_Default__c |
| 848 | UPDATE | DocGen_Template__c | Base_Object_API__c, Is_Default__c |
| 852 | UPDATE | DocGen_Template__c | Base_Object_API__c, Category__c, Description__c, Document_Title_Format__c, Is_Default__c, Name, Output_Format__c, Query_Config__c, Test_Record_Id__c, Type__c |
| 857 | READ | DocGen_Template_Version__c | Is_Active__c, Template__c |
| 864 | UPDATE | DocGen_Template_Version__c | Is_Active__c, Template__c |
| 893 | INSERT | DocGen_Template_Version__c | Base_Object_API__c, Category__c, Content_Version_Id__c, Description__c, Is_Active__c, Query_Config__c, Template__c, Type__c |
| 932 | READ | DocGen_Template_Version__c | Base_Object_API__c, Category__c, Content_Version_Id__c, CreatedBy.Name, CreatedDate, Description__c, Is_Active__c, Name, Query_Config__c, Template__c, Type__c |
| 947 | READ | DocGen_Template_Version__c | Base_Object_API__c, Category__c, Content_Version_Id__c, Description__c, Query_Config__c, Template__c, Type__c |
| 955 | READ | DocGen_Template_Version__c | Is_Active__c, Template__c |
| 962 | UPDATE | DocGen_Template_Version__c | Is_Active__c, Template__c |
| 967 | UPDATE | DocGen_Template_Version__c | Is_Active__c |
| 977 | UPDATE | DocGen_Template__c | Base_Object_API__c, Category__c, Description__c, Query_Config__c, Type__c |
| 3065 | INSERT | DocGen_Template__c | Base_Object_API__c, Category__c, Description__c, Name, Output_Format__c, Query_Config__c, Type__c |
| 3098 | INSERT | DocGen_Template_Version__c | Base_Object_API__c, Category__c, Content_Version_Id__c, Description__c, Is_Active__c, Query_Config__c, Template__c, Type__c |
| 3131 | READ | DocGen_Template__c | Base_Object_API__c, Category__c, Description__c, Document_Title_Format__c, Is_Default__c, Name, Output_Format__c, Query_Config__c, Type__c |
| 3131 | READ | Saved_Queries__r | Description__c, Name, Query_Condition__c |
| 3131 | READ | Versions__r | Base_Object_API__c, Category__c, Content_Version_Id__c, Description__c, Is_Active__c, Query_Config__c, Type__c |
| 3227 | INSERT | DocGen_Template__c | Base_Object_API__c, Category__c, Description__c, Document_Title_Format__c, Is_Default__c, Name, Output_Format__c, Query_Config__c, Type__c |
| 3254 | INSERT | DocGen_Template_Version__c | Base_Object_API__c, Category__c, Content_Version_Id__c, Description__c, Is_Active__c, Query_Config__c, Template__c, Type__c |
| 3277 | INSERT | DocGen_Saved_Query__c | Description__c, DocGen_Template__c, Name, Query_Condition__c |
DocGenTemplateManager.cls (1 finding):
| Line | Operation | Object | Fields |
|---|---|---|---|
| 14 | READ | DocGen_Template_Version__c | Content_Version_Id__c, Is_Active__c, Template__c |
All 51 findings are sfge:ApexFlsViolation on package-internal custom objects using SYSTEM_MODE. Standard object queries (ContentVersion, ContentDocumentLink) use USER_MODE with Security.stripInaccessible() and have zero violations.
Found a vulnerability? See SECURITY.md.
We maintain a beta channel for users who want early access to security hardening and new features before they're promoted to production. Beta packages can only be installed in sandboxes and scratch orgs.
sf package install --package 04tal000006PhG1AAK --wait 10 --target-org <your-sandbox>Current beta (v1.26.0) includes:
- Everything in v1.25.0 (stable)
- Schema-based allowlisting for all dynamic SOQL queries (SOQL injection hardening)
validateObjectName()andvalidateFieldList()enforce that all dynamic values in queries are validated againstSchema.getGlobalDescribe()before execution
| Version | Channel | Install Target | Package ID |
|---|---|---|---|
| v1.25.0 | Stable (Released) | Production + Sandbox | 04tal000006PhBBAA0 |
| v1.26.0 | Beta | Sandbox only | 04tal000006PhG1AAK |
Once the beta passes Checkmarx re-scan and additional hardening (CSRF, DML bulkification), it will be promoted to a stable release.
Apache License, Version 2.0. See LICENSE.
Built by Portwood Global Solutions