Files are what a run makes besides JSON: screenshots, downloads, HTTP bodies, generated documents. An item carries its files next to its json, by key, as references; the bytes stay on the engine with the run, and a page opens them through a short link from a separate host.
The file reference
type TFlowFile = { id: string; runId: string; name: string; mime: string; size: number; extension: string };
type TFlowItem = { json: Record<string, unknown>; file?: Record<string, TFlowFile>; trail: Record<string, number> };
fileis keyed: a Screenshot node's item hasfile.screenshot, a script can use any key (file.doc,file.invoice).- An item without files has no
fileat all. - A node passes its input's files on only by returning them (
file: ctx.file.item). Built-in nodes that only change JSON (Set, Filter…) drop them, like n8n.
Files from outside
Files can come with what starts a run, or with a form that answers its wait:
- Webhook:
multipart/form-datafiles, by field name. - Form trigger: file questions, by question name (
invoice, theninvoice_2…). - Form node and Wait for a form (public page): same as the Form trigger, on the answered item.
mooflair sends them base64 with the start (upload, 25 MB per call) or with the resume call (upload). The engine writes them into the run's folder like a node's file, then drops them from the payload. They show up as the start item's files (the trigger passes them on), or on the waiting node's output item.
Where the bytes live
| What | Where |
|---|---|
| A file's bytes | library/workspace/<workspace id>/file/<runId>/<id>: one area per workspace, mounted at /file in every browser and worker of the workspace, so a run's files are there whichever browser its step runs in |
| Its details (name, mime, size) | beside it, <id>.json |
| In a browser or the worker | /file/<runId>/<id> |
The owner is the run's browser owner: the campaign for real runs, the flow for Studio tests. Every browser of that owner, and its , gets the owner's file folder mounted at /file. So a file one browser wrote can be read in another, and ctx.file.path() works with page.setInputFiles(). A browser without an owner (POST /browser) has no /file, and writing a file there fails.
Reading and writing in a node
| Member | What it does |
|---|---|
ctx.file.item | The files of the item the node runs for, by key |
ctx.file.items | The files of every input item, in the same order as ctx.items |
ctx.file.write({ name, mime, data }) | Saves data (a Buffer, bytes or text) and returns its reference. A name without an extension gets one from the mime type |
ctx.file.read(file) | The bytes, as a Buffer |
ctx.file.path(file) | Its path in this container, under the file's own name (a link at /tmp/file/<id>/<name>), so a page given it sees the name and type (𝕏 drops a file without a type) |
Caps:
- 100 MB per file.
- 1 GB per run, for all its browsers together.
A write past either cap fails the step.
// ** A node that writes a file and hands it on as item.file.doc
export default async (ctx) => {
const doc = await ctx.file.write({ name: "report.csv", mime: "text/csv", data: "a,b\n1,2" });
return { item: [{ dot: "next", json: { rows: 1 }, file: { doc } }] };
};
In templates, {{ file.<key> }} is the reference and {{ file.<key>.name }}, .mime, .size its details. See .
Drive fields
A node field of kind drive (the post nodes' Media) holds Drive file ids: a list ({{ nodes.next.output.media }} from Next post), one id, a comma list, or items with an id (the Drive node's list). Before each step of that node the engine copies every file into the run folder (library/workspace/<id>/file/<run id>/), from the engine holding it when it's another one, and hands the node run files instead of ids, so it can page.setInputFiles(files.map((entry) => file.path(entry))). Only the workspace's own files; a deleted one fails the step with its id, and one on an engine that's down fails with isn't answering right now (it works again once that engine is back). The copies go with the run's other files.
How long files are kept
A run's files go when the run goes (90 days), or sooner with the flow's Keep files (days) setting (setting.file.ttlMs, sent with what the run gets as file.ttlMs), counted from the run's end. Once an hour the engine removes:
- the folder of a run that no longer exists (expired, or a campaign's Clear);
- the folder of a finished run past its
ttlMs.
Deleting a campaign or a flow deletes its whole library folder, files included.
A resumed run is a new run that can still hold references to the earlier run's files. They are kept by the earlier run's rules, not the resumed one's.
Viewing and downloading
A page never loads a file from mooflair's origin:
- The page asks the engine for a link:
POST /run/<runId>/file/<id>/link{ isInline }, with its workspace token (a campaign token reaches only that campaign's runs). - The engine answers
{ url, expiresAt }. The URL lives 10 minutes and is signed withENGINE_SECRET(prefixedfile., so it can't stand in for a workspace token). GET /file/:tokenserves the bytes fromENGINE_FILE_URL(a host of its own in production, e.g.files.<domain>), elseENGINE_PUBLIC_URL.
| Header | Value |
|---|---|
Content-Disposition | attachment always, except inline for previews of PNG, JPEG, GIF, WebP and PDF |
Content-Type | The file's mime when shown in place, else application/octet-stream |
X-Content-Type-Options | nosniff |
Content-Security-Policy | default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox (not on PDFs: Chrome's viewer won't open in a sandbox) |
Cache-Control | private, max-age=600 |
So HTML or SVG a run fetched never runs on the app's origin or the file host. In Studio, shows an item's files as chips, with image and PDF previews and Download.
FAQ
Why are files references and not base64 in the JSON?
Items travel to mooflair and are saved with the run, capped at 1 MB. A file of a few MB in base64 would be cut. A reference is a few bytes, and the bytes stay where the browsers can use them.
Can I open a file from another workspace with its link?
No. The link route checks that the file's run belongs to your workspace, or to your campaign for a campaign token. A link that was changed is refused (401), and a link past 10 minutes answers 410.
Where do Drive files fit in?
Drive (Dataset › Files) keeps a workspace's files across runs: mooflair keeps the records, engines keep the bytes in library/drive as nodes of the Drive (see ). The Drive node turns one into a run file (read) or saves a run file into it (write). Run files are the short-lived, per-run kind described here.