DocRaptor vs CustomJS: Pricing & JavaScript Support
DocRaptor alternative compared: PrinceXML vs Chromium rendering, JavaScript support, and real cost per PDF at 500, 5k, and 50k docs. 600 free PDFs/month.
Updated October 2026
Power Automate cloud flows have no built-in action that runs JavaScript. The shortest way to get one is the CustomJS connector: add the Execute JavaScript Code action, map your data into Input Value, write code that ends in return, and read the result in the next step with a Parse JSON action.
Microsoft's own options each carry a precondition. Office Scripts run TypeScript, but only against an Excel workbook, and fetch is disabled when a script runs from a flow. Azure Functions run anything, but need a subscription, a deployment, and an HTTP action to call them. This guide walks through the inline code step with real input and output, three examples plus a PDF invoice, a comparison of the three routes, and the cases where Power Automate's own expressions are the better tool.
concat, formatDateTime, Select and Filter array are enough. Skip the code step there.Microsoft facts in this article (connector class, license entitlements, Office Scripts limits, Azure Functions free grant, expression functions) were checked against Microsoft Learn and the Power Automate pricing page in October 2026. Licensing terms change; the linked pages are the source of truth.
Cloud flows run on the Workflow Definition Language, the expression engine shared with Azure Logic Apps. It offers around 100 functions (concat, split, formatDateTime, addDays, json) and a few Data Operation actions, but no loops inside an expression, no user-defined functions, and no script step. When you hit that wall, Microsoft points to three things:
fetch is not defined. Microsoft caps it at 1,600 runs per user per day and 3 calls per 10 seconds (platform limits).WScript.Echo. It is not available in a cloud flow and is not Node.js. The CustomJS connector is the fourth option: a certified connector whose action sends your code and input to CustomJS servers, runs it in Node.js with async/await, fetch, Buffer and a fixed set of npm packages, and returns whatever you return. Nothing to deploy or host.
That is the honest trade-off against Office Scripts: Excel Online (Business) is a standard connector, so Office Scripts work on a plain Microsoft 365 license. If your logic is about an Excel workbook and needs no external calls, that route costs nothing extra.
input) and JavaScript Code (required, must end with return).


input.| Field | What goes in | Notes |
|---|---|---|
| Connection | Name plus your CustomJS API key | Created once. Not shareable; each flow owner creates their own. |
| Input Value | A string or dynamic content, such as the body of a previous action | Objects and arrays arrive serialized, so start your code with the one-line parse shown below. |
| JavaScript Code | Node.js code ending in return | Top-level await works. Return a string, number, object or array. |
The pattern never changes: put dynamic content in Input Value, parse it on the first line, return an object, and let Parse JSON turn that object into dynamic content. A complete order-summary step, with Input Value set to the body of a trigger or Compose action:
{
"customer": "Northwind Traders",
"currency": "USD",
"taxRate": 0.19,
"lines": [
{ "sku": "NW-200", "description": "Standing desk", "qty": 3, "unitPrice": 449.0 },
{ "sku": "NW-310", "description": "Monitor arm", "qty": 2, "unitPrice": 89.5 },
{ "sku": "NW-512", "description": "Cable tray", "qty": 5, "unitPrice": 12.25 }
]
}JavaScript Code:
const order = typeof input === "string" ? JSON.parse(input) : input;
const lines = order.lines.map(line => ({
sku: line.sku,
description: line.description,
qty: line.qty,
lineTotal: Math.round(line.qty * line.unitPrice * 100) / 100
}));
const subtotal = lines.reduce((sum, line) => sum + line.lineTotal, 0);
const tax = Math.round(subtotal * order.taxRate * 100) / 100;
return {
customer: order.customer,
currency: order.currency,
lineCount: lines.length,
subtotal: Math.round(subtotal * 100) / 100,
tax,
total: Math.round((subtotal + tax) * 100) / 100,
lines
};Output of the action:
{
"customer": "Northwind Traders",
"currency": "USD",
"lineCount": 3,
"subtotal": 1587.25,
"tax": 301.58,
"total": 1888.83,
"lines": [
{ "sku": "NW-200", "description": "Standing desk", "qty": 3, "lineTotal": 1347 },
{ "sku": "NW-310", "description": "Monitor arm", "qty": 2, "lineTotal": 179 },
{ "sku": "NW-512", "description": "Cable tray", "qty": 5, "lineTotal": 61.25 }
]
} The first line matters. A Compose output or action body mapped into Input Value is handed over as a JSON string, so typeof input === "string" ? JSON.parse(input) : input covers both cases. The rounding to cents exists because 0.1 + 0.2 is not 0.3 in JavaScript. In our logs the median inline script finishes in 121 milliseconds, and 97.7 percent finish inside one second; the Power Automate action adds its own round trip on top, as any connector call does.
body('Execute_JavaScript_Code')).total, customer and the lines array are now dynamic content everywhere below, and lines drops straight into an Apply to each. For a single value you can skip the action: body('Execute_JavaScript_Code')?['total']. If Power Automate shows the result as a string rather than an object, wrap it in json(). Parse JSON accepts either form, which is why it is the safer default.
SharePoint's Get items returns a value array where choice and person columns are nested objects (Status/Value, Owner/Email). Writing that to Excel, CSV or an email table means flattening it first. Map the Get items body into Input Value:
{
"value": [
{
"ID": 12,
"Title": "Laptop refresh",
"Status": { "Value": "Approved" },
"Owner": { "DisplayName": "Mia Chen", "Email": "[email protected]" },
"Amount": 1299.5,
"Modified": "2026-10-02T08:14:00Z"
},
{
"ID": 13,
"Title": "Conference booth",
"Status": { "Value": "Pending" },
"Owner": { "DisplayName": "Tom Reyes", "Email": "[email protected]" },
"Amount": 4800,
"Modified": "2026-10-03T16:40:00Z"
}
]
}const data = typeof input === "string" ? JSON.parse(input) : input;
const items = Array.isArray(data) ? data : data.value;
return items.map(item => ({
id: item.ID,
title: item.Title,
status: item.Status?.Value ?? "",
owner: item.Owner?.DisplayName ?? "",
email: (item.Owner?.Email ?? "").toLowerCase(),
amount: Number(item.Amount) || 0,
modified: item.Modified.slice(0, 10)
}));[
{
"id": 12,
"title": "Laptop refresh",
"status": "Approved",
"owner": "Mia Chen",
"email": "[email protected]",
"amount": 1299.5,
"modified": "2026-10-02"
},
{
"id": 13,
"title": "Conference booth",
"status": "Pending",
"owner": "Tom Reyes",
"email": "[email protected]",
"amount": 4800,
"modified": "2026-10-03"
}
]The result is a flat array ready for Create CSV table, Create HTML table, or an Apply to each that adds Excel rows. A Select action can do the renaming alone; the code step earns its place when you also need fallbacks for empty person fields, lowercase normalization and date trimming in the same pass.
Power Automate has dateDifference and dayOfWeek, but no working-days function, and an expression cannot loop. The usual workaround is a Do until loop with a counter variable, one action per day counted. The code step does it in one action and skips a holiday list:
{
"start": "2026-10-07",
"end": "2026-10-30",
"holidays": ["2026-10-26"]
}const { start, end, holidays = [] } =
typeof input === "string" ? JSON.parse(input) : input;
const skip = new Set(holidays);
const day = new Date(start + "T00:00:00Z");
const last = new Date(end + "T00:00:00Z");
let businessDays = 0;
while (day <= last) {
const weekday = day.getUTCDay();
const iso = day.toISOString().slice(0, 10);
if (weekday !== 0 && weekday !== 6 && !skip.has(iso)) businessDays++;
day.setUTCDate(day.getUTCDate() + 1);
}
return { start, end, holidaysSkipped: holidays.length, businessDays };{
"start": "2026-10-07",
"end": "2026-10-30",
"holidaysSkipped": 1,
"businessDays": 17
} October 7 to October 30, 2026 contains 18 weekdays; the holiday on Monday the 26th brings it to 17. Everything runs in UTC on purpose so a date like 2026-10-07 cannot slip into the previous day because of the server's time zone.
Calling an external API usually means composing a JSON body that does not match your trigger data: prices in cents, snake_case keys, trimmed lowercase emails, a nested contact object. Nested concat and mul expressions in a Compose can do it and are hard to read a month later. The code step builds the body in one place:
{
"customer": "Northwind Traders",
"email": " [email protected] ",
"phone": "+1 (206) 555-0144",
"lines": [
{ "sku": "NW-200", "qty": 3, "unitPrice": 449.0 },
{ "sku": "NW-310", "qty": 2, "unitPrice": 89.5 }
]
}const order = typeof input === "string" ? JSON.parse(input) : input;
const items = order.lines.map(line => ({
sku: line.sku,
quantity: line.qty,
unit_price_cents: Math.round(line.unitPrice * 100)
}));
const totalCents = items.reduce(
(sum, item) => sum + item.quantity * item.unit_price_cents, 0
);
return {
contact: {
name: order.customer.trim(),
email: order.email.trim().toLowerCase(),
phone: order.phone.replace(/[^\d+]/g, "")
},
items,
total_cents: totalCents,
currency: "USD",
idempotency_key: "order-" + Date.now()
};{
"contact": {
"name": "Northwind Traders",
"email": "[email protected]",
"phone": "+12065550144"
},
"items": [
{ "sku": "NW-200", "quantity": 3, "unit_price_cents": 44900 },
{ "sku": "NW-310", "quantity": 2, "unit_price_cents": 8950 }
],
"total_cents": 152600,
"currency": "USD",
"idempotency_key": "order-1791352660842"
} In the HTTP action, set Body to body('Execute_JavaScript_Code') and add a Content-Type: application/json header. Since the code runs with fetch available, you could also make the call inside the script and drop the HTTP action; the HTTP action is premium as well, so your license situation is the same either way.
"Power Automate JavaScript" and "Power Automate PDF" are searched together for a reason: the usual job is turning flow data into a document. The connector's second action, HTML to PDF, has one field, HTML Content, and returns the PDF as binary file content. Chain the two: the code step renders the HTML, the PDF step converts it.
{
"invoiceNo": "INV-2026-0142",
"customer": "Northwind Traders",
"date": "2026-10-15",
"lines": [
{ "description": "Standing desk", "qty": 3, "unitPrice": 449.0 },
{ "description": "Monitor arm", "qty": 2, "unitPrice": 89.5 }
]
}const inv = typeof input === "string" ? JSON.parse(input) : input;
const money = n => n.toFixed(2) + " USD";
let total = 0;
const rows = inv.lines.map(line => {
const sum = line.qty * line.unitPrice;
total += sum;
return "<tr><td>" + line.description + "</td><td>" + line.qty +
"</td><td>" + money(line.unitPrice) + "</td><td>" + money(sum) + "</td></tr>";
}).join("");
return [
"<!DOCTYPE html><html><head><style>",
"body{font-family:Arial,sans-serif;padding:40px;color:#111}",
"table{width:100%;border-collapse:collapse;margin-top:24px}",
"th,td{padding:8px;border-bottom:1px solid #ddd;text-align:left}",
"</style></head><body>",
"<h1>Invoice " + inv.invoiceNo + "</h1>",
"<p>" + inv.customer + " | " + inv.date + "</p>",
"<table><tr><th>Item</th><th>Qty</th><th>Unit</th><th>Total</th></tr>",
rows, "</table>",
"<h2 style='text-align:right'>Total: " + money(total) + "</h2>",
"</body></html>"
].join("");Add HTML to PDF with HTML Content set to the code step's output, then pass the PDF output into Create file (OneDrive or SharePoint, File Content) or the Attachments Content of Send an email (V2). Each action is one CustomJS request, so an invoice costs two. Free-plan PDFs carry a watermark; paid plans remove it. The long version with QR codes, Compose-based field mapping and troubleshooting is in Power Automate PDF generation.
| Criteria | CustomJS connector | Azure Functions | Office Scripts |
|---|---|---|---|
| Setup | Account, API key, add action. About 5 minutes. | Subscription, Function App and storage account, deploy, secure endpoint, HTTP action or custom connector. | Workbook in OneDrive or SharePoint, script in the Excel editor, Run script action. |
| Cost model | Per request: Free 600 per month, $9 for 100 per day, $29 for 500 per day, $99 for 5,000 per day. | Consumption: 1 million executions and 400,000 GB-s free per month, then per execution and GB-s, plus storage. | Included in business Microsoft 365 licenses. |
| Power Automate license | Premium connector: Premium or Process license. | HTTP action and custom connectors are premium: same licenses. | Standard connector: works on seeded Microsoft 365 rights. |
| Languages | JavaScript (Node.js) with a fixed npm list: moment, pdf-lib, cheerio, crypto-js, jsonwebtoken, mathjs and more. | JavaScript, TypeScript, C#, Python, Java, PowerShell. Any package you deploy. | TypeScript on the ExcelScript API. No npm packages. |
| Outbound HTTP | Yes (fetch, axios). | Yes. | No. fetch is not defined when run from a flow. |
| Limits | 30 s per request on Free, 60 s on paid. 100 calls per connection per 60 seconds. | 5 minute default timeout, up to 10 on Consumption. Power Automate's 120 second limit on synchronous actions still applies. | 1,600 runs per user per day, 3 calls per 10 seconds, 5 MB per request, 120 second timeout. |
| Best for | Reshaping, calculations, API payloads, PDFs, inside the flow, no infrastructure. | Long jobs, code your team already maintains in Azure, private network access. | Logic whose input and output live in an Excel workbook. |
If you already run a Function App, one more HTTP-triggered function is cheap and keeps everything in your tenant. If the job is "transform this JSON between two actions", a deployment pipeline is more machinery than the problem deserves.
A code step is one more connector, one more quota, and one more thing a reviewer has to read. Do not add it when an expression or a Data Operation action already does the job:
concat(firstName, ' ', lastName), split(csvLine, ','), toLower, trim, replace.formatDateTime(utcNow(), 'yyyy-MM-dd'), addDays(triggerBody()?['due'], 14), convertTimeZone. Code is for calendar logic such as business days..filter().add, sub, mul, div, mod. A total across an array is where expressions stop.If you can write it in one expression you will still understand in three months, write the expression. If you are nesting five functions or stacking Apply to each loops with Increment variable steps, the code step is shorter, cheaper in actions, and easier to test.
return fetch(url).then(r => r.json()) hands the flow a pending Promise; use const res = await fetch(url); return await res.json();.The quickest way to try this: create the free account, paste the order-summary example into a manual-trigger flow, and look at the raw output in the run history before adding Parse JSON.
Continue reading on similar topics
DocRaptor alternative compared: PrinceXML vs Chromium rendering, JavaScript support, and real cost per PDF at 500, 5k, and 50k docs. 600 free PDFs/month.
How a file moves through Make.com as base64 and binary: decode it with Buffer.from, detect the real type from the header bytes, and return a file or a data URI. Code with input and output.
Make's Code app bills 2 credits per second, caps at 30 s and ships npm only on Enterprise. CustomJS bills 1 credit per call with npm and stored functions on every plan. Break-even at 1 to 15 s.