Skip to main content

PDF generation

Actions can generate PDF files on the server and attach them to records. This is useful for invoices, reports, certificates, and document exports.

PDF rendering runs on Headless Chrome (Puppeteer) inside the platform's Node.js service layer. Your action code builds an HTML string, passes it to the Files API, and persists the result - all within a single server-side execution.

Overview

The typical workflow has four steps:

  1. Build an HTML string with the content you want in the PDF.
  2. Call Files.writePdfFileRef() to render the HTML into a PDF file reference.
  3. Persist the file reference with Files.persistFileRef().
  4. Attach the persisted file to the record.

Complete example

The action below generates a simple invoice PDF and attaches it to the current record.

import { entity } from "#typings";
import { Files } from "@comind/api/server";

function generate_invoice_pdf() {
// Build the HTML content
const html = `
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 40px; }
h1 { color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
th { background-color: #f5f5f5; }
.total { font-weight: bold; font-size: 1.2em; margin-top: 20px; }
</style>
</head>
<body>
<h1>Invoice #${entity.number}</h1>
<p>Date: ${new Date().toLocaleDateString()}</p>
<p>Customer: ${entity.c_customer_name || "N/A"}</p>
<table>
<tr><th>Item</th><th>Amount</th></tr>
<tr><td>${entity.c_item_description || "Service"}</td>
<td>$${entity.c_total_cost || 0}</td></tr>
</table>
<p class="total">Total: $${entity.c_total_cost || 0}</p>
</body>
</html>
`;

// Generate the PDF from HTML
const pdfRef = Files.writePdfFileRef({ html });

// Save the temporary file permanently
const storedFile = Files.persistFileRef(pdfRef);

// Attach the PDF to the record
entity.attachments__list ??= [];
entity.attachments__list.push({
file_uid: storedFile.uploadedFileId,
title: `Invoice-${entity.number}.pdf`,
});
}

export default {
generate_invoice_pdf,
} satisfies ActionLogic<typeof entity>;

Using Liquid templates

For complex documents, use LiquidParser.parse() to render a Liquid template before passing the result to the PDF generator.

const template = `
<h1>Report for {{ customer_name }}</h1>
<p>Generated on {{ date }}</p>
{% for item in items %}
<p>{{ item.name }}: ${{ item.amount }}</p>
{% endfor %}
`;

const htmlContent = LiquidParser.parse(template, {
customer_name: entity.c_customer_name,
date: new Date().toLocaleDateString(),
items: JSON.parse(entity.c_line_items || "[]"),
});

const pdfRef = Files.writePdfFileRef({ html: htmlContent });

Merging multiple PDFs

You can combine several PDF file references into a single document with Files.mergePdfFileRefs().

const coverRef = Files.writePdfFileRef({ html: coverPageHtml });
const bodyRef = Files.writePdfFileRef({ html: bodyHtml });

const mergedRef = Files.mergePdfFileRefs([coverRef, bodyRef]);
const storedFile = Files.persistFileRef(mergedRef);

Creating ZIP archives

Use Files.zipFileRefs() to bundle multiple files - PDFs or otherwise - into a single ZIP download.

const pdfRef = Files.writePdfFileRef({ html });
const zipRef = Files.zipFileRefs([pdfRef]);
const storedFile = Files.persistFileRef(zipRef);

Key points

  • PDF generation is server-side only. It runs inside action logic, not in the browser.
  • The Files global is available alongside entity, ComindServer, Utils, and console. See Server-side API for the full list.
  • HTML styling is fully supported. Use inline CSS or <style> blocks for layout control.
  • File references returned by writePdfFileRef() are temporary. Always call Files.persistFileRef() before attaching them to a record.
  • For data-driven PDFs, you can read spreadsheet or CSV content with Files.readExcelFileRef() and Files.readCsvFileRef(), then format the data into HTML.