Skip to main content

Directus

Prerequisites

  • A PolyDoc account and API key - sign up for free, no credit card required. The free plan includes 150 PDF conversions per month.
  • An object storage bucket you control (AWS S3, Google Cloud Storage, Azure Blob, Backblaze B2, Wasabi, …) and a presigned upload URL for it. A Directus Flow cannot forward a binary PDF between operations, so PolyDoc PUTs the finished PDF to your presigned URL and returns its location. See Cloud Storage for how to mint a presigned URL.
  • A Directus instance: self-hosted (free) or Directus Cloud Professional plan ($99/month) - Flows are not available on the Directus Cloud Starter or Plus plans.

Use Case 1: Auto-generate PDF when a record is created

Automatically generate an invoice PDF whenever a new row is created in the invoices collection, then save the download URL back to the record.
Event Hook (items.create) → Read Data (invoice_items) → Run Script (build payload) → Webhook / Request URL (PolyDoc) → Update Data (save URL)

Set up your data collections

Use invoices (one row per invoice) and invoice_items (line rows linked by invoice_number). Examples below - two tabs in Sheets; two tables or linked records elsewhere.

invoice_numberinvoice_dateinvoice_due_dateinvoice_subtotalinvoice_tax_rateinvoice_tax_amountinvoice_totalcustomer_namecustomer_emailcustomer_streetcustomer_citycustomer_country
INV-2026-0012026-01-152026-02-1412000.11201320John Doejohn.doe@example.com456 Customer AvenueBeautiful CityUK
invoice_numberquantitynamedescriptionpriceoriginal_price
INV-2026-0011Web Design ServicesCustom website design and development for company homepage800
INV-2026-0011Logo DesignProfessional logo design with 3 revision rounds300
INV-2026-0011SEO OptimizationSearch engine optimization for 10 target keywords100150

Recreate that structure as two Directus collections:

  • invoices — one row per invoice. Add the invoice header and customer fields shown above, plus a pdf_url text field that Step 6 writes the finished PDF link into.
  • invoice_items — one row per line item, with an invoice_number field that links each item back to its invoice.
Directus

Create the Flow and configure the trigger

In Directus, open Settings → Flows and click Create Flow. Give it a name (e.g. Generate Invoice PDF).

For the Trigger, select Event Hook and configure:

  • Type: Action (Non-Blocking) — the record already exists when the Flow runs, which is what we want.
  • Scope: items.create
  • Collections: invoices

Save the trigger. The Flow canvas opens; you will add the operations below one after another, ending up with the chain shown here.

Directus

Add a Read Data operation

Click the + after the trigger to add an operation. Choose Read Data and configure it to fetch the line items that belong to the invoice from Step 2:

  • Key: readItems (this name is how later steps reference the result)
  • Collection: invoice_items
  • Permissions: Full Access (or a role with read access to invoice_items)
  • Query → Filter:
{
"invoice_number": {
"_eq": "{{ $trigger.payload.invoice_number }}"
}
}

This returns every invoice_items row whose invoice_number matches the invoice that triggered the Flow.

Add a Run Script operation

Add a Run Script operation after Read Data (Key: buildPayload). It merges the invoice header from the trigger with the line items from Step 3 and returns the exact JSON body the PolyDoc API expects:

module.exports = async function (data) {
const header = data.$trigger.payload;
const rows = Array.isArray(data.readItems) ? data.readItems : [];

const items = rows.map((row) => {
const item = {
quantity: Number(row.quantity),
name: String(row.name),
description: String(row.description),
price: Number(row.price),
};
if (row.original_price != null && row.original_price !== "") {
item.original_price = Number(row.original_price);
}
return item;
});

return {
source: "[template:YOUR_TEMPLATE_ID]",
templateData: {
invoice_number: header.invoice_number,
invoice_date: header.invoice_date,
invoice_due_date: header.invoice_due_date,
invoice_subtotal: Number(header.invoice_subtotal),
invoice_tax_rate: Number(header.invoice_tax_rate),
invoice_tax_amount: Number(header.invoice_tax_amount),
invoice_total: Number(header.invoice_total),
customer_name: header.customer_name,
customer_email: header.customer_email,
customer_street: header.customer_street,
customer_city: header.customer_city,
customer_country: header.customer_country,
items: items,
},
cloudStorage: {
presignedUrl: "YOUR_PRESIGNED_UPLOAD_URL",
},
};
};

A few things this script gets right on purpose:

  • source is a string of the form [template:YOUR_TEMPLATE_ID] — not an object. Replace YOUR_TEMPLATE_ID with your template's short ID from the Dashboard (see Templates).
  • Every template field goes inside templateData, and the line items go in templateData.items — not in a separate eInvoice wrapper or a top-level items array.
  • Numeric fields are wrapped in Number(...) so the renderer treats them as numbers, not quoted strings.

Add a Webhook / Request URL operation

Add a Webhook / Request URL operation after Run Script (Key: requestPolyDoc) to send the payload built in Step 4 to PolyDoc:

  • Method: POST
  • URL: https://api.polydoc.tech/pdf/convert
  • Headers:
  • Request Body: {{ buildPayload }}

Directus serialises the object returned by Run Script to JSON automatically. PolyDoc renders the PDF, PUTs it to your presigned URL, and responds with a small JSON body: { "success": true, "data": { "url": "…" } }.

Add an Update Data operation

Add an Update Data operation after Webhook / Request URL to write the PDF link back to the invoice record:

  • Collection: invoices
  • IDs: {{ $trigger.key }} (the primary key of the record that triggered the Flow)
  • Payload:
{
"pdf_url": "{{ requestPolyDoc.data.data.url }}"
}

Enable and test the Flow

Toggle the Flow active (top-right) and save. To test, create a new row in your invoices collection with a few matching invoice_items. Open the Flow's Logs tab and click the run to inspect each operation's output — every step should show a green checkmark, and pdf_url on the invoice record should now hold the link.

Directus

Use Case 2: Webhook-triggered PDF with synchronous URL response

Expose a webhook endpoint in Directus that any external service can call with invoice data and a presigned upload URL. The Flow calls PolyDoc, which uploads the PDF to the caller's bucket, and returns the download URL synchronously to the caller.
Webhook trigger → Run Script (build payload) → Webhook / Request URL (PolyDoc) → Run Script (return URL)

Create the Flow with a Webhook trigger

Create a new Flow. For the Trigger, choose Webhook, set the Method to POST, and enable Respond to Request. Directus generates a unique webhook URL — copy it. The JSON sent to that URL is available as {{ $trigger.body }} in the operations that follow.

Directus

Add a Run Script operation

Add a Run Script operation (Key: buildPayload) that assembles the PolyDoc request from the incoming body — including the presignedUrl the caller provides:

module.exports = async function (data) {
const body = data.$trigger.body;
const items = (body.items || []).map((row) => ({
quantity: Number(row.quantity),
name: String(row.name),
description: String(row.description),
price: Number(row.price),
...(row.original_price != null && row.original_price !== ""
? { original_price: Number(row.original_price) }
: {}),
}));

return {
source: "[template:YOUR_TEMPLATE_ID]",
templateData: {
invoice_number: body.invoice_number,
invoice_date: body.invoice_date,
invoice_due_date: body.invoice_due_date,
invoice_subtotal: Number(body.invoice_subtotal),
invoice_tax_rate: Number(body.invoice_tax_rate),
invoice_tax_amount: Number(body.invoice_tax_amount),
invoice_total: Number(body.invoice_total),
customer_name: body.customer_name,
customer_email: body.customer_email,
customer_street: body.customer_street,
customer_city: body.customer_city,
customer_country: body.customer_country,
items: items,
},
cloudStorage: {
presignedUrl: body.presignedUrl,
},
};
};

Add a Webhook / Request URL operation

Add a Webhook / Request URL operation (Key: requestPolyDoc) with the same URL, headers and {{ buildPayload }} body as Use Case 1, Step 5. PolyDoc renders the PDF, uploads it to the caller's presigned URL, and returns { "success": true, "data": { "url": "…" } }.

Return the URL to the caller

Add one more Run Script operation (Key: returnUrl) to hand the caller a clean response instead of the raw Request URL envelope:

module.exports = async function (data) {
return { url: data.requestPolyDoc.data.data.url };
};

Set the Webhook trigger's Response Body to this last operation. Because Respond to Request is enabled, the caller receives exactly:

{
"url": "https://your-bucket.s3.eu-central-1.amazonaws.com/invoices/INV-2026-001.pdf"
}