Why I picked Deno to run arbitrary code inside QuickFlo workflows
Every workflow platform has a code step. And on every platform I have used, it is the same thing: a sandbox with fetch, the standard library, and nothing else.
Zapier’s Code by Zapier step gives you JavaScript with fetch or Python with requests and BeautifulSoup. That is it. Try to import moment and you get "moment is not defined." Try to use pandas for a data transformation and it is not there. The documented workaround on Zapier’s own community forums is to call an AWS Lambda via webhook — which means you are now maintaining a separate service, a separate deployment, and correlating logs across two systems just to use a date library. Zapier did launch a newer product called Functions that allows package imports, but it is a separate feature from the standard Code step that most users reach for.
n8n Cloud is even more locked down. After their 2.0 upgrade, even Python standard library imports like re and json are blocked in cloud-hosted code nodes. Users hit "Import of standard library module 're' is disallowed" errors. Self-hosted n8n lets you configure allowed packages via environment variables, but cloud users are stuck.
Make’s Code App provides a sandboxed environment with access to standard libraries like datetime, json, and math, but third-party packages from npm or PyPI are not available. Power Automate does not have a native code step in cloud flows at all — you get expressions, or you call out to Azure Functions as a workaround.
The reason every platform does this is the same: running arbitrary user code on shared infrastructure is a security problem. If a user can import fs and read the filesystem, or import child_process and spawn a shell, you have a serious vulnerability. So platforms lock it down. Allowlist a few safe libraries, block everything else, call it a day.
I did not want that ceiling.
The Deno bet
When someone writes TypeScript inside a QuickFlo workflow step, that code runs on my infrastructure. I cannot hand-wave security. But I also did not want to restrict users to a curated list of five approved packages.
Deno solved this. Its permission model is built into the runtime — not bolted on as a wrapper, not a fragile sandbox library. By default, a Deno process cannot access the network, the filesystem, environment variables, or subprocess execution. Each permission has to be explicitly granted with a flag. This means I can let users import whatever they want, because even if a package tries to do something dangerous, the runtime blocks it at the syscall level.
The other runtimes did not offer this:
Node.js has no permission model. The process can do anything. You can sandbox it with vm2 or isolated-vm, but vm2 was deprecated in 2023 after a critical sandbox escape vulnerability. Maintaining a custom sandbox is exactly the kind of thing that bites you eighteen months later. That is why every Node-based platform falls back to allowlists — it is the only safe option without a real sandbox.
WASM offered true isolation but the DX is rough. You cannot just write TypeScript and have it run. You need a compile step, the debugging story is bad, and the ecosystem for common tasks is not there yet.
Deno gave me security guarantees at the runtime level, native TypeScript execution with no build step, and fast cold starts. But the real payoff is what it unlocks for users.
Import anything
This is the point of the Code step. Not “an escape hatch for complex logic” — you can already express complex logic with visual steps, it just takes more nodes. The point is that you have access to the entire npm ecosystem inside your workflow.
import { NotionToMarkdown } from "npm:notion-to-md";
import { Client } from "npm:@notionhq/client";
const notion = new Client({ auth: inputs.notionToken });
const n2m = new NotionToMarkdown({ notionClient: notion });
const blocks = await n2m.pageToMarkdown(inputs.pageId);
const markdown = n2m.toMarkdownString(blocks).parent;
return { markdown };
That is a workflow step that converts a Notion page to Markdown. On Zapier, that is a Lambda function, a deployment pipeline, and a webhook call. On QuickFlo, it is one step with six lines of code that shows up in the execution trace like any other step.
Need to parse a PDF? import { PDFDocument } from "npm:pdf-lib". Need to scrape HTML? import * as cheerio from "npm:cheerio". Need fuzzy string matching for deduplication? import Fuse from "npm:fuse.js". Whatever library solves your problem, you can use it. You are writing real code in a real runtime, not a constrained expression editor with a handful of built-in functions.
Why other platforms do not do this
It is not that Zapier and Make do not want to let you import packages. It is that they cannot safely do it on Node.js without either a fragile sandbox or a per-execution container (which adds seconds of cold-start latency that kills the workflow experience). The allowlist is the rational choice when your runtime does not give you security guarantees.
Deno changes the calculus. The safe thing and the powerful thing are the same thing. You do not have to choose between “users can import anything” and “the platform is secure.” The permission model handles it.
I think this design space was available for a while and nobody took it — partly because these platforms are deeply invested in Node infrastructure, partly because the allowlist was “good enough” for most users, and partly because the people building code steps at these companies may not have connected “Deno’s permission model” to “we could let users import anything.” Being a solo founder with no committee to convince has its advantages.
The escape hatch is real too
To be clear, the Code step is also good for the simpler case — expressing logic that would be awkward as visual nodes. Lead scoring with six weighted fields. Fuzzy deduplication with custom precedence rules. Flattening nested JSON where the keys change per record type. Sometimes forty lines of TypeScript is clearer than eight visual steps wired together.
const records = inputs.fetchRecords.data;
const scored = records.map(record => ({
...record,
score: calculateScore(record.revenue, record.engagement, record.recency),
tier: record.revenue > 50000 ? 'enterprise' : 'standard',
}));
return { scored, totalProcessed: scored.length };
Visual steps for the 90% where drag-and-drop is genuinely clearer. Code for the 10% where prose-style logic is the right representation. But the reason the Code step is a fundamentally different capability — not just a convenience — is the package ecosystem. That is what makes it a real runtime instead of a fancy calculator.
What's next for the Code step
The Code step currently ships with a textarea editor. We are actively building a Monaco-based editor with full TypeScript intellisense, autocomplete for the workflow context, and inline type information for step inputs and outputs.
Everything stays in one place
The worst thing a workflow platform can do is force you to leave it. The moment you move logic to an external Lambda or a separate script, you split your workflow across two systems, and debugging means correlating logs in two different places.
The Code step keeps everything inside QuickFlo. Your code shows up in the execution trace the same way every other step does — inputs, outputs, and the full stderr preserved in the execution panel so you can see exactly what your code logged or errored on. When you replay a past execution, you see what your code did on that specific run. When the AI Builder diagnoses a failure, it reads your code step’s output the same way it reads any other step.
That is the whole point. Not “we added code because visual was not enough.” We added code because the alternative — forcing users out to Lambda when they need a library — breaks everything that makes a workflow platform valuable in the first place.