HTMLToPDFConverter is Traditional Web only — it produces blank PDFs in Reactive apps. For Reactive and ODC, use the Ultimate PDF Forge component (by Labs) which renders an OutSystems screen server-side into a PDF. Install it from the Forge, create a PDF screen, call GeneratePDF Server Action with the screen URL, and download the Binary Data result via the Download action.
PDF Generation in OutSystems Reactive Apps
PDF generation is the most discussed technical gap in OutSystems content — 8+ forum threads, no clean built-in solution for Reactive apps. The built-in HTMLToPDFConverter works only in Traditional Web. Reactive and ODC developers must use the Ultimate PDF Forge component, which runs a headless browser server-side to render an OutSystems screen as a PDF. This tutorial covers the complete production-ready pattern used in enterprise OutSystems apps.
Prerequisites
- An OutSystems 11 Reactive Web App or ODC app
- Service Studio with access to the Forge (or ODC Studio for ODC apps)
- An entity with data to include in the PDF (e.g., Invoice with InvoiceNumber, CustomerName, Amount, Lines)
- Understanding of Server Actions and the Download action
Step-by-step guide
Install the Ultimate PDF Forge component
Install the Ultimate PDF Forge component
The Ultimate PDF component (by OutSystems Labs) is available at forge.outsystems.com. It uses a headless Chromium browser running on the OutSystems server to render any URL as a PDF. In Service Studio: go to the Forge icon in the top toolbar → search 'Ultimate PDF' → click Install → follow installation wizard (installs to your environment). After installation, in your module: Module → Manage Dependencies (Ctrl+Q) → search 'UltimatePDF' → select it → check the GeneratePDF Server Action and any needed Structures → click Apply. In ODC, use the ODC Asset Library instead: ODC Portal → Assets → search 'Ultimate PDF' → install to your stage. Then reference it in ODC Studio via the same Manage Dependencies dialog.
Expected result: UltimatePDF appears in Logic tab → Referenced Actions. GeneratePDF action is available in your module.
Create a dedicated PDF template screen
Create a dedicated PDF template screen
Ultimate PDF renders a screen URL. Create a screen dedicated to PDF output — it should look like a printed page, not a web app. Interface tab → UI Flows → right-click → Add Screen. Name it 'InvoicePDFScreen'. Add an Input Parameter: InvoiceId (Invoice Identifier). Screen design guidelines for print: - Set container width to 800px (A4 landscape) or 600px (A4 portrait) - Remove all navigation elements (header, menu, footer) - Use static styles, not OutSystems UI themes (themes load external CSS which may not render in headless browser) - Ensure all images are inline (base64) or served from the same domain - Use print-safe fonts (Arial, Times New Roman) not Google Fonts (may fail in headless) Set the screen's Roles to Anonymous so the headless browser can access it without login.
Expected result: InvoicePDFScreen exists with an InvoiceId input parameter and displays invoice data from a Fetch data query. It has no navigation elements and is set to Anonymous access.
Create the GenerateInvoicePDF Server Action
Create the GenerateInvoicePDF Server Action
Logic tab → Server Actions → right-click → Add Server Action. Name it 'GenerateInvoicePDF'. Add: - Input: InvoiceId (Invoice Identifier) - Output: PDFContent (Binary Data) Action flow: Start → Assign: ScreenURL = GetOwnerURLPath() + 'InvoicePDFScreen?InvoiceId=' + IntegerToText(InvoiceId) → GeneratePDF (drag from Logic tab → Referenced Actions → UltimatePDF) URL: ScreenURL WaitForSelector: '.invoice-ready' (CSS selector that appears when data is loaded) (other defaults are fine for basic use) → Assign: PDFContent = GeneratePDF.PDF → End The WaitForSelector parameter tells the headless browser to wait until a specific CSS class appears in the DOM before rendering — this ensures the data has loaded before capture. Add class 'invoice-ready' to the outermost container of InvoicePDFScreen.
1/* Assign node for URL building */2ScreenURL = GetOwnerURLPath() + "InvoicePDFScreen?InvoiceId=" + IntegerToText(InvoiceId)34/* GeneratePDF parameters */5URL: ScreenURL6WaitForSelector: ".invoice-ready"7Orientation: Landscape /* or Portrait */8Format: A4910/* Output assignment */11PDFContent = GeneratePDF.PDFExpected result: GenerateInvoicePDF builds the URL, calls the headless browser renderer, and returns a Binary Data PDF.
Trigger download from a Client Action
Trigger download from a Client Action
Create a button on the Invoice detail screen. Right-click → Add OnClick Client Action 'ButtonDownloadPDFOnClick'. Action flow: Start → GenerateInvoicePDF (Server Action): InvoiceId = CurrentInvoice.Invoice.Id → Download: FileContent: GenerateInvoicePDF.PDFContent FileName: 'Invoice_' + IntegerToText(CurrentInvoice.Invoice.InvoiceNumber) + '.pdf' MimeType: 'application/pdf' → End Note: GeneratePDF typically takes 2-8 seconds for the headless browser to render. Add a loading indicator (ButtonLoading pattern or a Spinner widget) to communicate progress. Show the spinner before the Server Action call and hide it after (in a Client Action, use a Local Variable IsLoading = True before the call, set False after).
1/* ButtonDownloadPDFOnClick */2Start3 --> Assign: IsLoading = True4 --> GenerateInvoicePDF: InvoiceId = CurrentInvoice.Invoice.Id5 --> Assign: IsLoading = False6 --> Download:7 FileContent: GenerateInvoicePDF.PDFContent8 FileName: "Invoice_" + IntegerToText(CurrentInvoice.Invoice.InvoiceNumber) + ".pdf"9 MimeType: "application/pdf"10 --> End1112Exception Handler (AllExceptions)13 --> Assign: IsLoading = False14 --> Message: "PDF generation failed. Please try again." (Error)15 --> EndExpected result: Clicking the Download PDF button shows a loading state, generates the PDF on the server, and triggers the browser download dialog with the invoice PDF.
Handle ODC-specific constraints and authentication
Handle ODC-specific constraints and authentication
In ODC, two additional challenges apply: 1. 5.5 MB file size limit: Ultimate PDF in ODC enforces a maximum output file size of 5.5 MB. Reduce image resolution, compress images, and avoid embedding large assets directly in the PDF screen. 2. Authentication for PDF screen: ODC apps use secure authentication. The PDF template screen must be either (a) set to Anonymous role — acceptable for non-sensitive documents, or (b) passed an authentication token in the URL. For sensitive documents, use option (b): In GenerateInvoicePDF Server Action, generate a short-lived token: - Create a PDFAccessToken entity with Token (Text), ExpiresAt (DateTime), InvoiceId - Create a token: CreatePDFAccessToken record with Token = GenerateToken(), ExpiresAt = AddMinutes(CurrDateTime(), 5) - Pass token in URL: ScreenURL = ... + '&Token=' + TokenValue - In InvoicePDFScreen, validate the token before displaying data This pattern keeps sensitive invoices secure while allowing the headless browser to access them.
1/* ODC token-based URL pattern */2ScreenURL = GetOwnerURLPath()3 + "InvoicePDFScreen"4 + "?InvoiceId=" + IntegerToText(InvoiceId)5 + "&Token=" + AccessToken67/* InvoicePDFScreen OnInitialize */8Start9 --> ValidatePDFToken: Token = Token, InvoiceId = InvoiceId10 --> If: not ValidatePDFToken.IsValid11 [True] --> Exception: Raise SecurityException12 --> (load invoice data)13 --> EndExpected result: ODC app generates PDFs with short-lived tokens, preventing unauthorized access to sensitive invoice PDFs while allowing the headless renderer to access the screen.
Complete working example
1/* ============================================================2 SCREEN: InvoicePDFScreen (PDF template)3 Input: InvoiceId (Invoice Identifier)4 Role: Anonymous (or token-validated for sensitive data)5 CSS: .invoice-ready { display: block; } /* signal to headless browser */6 ============================================================ */78/* Screen Aggregate: GetInvoiceData */9Fetch: Only on demand10Source: Invoice join InvoiceLine (With or Without)11Filter: Invoice.Id = InvoiceId1213/* ============================================================14 SERVER ACTION: GenerateInvoicePDF15 Input: InvoiceId (Invoice Identifier)16 Output: PDFContent (Binary Data)17 ============================================================ */18Start19 --> Assign:20 BaseURL = GetOwnerURLPath()21 ScreenURL = BaseURL + "InvoicePDFScreen?InvoiceId=" + IntegerToText(InvoiceId)22 --> GeneratePDF (UltimatePDF):23 URL: ScreenURL24 WaitForSelector: ".invoice-ready"25 Orientation: Portrait26 Format: A427 MarginTop: "10mm"28 MarginBottom: "10mm"29 --> Assign: PDFContent = GeneratePDF.PDF30 --> End3132Exception Handler (AllExceptions)33 --> LogError: "InvoicePDF", "GenerateInvoicePDF", ExceptionMessage34 --> Raise UserException: "PDFGenerationFailed"35 Message = "Could not generate PDF. Please try again."36 --> End3738/* ============================================================39 CLIENT ACTION: ButtonDownloadPDFOnClick40 ============================================================ */41Start42 --> Assign: IsGenerating = True43 --> GenerateInvoicePDF: InvoiceId = CurrentInvoice.Invoice.Id44 --> Assign: IsGenerating = False45 --> Download:46 FileContent: GenerateInvoicePDF.PDFContent47 FileName: "Invoice_" + IntegerToText(InvoiceNumber) + ".pdf"48 MimeType: "application/pdf"49 --> End5051Exception Handler (User Exception: PDFGenerationFailed)52 --> Assign: IsGenerating = False53 --> Message: ExceptionMessage (Error)54 --> EndCommon mistakes
Why it's a problem: Using HTMLToPDFConverter from the Platform Actions in a Reactive Web app
How to avoid: HTMLToPDFConverter only works in Traditional Web Apps (O11). In Reactive apps, it generates blank PDFs. Use the Ultimate PDF Forge component which uses a headless browser to render Reactive screen URLs.
Why it's a problem: PDF template screen has navigation header and footer, cluttering the PDF output
How to avoid: Create a dedicated layout for PDF screens (Interface tab → Layouts → add a Print layout) that has no navigation chrome. Set the PDF UI Flow to use this layout. The PDF should look like a document, not a web app.
Why it's a problem: Not waiting for data to load — PDF captures the loading skeleton instead of actual data
How to avoid: Set WaitForSelector in GeneratePDF to a CSS class you add to the outermost content container. The headless browser waits until that element appears in the DOM before capturing the page, ensuring data has loaded.
Why it's a problem: Generating PDFs without a loading indicator, causing users to click multiple times
How to avoid: PDF generation takes 2-8 seconds. Use a Local Variable IsGenerating = True before the Server Action call to show a spinner and disable the button. Set IsGenerating = False after the call completes or in exception handlers.
Best practices
- Create a separate UI Flow for all PDF template screens with a print-optimized layout — this keeps PDF screens visually distinct from regular app screens.
- Set the PDF template screen to Anonymous role only for non-sensitive documents. For sensitive documents (invoices, HR data), implement token-based access.
- Add a WaitForSelector CSS class to the outermost content container of the PDF screen and use it in GeneratePDF to ensure all data has loaded before rendering.
- Show a loading state (IsGenerating variable controlling a Spinner) because PDF generation takes 2-8 seconds — users need visual feedback.
- Add an AllExceptions handler to the PDF Server Action with LogError and a re-raised User Exception — PDF generation failures should show a clean error, not a system crash.
- For ODC, monitor PDF file sizes and keep them under 5 MB. Compress images, use system fonts, and consider page breaks for very long documents.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to generate PDFs in an OutSystems 11 Reactive Web app. The built-in HTMLToPDFConverter doesn't work in Reactive. Explain the Ultimate PDF Forge component approach: how to install it, create a dedicated InvoicePDFScreen with Anonymous role and a '.invoice-ready' CSS selector, create a GenerateInvoicePDF Server Action that builds the URL and calls GeneratePDF, and download the PDF via the Download action with MimeType 'application/pdf'.
Set up PDF generation for invoices in my OutSystems Reactive app using Ultimate PDF. Create: (1) InvoicePDFScreen with InvoiceId input parameter, Anonymous role, invoice data aggregate, and '.invoice-ready' class on the main container. (2) GenerateInvoicePDF Server Action with InvoiceId input, PDFContent Binary Data output, that builds the screen URL with GetOwnerURLPath(), calls GeneratePDF with WaitForSelector='.invoice-ready', and has an AllExceptions handler with LogError. (3) ButtonDownloadPDFOnClick Client Action with IsGenerating loading state and Download action call.
Frequently asked questions
Why does HTMLToPDFConverter produce blank PDFs in Reactive Web apps?
HTMLToPDFConverter is a Traditional Web-only action that renders server-side HTML. In Reactive apps, the UI is rendered client-side (JavaScript). When HTMLToPDFConverter requests the screen content, it gets the empty pre-JavaScript HTML shell — no data, no UI, just blank. The Ultimate PDF component solves this by using a real headless browser (Chromium) that executes JavaScript before capturing the rendered result.
Can I generate a PDF and email it without the user downloading it?
Yes. In the GenerateInvoicePDF Server Action, after getting the PDFContent Binary Data, pass it directly to your email sending action as an attachment binary. Do not call the Download action — that is only needed for browser downloads. See the outsystems-email-pdf-attachment tutorial for the complete email attachment pattern.
What is the 5.5 MB limit in ODC and how do I work around it?
ODC limits outgoing HTTP responses and file generation to 5.5 MB as a platform guardrail. To stay under the limit: compress images (use max 150 DPI for charts/photos), avoid embedding large logo images (reference them by URL instead of base64), use system fonts instead of web fonts, and split very long reports into multiple pages or multiple files.
Can I add page numbers and headers/footers to the generated PDF?
Yes, via Ultimate PDF's HeaderHTML and FooterHTML parameters. These accept HTML strings rendered as fixed page headers and footers. Use the special CSS class 'pageNumber' and 'totalPages' which Chromium substitutes automatically. Example: HeaderHTML = '<div style="text-align:right">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>'
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation