# Sending Emails with Generated PDF Attachments in OutSystems

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

## TL;DR

Generate the PDF as Binary Data using the Ultimate PDF Forge component's GeneratePDF action (see outsystems-pdf-generation), then build an EmailAttachment record with the Binary Data, FileName, and MimeType 'application/pdf', append it to an EmailAttachment List, and pass the list to Send_Email's Attachments parameter. Both PDF generation and Send_Email must be in the same Server Action.

## Emails with PDF Attachments in OutSystems

Two of the most-requested OutSystems features — PDF generation and email sending — combine naturally for invoice delivery, report distribution, and document workflows. This tutorial builds on both the pdf-generation and send-email tutorials, focusing on the integration pattern: generating PDFs on demand, attaching them to emails, and the batch variant using Timers for sending to many recipients.

## Before you start

- Ultimate PDF Forge component installed and referenced (see outsystems-pdf-generation tutorial)
- SMTP configured in Service Center or an ODC email connector set up (see outsystems-send-email tutorial)
- An Invoice entity with InvoiceNumber, CustomerId, Amount, and a corresponding InvoiceLine entity
- An InvoicePDFScreen created and accessible (Anonymous role for the headless renderer)

## Step-by-step guide

### 1. Verify dependencies before combining the patterns

Before writing the combined Server Action, confirm both subsystems work independently:

1. PDF generation: open GenerateInvoicePDF Server Action (from pdf-generation tutorial), run it in the Debugger for a known InvoiceId, verify PDFContent Binary Data is populated (not NullBinaryData()).

2. Email sending: open SendInvoiceEmail Server Action (from send-email tutorial), run it for a test customer, verify delivery in Service Center → Monitoring → Email.

Only proceed to the combined action once both work independently. Debugging a combined action where both subsystems are new makes root cause analysis much harder.

If GenerateInvoicePDF is not yet created, follow the pdf-generation tutorial first and return here.

**Expected result:** Both GenerateInvoicePDF and a basic email send work independently. You are ready to combine them.

### 2. Create the SendInvoiceWithPDF Server Action

Logic tab → Server Actions → right-click → Add Server Action. Name it 'SendInvoiceWithPDF'. Add input parameters:
- InvoiceId: Invoice Identifier
- CustomerEmail: Text
- CustomerName: Text

Action flow:
Start
  → GenerateInvoicePDF: InvoiceId = InvoiceId
      (output: PDFContent Binary Data)
  → Build EmailAttachment structure:
      Assign: Attachment.FileContent = GenerateInvoicePDF.PDFContent
              Attachment.FileName    = 'Invoice_' + IntegerToText(InvoiceId) + '.pdf'
              Attachment.MimeType    = 'application/pdf'
      ListAppend(AttachmentList, Attachment)
  → Send_Email:
      From: Site.SenderEmail
      To: CustomerEmail
      Subject: 'Your Invoice #' + IntegerToText(InvoiceNumber)
      Email: InvoiceEmail (screen)
        CustomerName: CustomerName
        InvoiceId: InvoiceId
      Attachments: AttachmentList
  → End

```
/* Local Variables needed */
Attachment:     EmailAttachment (Structure)
AttachmentList: EmailAttachment List

/* Build attachment */
Assign:
  Attachment.FileContent = GenerateInvoicePDF.PDFContent
  Attachment.FileName    = "Invoice_" + IntegerToText(InvoiceId) + ".pdf"
  Attachment.MimeType    = "application/pdf"

ListAppend(AttachmentList, Attachment)

/* Send_Email */
Send_Email:
  From:        Site.SenderEmail
  To:          CustomerEmail
  Subject:     "Your Invoice #" + IntegerToText(InvoiceNumber)
  Email:       InvoiceEmail
    CustomerName: CustomerName
    InvoiceId:    InvoiceId
  Attachments: AttachmentList
```

**Expected result:** SendInvoiceWithPDF generates the PDF, builds the attachment, and sends the email with the PDF attached. Verify in Service Center → Monitoring → Email.

### 3. Add error handling that distinguishes PDF failure from email failure

PDF generation and email delivery are independent failure modes. Handle them separately so one failure does not mask the other.

Wrap the GeneratePDF call in an exception handler that logs and re-raises a specific exception:
Exception Handler (AllExceptions) around GenerateInvoicePDF node:
  → LogError('InvoiceModule', 'SendInvoiceWithPDF', 'PDF generation failed: ' + ExceptionMessage)
  → Raise 'PDFGenerationFailed' User Exception with InvoiceId in message

Wrap Send_Email in a separate exception handler:
Exception Handler (AllExceptions) around Send_Email node:
  → LogError('InvoiceModule', 'SendInvoiceWithPDF', 'Email send failed: ' + ExceptionMessage)
  → Raise 'EmailSendFailed' User Exception with CustomerEmail in message

In the calling Client Action:
- Handle PDFGenerationFailed: show 'Could not generate PDF. Please try again.'
- Handle EmailSendFailed: show 'PDF created but email could not be sent. Download the PDF manually.'

This distinction matters because PDF failure means you should offer the download button, while email failure means the user can retry sending separately.

```
/* Exception handlers in SendInvoiceWithPDF Server Action */

/* Handler 1: wraps GenerateInvoicePDF call */
Exception Handler (AllExceptions on GeneratePDF node)
  --> LogError: "InvoiceModule", "PDF_Generate",
                "PDF failed for Invoice " + IntegerToText(InvoiceId) + ": " + ExceptionMessage
  --> Raise: PDFGenerationFailed
               Message = "Could not generate PDF for invoice #" + IntegerToText(InvoiceId)
  --> End

/* Handler 2: wraps Send_Email call */
Exception Handler (AllExceptions on Send_Email node)
  --> LogError: "InvoiceModule", "Email_Send",
                "Email failed to " + CustomerEmail + ": " + ExceptionMessage
  --> Raise: EmailSendFailed
               Message = "PDF was generated but email to " + CustomerEmail + " could not be sent."
  --> End
```

**Expected result:** PDF failures and email failures produce distinct error messages in the UI. Service Center logs identify exactly which step failed and for which invoice/customer.

### 4. Build a Timer-based batch sending pattern

For sending invoices to many customers (e.g., end-of-month billing run), use a Timer rather than looping in a Server Action triggered by a user click.

Create a Timer: Processes tab → Timers → Add Timer → 'MonthlyInvoiceSend'. Schedule: first of each month at 2 AM: '00 00 2 1 * *'. Link to Server Action: 'SendMonthlyInvoices'.

Server Action: SendMonthlyInvoices:
Start
  → Aggregate: GetUnsentInvoices
      Filter: Invoice.SentAt = NullDateTime() and Invoice.InvoiceDate <= CurrDate()
      Max Records: Site.InvoiceBatchSize
  → If: GetUnsentInvoices.List.Empty → [True] End
  → For Each Invoice in GetUnsentInvoices.List
       → SendInvoiceWithPDF:
             InvoiceId:     Invoice.Invoice.Id
             CustomerEmail: Invoice.Customer.Email
             CustomerName:  Invoice.Customer.Name
       → UpdateInvoice: Invoice.SentAt = CurrDateTime()
  → End

Per-invoice exception handler inside the For Each:
Exception Handler (AllExceptions)
  → LogError: 'SendMonthlyInvoices', ExceptionMessage + ' InvoiceId: ' + InvoiceId
  → Continue (do not let one failed invoice stop all others)

```
/* Timer schedule: first of month at 2 AM */
"00 00 2 1 * *"

/* Server Action: SendMonthlyInvoices */
Start
  --> GetUnsentInvoices:
        Filter: Invoice.SentAt = NullDateTime()
                and Invoice.InvoiceDate <= CurrDate()
        Max Records: Site.InvoiceBatchSize
  --> If: GetUnsentInvoices.List.Empty --> End
  --> For Each: Inv in GetUnsentInvoices.List
       --> SendInvoiceWithPDF:
               InvoiceId     = Inv.Invoice.Id
               CustomerEmail = Inv.Customer.Email
               CustomerName  = Inv.Customer.Name
       --> UpdateInvoice:
               Invoice.Id     = Inv.Invoice.Id
               Invoice.SentAt = CurrDateTime()
  --> End

  Exception Handler (AllExceptions) inside For Each
    --> LogError: "InvoiceModule", "BatchInvoice",
                  "Failed: InvoiceId=" + IntegerToText(Inv.Invoice.Id)
                  + " Error: " + ExceptionMessage
    --> Continue  /* Process next invoice */
```

**Expected result:** The monthly timer runs at 2 AM on the first of each month, generates PDFs and sends emails for all unsent invoices, and marks each one as sent. Failed invoices are logged and processed in the next run.

### 5. Handle the ODC 5.5 MB PDF limit in batch sending

In ODC, each call to GeneratePDF is subject to a 5.5 MB file size limit. In a batch of 100 invoices, any invoice whose PDF exceeds this limit throws an exception. The per-invoice AllExceptions handler catches it and continues — but the customer does not receive their invoice.

Solutions:
1. Monitor: check GeneratePDF.PDF binary size before attaching: If Length(GenerateInvoicePDF.PDFContent) > 5000000 then log a warning and send without attachment, notifying the customer to download the invoice from the app.
2. Optimize: reduce image sizes in the PDF template screen, remove large logos.
3. Split: for invoices with many line items, split into multiple smaller PDFs each under 5 MB.

Implementation: After the GeneratePDF call, add:
If: Length(GenerateInvoicePDF.PDFContent) > 5242880 (5 MB in bytes)
  [True] → Send_Email without attachments, body says 'Your invoice is available at [link]'
  [False] → proceed with attachment

```
/* ODC size check after GeneratePDF */
If: Length(GenerateInvoicePDF.PDFContent) > 5242880
  [True]  --> Send_Email:
                Subject: "Your Invoice #" + ... + " (See link below)"
                Email: InvoiceEmailNoAttachment
                  DownloadLink: InvoiceDownloadURL
              /* No Attachments parameter */
  [False] --> (proceed with full attachment as normal)
```

**Expected result:** In ODC, oversized PDFs trigger an alternative email with a download link instead of failing silently. The customer still receives their invoice.

## Complete code example

File: `EmailWithPDFAttachment_complete.txt`

```outsystems-pseudocode
/* ============================================================
   DEPENDENCIES:
   - Ultimate PDF Forge component (GeneratePDF action)
   - InvoicePDFScreen (Template screen, Anonymous role)
   - InvoiceEmail (Email screen with CustomerName, InvoiceId inputs)
   - SMTP configured in Service Center
   ============================================================ */

/* ============================================================
   SERVER ACTION: SendInvoiceWithPDF
   Input:  InvoiceId (Invoice Identifier),
           CustomerEmail (Text), CustomerName (Text)
   ============================================================ */
Start
  --> Assign:
        ScreenURL = GetOwnerURLPath() + "InvoicePDFScreen?InvoiceId="
                  + IntegerToText(InvoiceId)
  --> GeneratePDF (UltimatePDF):
        URL:             ScreenURL
        WaitForSelector: ".invoice-ready"
        Format:          A4
        Orientation:     Portrait

  Exception Handler (AllExceptions on GeneratePDF)
    --> LogError: "InvoiceModule", "PDF", ExceptionMessage
    --> Raise: PDFGenerationFailed
    --> End

  --> If: Length(GeneratePDF.PDF) > 5242880  /* ODC 5MB check */
       [True]  --> Send_Email (no attachment, with download link)
       [False] --> Assign: Attachment.FileContent = GeneratePDF.PDF
                           Attachment.FileName    = "Invoice_" + IntegerToText(InvoiceId) + ".pdf"
                           Attachment.MimeType    = "application/pdf"
               --> ListAppend(AttachmentList, Attachment)
               --> Send_Email:
                     From:        Site.SenderEmail
                     To:          CustomerEmail
                     Subject:     "Your Invoice #" + IntegerToText(InvoiceId)
                     Email:       InvoiceEmail
                       CustomerName: CustomerName
                       InvoiceId:    InvoiceId
                     Attachments: AttachmentList

  Exception Handler (AllExceptions on Send_Email)
    --> LogError: "InvoiceModule", "Email", ExceptionMessage
    --> Raise: EmailSendFailed
    --> End

  --> End

/* ============================================================
   TIMER: MonthlyInvoiceSend
   Schedule: '00 00 2 1 * *'
   Action:   SendMonthlyInvoices
   ============================================================ */

/* SERVER ACTION: SendMonthlyInvoices */
Start
  --> GetUnsentInvoices: (filter SentAt = NullDateTime())
  --> If: empty --> End
  --> For Each: Inv in list
       --> SendInvoiceWithPDF: InvoiceId, Email, Name
       --> UpdateInvoice: SentAt = CurrDateTime()

  Exception Handler (AllExceptions inside For Each)
    --> LogError with InvoiceId
    --> Continue
  --> End
```

## Common mistakes

- **Generating the PDF and sending the email in separate Server Actions called sequentially from a Client Action** — undefined Fix: The Binary Data from GeneratePDF must be passed to Send_Email in the same Server Action call. You cannot return Binary Data from one Server Action to a Client Action and then pass it to another Server Action — the data transfer size through the client is limited. Keep both calls in one Server Action.
- **Not handling the case where GeneratePDF.PDF is NullBinaryData() before passing to Send_Email** — undefined Fix: If GeneratePDF fails silently (returns empty rather than throwing), Send_Email will attach a zero-byte file. Add an If node: If GeneratePDF.PDF = NullBinaryData() → raise a PDFGenerationFailed exception rather than sending a broken attachment.
- **Updating Invoice.SentAt before calling SendInvoiceWithPDF — marking it sent even if the send fails** — undefined Fix: Update SentAt only after a successful SendInvoiceWithPDF call. If the send fails, the exception handler fires and the update never executes, leaving the invoice eligible for the next batch run.
- **Not resetting AttachmentList between batch iterations in the timer loop** — undefined Fix: If AttachmentList is a screen-level or action-level variable initialized outside the For Each loop, it accumulates attachments across iterations. Initialize (or clear) the AttachmentList inside the For Each loop body so each email gets only its own invoice attached.

## Best practices

- Test the complete end-to-end flow (generate PDF, attach, send) in Development with a real email address before deploying to QA or Production.
- Track email send status on the Invoice entity (SentAt, SentTo, AttachmentSizeBytes) for audit trails and resend capability.
- Use a per-invoice exception handler inside the timer batch loop so one failing invoice does not prevent all subsequent invoices from sending.
- Check PDF file size before attaching in ODC apps — implement the >5 MB fallback to a download link to prevent silent batch failures.
- For high-volume sending, consider using a transactional email API (SendGrid, Postmark) via REST instead of SMTP — they provide better deliverability, open tracking, and bounce handling.
- Store the generated PDF in the database (Invoice.PDFContent Binary Data) so users can download it again without re-generating, and for audit purposes.

## Frequently asked questions

### Can I attach an Excel export and a PDF in the same email?

Yes. Create two Local Variables of type EmailAttachment, populate each with the respective Binary Data (one from RecordListToExcel, one from GeneratePDF), call ListAppend for each, and pass the list to Send_Email's Attachments parameter. There is no limit on the number of attachments, only the total message size enforced by the SMTP server (typically 10-25 MB).

### How do I resend a failed invoice email without regenerating the PDF?

If you stored the generated PDF Binary Data in the Invoice entity (Invoice.PDFContent), you can retrieve it from the database and pass it directly to Send_Email without calling GeneratePDF again. This is faster for resends, provides a consistent attachment (same PDF as originally generated), and works even if the invoice data changes after the original send.

### Can the PDF attachment be password-protected?

The Ultimate PDF Forge component does not natively support password-protected PDFs. For password protection, use a Forge component that wraps iTextSharp or a similar PDF library. Alternatively, for ODC use an External Library that calls a PDF security API. Password-protect before attaching — the Binary Data you pass to EmailAttachment can come from any source.

### What happens if Send_Email succeeds but the email never reaches the recipient?

OutSystems' Send_Email queues the email for delivery via SMTP and reports success once the SMTP server accepts it. If the SMTP server later bounces or the recipient's server rejects it, OutSystems does not receive that feedback natively. For bounce handling, use a transactional email service (SendGrid, Postmark) via their API, which provides webhooks for bounces, opens, and clicks.

---

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