# PDF Generation in OutSystems Reactive Apps: Complete Guide

- Tool: OutSystems
- Difficulty: Advanced
- Time required: 45-60 min
- Compatibility: OutSystems 11 and ODC
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

### 2. 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.

### 3. 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.

```
/* Assign node for URL building */
ScreenURL = GetOwnerURLPath() + "InvoicePDFScreen?InvoiceId=" + IntegerToText(InvoiceId)

/* GeneratePDF parameters */
URL:             ScreenURL
WaitForSelector: ".invoice-ready"
Orientation:     Landscape  /* or Portrait */
Format:          A4

/* Output assignment */
PDFContent = GeneratePDF.PDF
```

**Expected result:** GenerateInvoicePDF builds the URL, calls the headless browser renderer, and returns a Binary Data PDF.

### 4. 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).

```
/* ButtonDownloadPDFOnClick */
Start
  --> Assign: IsLoading = True
  --> GenerateInvoicePDF: InvoiceId = CurrentInvoice.Invoice.Id
  --> Assign: IsLoading = False
  --> Download:
        FileContent: GenerateInvoicePDF.PDFContent
        FileName:    "Invoice_" + IntegerToText(CurrentInvoice.Invoice.InvoiceNumber) + ".pdf"
        MimeType:    "application/pdf"
  --> End

Exception Handler (AllExceptions)
  --> Assign: IsLoading = False
  --> Message: "PDF generation failed. Please try again." (Error)
  --> End
```

**Expected 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.

### 5. 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.

```
/* ODC token-based URL pattern */
ScreenURL = GetOwnerURLPath()
          + "InvoicePDFScreen"
          + "?InvoiceId=" + IntegerToText(InvoiceId)
          + "&Token=" + AccessToken

/* InvoicePDFScreen OnInitialize */
Start
  --> ValidatePDFToken: Token = Token, InvoiceId = InvoiceId
  --> If: not ValidatePDFToken.IsValid
       [True] --> Exception: Raise SecurityException
  --> (load invoice data)
  --> End
```

**Expected 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 code example

File: `PDFGeneration_pattern.txt`

```outsystems-pseudocode
/* ============================================================
   SCREEN: InvoicePDFScreen (PDF template)
   Input:  InvoiceId (Invoice Identifier)
   Role:   Anonymous (or token-validated for sensitive data)
   CSS:    .invoice-ready { display: block; } /* signal to headless browser */
   ============================================================ */

/* Screen Aggregate: GetInvoiceData */
Fetch: Only on demand
Source: Invoice join InvoiceLine (With or Without)
Filter: Invoice.Id = InvoiceId

/* ============================================================
   SERVER ACTION: GenerateInvoicePDF
   Input:  InvoiceId (Invoice Identifier)
   Output: PDFContent (Binary Data)
   ============================================================ */
Start
  --> Assign:
        BaseURL   = GetOwnerURLPath()
        ScreenURL = BaseURL + "InvoicePDFScreen?InvoiceId=" + IntegerToText(InvoiceId)
  --> GeneratePDF (UltimatePDF):
        URL:             ScreenURL
        WaitForSelector: ".invoice-ready"
        Orientation:     Portrait
        Format:          A4
        MarginTop:       "10mm"
        MarginBottom:    "10mm"
  --> Assign: PDFContent = GeneratePDF.PDF
  --> End

Exception Handler (AllExceptions)
  --> LogError: "InvoicePDF", "GenerateInvoicePDF", ExceptionMessage
  --> Raise UserException: "PDFGenerationFailed"
        Message = "Could not generate PDF. Please try again."
  --> End

/* ============================================================
   CLIENT ACTION: ButtonDownloadPDFOnClick
   ============================================================ */
Start
  --> Assign: IsGenerating = True
  --> GenerateInvoicePDF: InvoiceId = CurrentInvoice.Invoice.Id
  --> Assign: IsGenerating = False
  --> Download:
        FileContent: GenerateInvoicePDF.PDFContent
        FileName:    "Invoice_" + IntegerToText(InvoiceNumber) + ".pdf"
        MimeType:    "application/pdf"
  --> End

Exception Handler (User Exception: PDFGenerationFailed)
  --> Assign: IsGenerating = False
  --> Message: ExceptionMessage (Error)
  --> End
```

## Common mistakes

- **Using HTMLToPDFConverter from the Platform Actions in a Reactive Web app** — undefined Fix: 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.
- **PDF template screen has navigation header and footer, cluttering the PDF output** — undefined Fix: 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.
- **Not waiting for data to load — PDF captures the loading skeleton instead of actual data** — undefined Fix: 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.
- **Generating PDFs without a loading indicator, causing users to click multiple times** — undefined Fix: 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.

## 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>'

---

Source: https://www.rapidevelopers.com/outsystems-tutorial/outsystems-pdf-generation
© RapidDev — https://www.rapidevelopers.com/outsystems-tutorial/outsystems-pdf-generation
