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:
- Build an HTML string with the content you want in the PDF.
- Call
Files.writePdfFileRef()to render the HTML into a PDF file reference. - Persist the file reference with
Files.persistFileRef(). - 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
Filesglobal is available alongsideentity,ComindServer,Utils, andconsole. 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 callFiles.persistFileRef()before attaching them to a record. - For data-driven PDFs, you can read spreadsheet or CSV content with
Files.readExcelFileRef()andFiles.readCsvFileRef(), then format the data into HTML.