How to Return Binary Data from a Programmatic n8n Node
A programmatic n8n node returns a file as part of an item. The item needs a json object for ordinary fields and a binary object for file data. Each binary value has a property name such as data, file, or attachment. That name becomes part of the node's contract with the next node.
Use n8n's binary helper to read the bytes, then create the output binary object with the file name and MIME type the next step needs. Keep one output item per input item unless the operation intentionally changes the count, and preserve item relationships when you filter, expand, or reorder data.
The examples below are patterns to adapt and run. Check them against your n8n version and the n8n-workflow types your package builds against before you ship.
A typical flow is HTTP Request (downloads a PDF into binary.data) → your node (reads data, transforms or uploads it, returns a file under binary.output) → Google Drive or S3 (uploads the property named output). Every hand-off depends on the property name and metadata being right.
Start with the n8n item shape
n8n passes data between nodes as an array of items. A custom node should return the same outer shape even when the useful payload is a file.
[
{
"json": {
"documentId": "doc_123",
"source": "customer-upload"
},
"binary": {
"data": {
"data": "base64 data or n8n binary storage reference",
"mimeType": "application/pdf",
"fileName": "contract.pdf",
"fileExtension": "pdf"
}
}
}
]
The data field is required for binary data. n8n also recommends setting MIME type, file extension, and file name when they are known. The official data-structure guide documents the json and binary layout, including the rule that custom nodes still return the json key. If you are building the surrounding package, the NativeShip custom-node guide covers the project structure around the execute method.
| Part | What it carries | What your node should decide |
|---|---|---|
| json | IDs, status, extracted values, and other ordinary fields | Which values describe the file or the API result |
| binary | Named file properties | Which property name the next node reads |
| binary.data | The file bytes managed by n8n | How the node gets or creates the bytes |
| binary.mimeType | The content type | The API response type or a safe fallback |
| binary.fileName | The name shown to users and downstream nodes | A useful, safe name with an extension |
| binary.fileExtension | The extension used by file-aware nodes | The extension that matches the content type |
A Code node can add some missing wrapper syntax in newer n8n versions. A custom node does not get that shortcut. Return an item with json and add binary beside it.
Treat the binary property name as a contract
A node that accepts a binary input should expose the property name as a parameter or document a fixed name. Many built-in nodes use data, but an upstream node may output file, attachment, image, or another name.
Read the same property name that the user selected. Do not assume the first binary key is the right file when an item can contain more than one file.
| Situation | Safer behavior |
|---|---|
| The user chooses a binary property | Read that exact name for every input item |
| Your node always consumes data | Document data as the required input name and report a clear error when it is missing |
| The operation creates a file | Let the user choose the output property or use a documented default |
| The operation passes a file through | Keep the original binary map unless the node is meant to replace it |
| An upload API expects a field such as file | Map the selected n8n property to that API field in the request |
The property name inside binary and the field name in an HTTP multipart request do different jobs. Keep the mapping explicit so a rename in the node UI does not silently upload the wrong file.
Read bytes with getBinaryDataBuffer
The current n8n Code node cookbook recommends getBinaryDataBuffer for reading a file. It also warns against older expressions that reach into a base64 string such as items[0].binary.data.data. That direct path depends on storage details and breaks when n8n stores the file outside the item.
In a programmatic node, the pattern looks like this:
import { NodeOperationError } from 'n8n-workflow';
const items = this.getInputData();
const returnData = [];
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
const inputItem = items[itemIndex];
const binaryPropertyName = this.getNodeParameter(
'binaryPropertyName',
itemIndex,
) as string;
const inputBinary = inputItem.binary?.[binaryPropertyName];
if (!inputBinary) {
throw new NodeOperationError(
this.getNode(),
'No binary file was found in property ' + binaryPropertyName,
{ itemIndex },
);
}
const inputBuffer = await this.helpers.getBinaryDataBuffer(
itemIndex,
binaryPropertyName,
);
// Transform inputBuffer or send it to the API here.
returnData.push({
json: { ...inputItem.json },
binary: inputItem.binary,
pairedItem: { item: itemIndex },
});
}
return [returnData];
The helper takes an item index and a binary property name. Use the index from the loop, not a hard-coded zero. That keeps a batch of files aligned with its JSON fields and with any downstream item expressions.
The getBinaryDataBuffer recipe is written for the Code node and is not available in Python. Check the current n8n node types and runtime before adding the helper to a TypeScript package.
Create output binary data with n8n's helper
When a node changes file bytes or downloads a new file, create a new binary value instead of copying the old metadata onto unrelated bytes. The current n8n binary helper interface exposes prepareBinaryData for a Buffer or readable stream. It stores the bytes in the configured binary-data system and returns metadata that n8n can use.
This example keeps the input file name and MIME type after a transform:
import { NodeOperationError } from 'n8n-workflow';
const inputBinary = inputItem.binary?.[binaryPropertyName];
if (!inputBinary) {
throw new NodeOperationError(
this.getNode(),
'The selected binary property is missing',
{ itemIndex },
);
}
const outputBuffer = transformFile(inputBuffer);
const outputBinary = await this.helpers.prepareBinaryData(
outputBuffer,
inputBinary.fileName ?? 'output.bin',
inputBinary.mimeType ?? 'application/octet-stream',
);
returnData.push({
json: {
...inputItem.json,
transformed: true,
},
binary: {
output: outputBinary,
},
pairedItem: { item: itemIndex },
});
The second argument is a file path or name used to build metadata. Pass a safe file name, not a path supplied by an untrusted user. Pass a known MIME type when the API gives you one. If the transformation changes the format, change both the file name and MIME type to match the new bytes.
The helper is part of the current binary helper types and implementation. Its signature can change with n8n releases, so treat the version in your package lockfile as part of the implementation contract. Check the binary helper types in the n8n-workflow interfaces for the version you build against before releasing a node that depends on it.
Return a downloaded file
A download operation usually has three steps:
- Request the remote resource with the node's credentials.
- Keep the response body as bytes instead of parsing it as JSON or text.
- Call prepareBinaryData and place the result under the output property name.
A simplified response-to-item pattern is:
function chooseSafeFileName(header: unknown, fallback: string): string {
const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(String(header ?? ''));
const raw = match ? decodeURIComponent(match[1]) : fallback;
// Drop any directory part and characters that file systems reject.
const base = raw.split(/[\\/]/).pop() ?? fallback;
return base.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 200) || fallback;
}
const response = await this.helpers.httpRequestWithAuthentication.call(this, 'serviceApi', {
method: 'GET',
url: downloadUrl,
encoding: 'arraybuffer',
json: false,
returnFullResponse: true,
});
const responseBuffer = response.body as Buffer;
const responseHeaders = response.headers ?? {};
const contentType = String(
responseHeaders['content-type'] ?? 'application/octet-stream',
);
const fileName = chooseSafeFileName(
responseHeaders['content-disposition'],
'download.bin',
);
const binaryFile = await this.helpers.prepareBinaryData(
responseBuffer,
fileName,
contentType,
);
return [[
{
json: {
sourceUrl: downloadUrl,
contentType,
},
binary: {
data: binaryFile,
},
pairedItem: { item: 0 }, // single-input example; use the loop index in a batch
},
]];
The request option and response type need to match the helper version your node uses. Some APIs return a filename in Content-Disposition; others return only a path or an ID. Validate the header before using it, strip directory components, and fall back to a safe name when the server provides none.
Do not parse a PDF, image, archive, or spreadsheet as JSON. If the endpoint can return an error document with a successful HTTP transport response, inspect the content type and status before turning the body into a binary output.
Preserve metadata that downstream nodes need
File bytes alone are not enough for a reliable workflow. Downstream nodes use metadata to choose a parser, display a name, build a multipart request, or decide whether a file is an image or a document.
Keep these values aligned:
- MIME type describes the content, such as application/pdf or image/png.
- File extension matches the actual format, such as pdf or png.
- File name gives the user a useful name and includes the extension.
- Binary property name matches the node parameter and the next node's field selection.
- JSON fields identify the source record, download ID, or transformation result.
A file named report.pdf with MIME type image/png makes later debugging harder. A generic application/octet-stream fallback is safer than a false type, but it gives downstream nodes less information. Use the API's verified content type or detect the format from the bytes when your node has that capability.
If your node uploads the file, keep the binary value as binary until the request helper builds the upload body. Sending the binary metadata object as JSON produces a request containing fields such as data and mimeType instead of the file body.
Process multiple items without losing files
Programmatic nodes receive arrays and should process each item in the loop. Do not read item zero once and reuse its buffer for every output. That mistake can pass the first customer's file to every later item while the JSON fields still look correct.
For a one-to-one transform:
- Read the selected binary property for the current item index.
- Create or pass through that item's binary map.
- Copy or update that item's JSON fields.
- Keep the output's item relationship attached to the input index.
- Return one output item for each successful input unless the operation has a documented reason to filter it.
If one input creates several output files, add the source relationship for each output and give each file a stable property name. If a later node reports "Paired item data unavailable", the paired item troubleshooting guide explains how to trace the missing link. If the node filters items, decide whether a skipped item should disappear, produce an error output, or continue with a status field. Test the downstream expression that reads the result.
The NativeShip debugging guide can help you inspect the item shape while stepping through a custom node. Run the node with npm run dev; from n8n 3.0, n8n-node dev starts n8n in a container, so Docker or Podman must be installed, or use --external-n8n with your own instance started with N8N_DEV_RELOAD=true.
Plan for the deployment's binary storage mode
On n8n 2.x, binary data stays in memory by default (N8N_DEFAULT_BINARY_DATA_MODE=default). Large files can therefore raise memory use or crash an instance before the next node receives the item. Self-hosted administrators can set N8N_DEFAULT_BINARY_DATA_MODE to filesystem so n8n writes binary data to disk. Queue mode does not support filesystem mode; n8n's scaling guidance says to use database (or external storage such as s3) in queue deployments.
n8n 3.0 changes this. The in-memory default mode is removed, and instances that still use it switch to filesystem on upgrade. Valid values are filesystem, s3, azure, and database. n8n 3.0 also renames the storage folder from ~/.n8n/binaryData to ~/.n8n/storage on first start, so a Docker volume mounted at the old path needs moving (see the v3.0 breaking changes). Your node code does not change, because it reads and writes through the helpers either way.
The node code still reads a Buffer through the helper. The deployment setting changes where n8n stores the data between steps. Test a file size that resembles production traffic and record the deployment mode with the result. A node that works with a 50 KB image may fail when a workflow downloads a 500 MB archive.
The official binary-data scaling guide documents the memory default, filesystem setting, queue-mode limit, and pruning behavior. The files and images guide lists the built-in nodes that can convert, extract, read, and write binary data when a custom node is not required.
Test the binary contract before publishing
Record the n8n version, node package version, binary input name, output name, and deployment mode for each run. A useful test matrix includes:
| Test | What to inspect |
|---|---|
| Small text file | Bytes, file name, MIME type, and JSON fields |
| PNG, PDF, or other real file | The file opens after the node returns it |
| Missing binary property | The error names the selected property and item index |
| Multiple binary properties | The node reads the selected property, not the first key |
| Multiple input items | Each output contains the matching file and JSON record |
| File transform | New bytes match the new name, extension, and MIME type |
| Download with headers | Safe filename and verified content type |
| Downstream upload or extraction | The next node receives a file body, not metadata JSON |
| Large file in the target deployment mode | Memory, queue, pruning, and execution behavior |
Run this matrix after the node's credentials, request options, and output shape are in place. The NativeShip credentials guide covers the credential boundary for an authenticated download, and the node versioning guide helps record changes when the binary contract evolves.
A generated package or an OpenAPI definition can give you a starting operation. It does not prove that a file download, upload, or transform works in the target n8n version. Keep the binary test evidence beside the release so a future API or n8n change has a clear comparison point.
Frequently asked questions
Yes. Return an item with a json object and place the file under the item's binary object. The Code node may add missing wrappers in newer versions, but a custom node should return the complete item shape.
Read the selected binary property with getBinaryDataBuffer using the current item index. Avoid reaching into a stored base64 path such as items[0].binary.data.data.
No. data is a common default, not a requirement. Let the node accept a property name or document the name it requires, then use the same name consistently.
Keep the bytes in a Buffer or readable stream, call prepareBinaryData with a safe file name and known MIME type, and put the returned value under the output binary property.
Read and write the binary value inside the loop for each item. Keep the matching JSON fields and item relationship with that output. Do not reuse the buffer from item zero.
Common causes include parsing bytes as JSON or text, using the wrong MIME type or extension, returning metadata as JSON, or writing a new file while retaining the old metadata. Inspect the bytes and metadata together.
On n8n 2.x, binary data stays in memory by default, so set N8N_DEFAULT_BINARY_DATA_MODE=filesystem (or database/s3 in queue mode). n8n 3.0 removes the in-memory mode and switches such instances to filesystem. Test the file size and storage mode you plan to support.