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_number | invoice_date | invoice_due_date | invoice_subtotal | invoice_tax_rate | invoice_tax_amount | invoice_total | customer_name | customer_email | customer_street | customer_city | customer_country |
|---|---|---|---|---|---|---|---|---|---|---|---|
| INV-2026-001 | 2026-01-15 | 2026-02-14 | 1200 | 0.1 | 120 | 1320 | John Doe | john.doe@example.com | 456 Customer Avenue | Beautiful City | UK |
| invoice_number | quantity | name | description | price | original_price |
|---|---|---|---|---|---|
| INV-2026-001 | 1 | Web Design Services | Custom website design and development for company homepage | 800 | |
| INV-2026-001 | 1 | Logo Design | Professional logo design with 3 revision rounds | 300 | |
| INV-2026-001 | 1 | SEO Optimization | Search engine optimization for 10 target keywords | 100 | 150 |
Recreate that structure as two Directus collections:
invoices— one row per invoice. Add the invoice header and customer fields shown above, plus apdf_urltext field that Step 6 writes the finished PDF link into.invoice_items— one row per line item, with aninvoice_numberfield that links each item back to its invoice.
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.
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:
sourceis a string of the form[template:YOUR_TEMPLATE_ID]— not an object. ReplaceYOUR_TEMPLATE_IDwith your template's short ID from the Dashboard (see Templates).- Every template field goes inside
templateData, and the line items go intemplateData.items— not in a separateeInvoicewrapper or a top-levelitemsarray. - 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:
Authorization:Bearer YOUR_API_KEY(from the PolyDoc Dashboard)Content-Type:application/jsonX-Sandbox:true- 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.
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.
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"
}