188 lines
8.9 KiB
Markdown
188 lines
8.9 KiB
Markdown
# Binary Basics
|
|
|
|
The `$binary` slot in depth: its shape, which nodes fill and read it, how to handle the bytes in a Code node, mime types, size limits, and how to confirm a file actually made it through.
|
|
|
|
## Contents
|
|
|
|
- [The slot shape](#the-slot-shape)
|
|
- [Which nodes produce binary](#which-nodes-produce-binary)
|
|
- [Which nodes consume binary](#which-nodes-consume-binary)
|
|
- [Reading binary in a Code node](#reading-binary-in-a-code-node)
|
|
- [Writing binary in a Code node](#writing-binary-in-a-code-node)
|
|
- [Mime types](#mime-types)
|
|
- [File-size limits](#file-size-limits)
|
|
- [Inspecting binary in an execution](#inspecting-binary-in-an-execution)
|
|
- [When binary is the trigger input](#when-binary-is-the-trigger-input)
|
|
|
|
---
|
|
|
|
## The slot shape
|
|
|
|
Every item has two top-level keys. `json` is your data; `binary` is your files. They are independent — a transform that rewrites `json` doesn't automatically carry `binary`, and vice versa.
|
|
|
|
```json
|
|
{
|
|
"json": { "customerId": 42, "status": "sent" },
|
|
"binary": {
|
|
"invoice": {
|
|
"data": "<base64-encoded bytes>",
|
|
"mimeType": "application/pdf",
|
|
"fileName": "invoice-42.pdf",
|
|
"fileExtension": "pdf",
|
|
"fileSize": "12 kB"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The key inside `binary` — `invoice` here — is the **binary property name**. It can be anything; `data` is the default that most nodes use. File-handling nodes expose a `binaryPropertyName` parameter that points at this key, so the producer names the slot and every consumer references it by that exact name. Get the name wrong on the consumer and it looks for a slot that doesn't exist.
|
|
|
|
The four fields that matter:
|
|
|
|
| Field | What it is |
|
|
|---|---|
|
|
| `data` | The bytes, base64-encoded |
|
|
| `mimeType` | How consumers should interpret the bytes (`application/pdf`, `image/png`, …) |
|
|
| `fileName` | Used by email attachments, uploads, downloads to disk |
|
|
| `fileExtension` | Often derived from `fileName`; some nodes use it directly |
|
|
|
|
---
|
|
|
|
## Which nodes produce binary
|
|
|
|
You almost never assemble the slot by hand — a node populates it:
|
|
|
|
| Node | What to set | Result |
|
|
|---|---|---|
|
|
| HTTP Request | `responseFormat: "file"` | Response body in `$binary.data` (or the name in `options`) |
|
|
| Read/Write Files from Disk (read) | the file path | File contents in `$binary` |
|
|
| S3 / Google Drive / Dropbox (download) | the file reference | Downloaded file in `$binary.<key>` |
|
|
| Email triggers (IMAP, Gmail trigger) | attachment handling on | Each attachment in `$binary` |
|
|
| Provider AI media nodes (image/audio gen) | `options.binaryPropertyOutput` | Generated bytes in the named slot |
|
|
|
|
The single most common bug here: an **HTTP Request download left on the default response format**. Without `responseFormat: "file"`, n8n tries to parse the body as JSON or text and you end up with a corrupted string in `$json` instead of clean bytes in `$binary`. Confirm the field with `get_node` on `nodes-base.httpRequest` — the response-handling options sit under different shapes across versions.
|
|
|
|
Provider AI nodes (image generation, text-to-speech) are the other recurring trap: many don't emit binary unless you set `options.binaryPropertyOutput` explicitly. Without it, the next node has nothing to upload.
|
|
|
|
---
|
|
|
|
## Which nodes consume binary
|
|
|
|
Consumers reference the slot by its property name:
|
|
|
|
| Node | How it references binary |
|
|
|---|---|
|
|
| Email (Send) | attachment field points at `binaryPropertyName` |
|
|
| Slack (send file) | references the binary property |
|
|
| HTTP Request (multipart/form-data) | references binary in the body parameters |
|
|
| Storage upload (S3, R2, Drive) | references binary as the request body |
|
|
| Write Files to Disk | writes the named binary property to a path |
|
|
|
|
The pattern is always the same: producer names a property, consumers point at that name. Most "the file didn't attach" bugs are a property-name mismatch between the two ends — verify both with `get_node` and by inspecting the execution.
|
|
|
|
---
|
|
|
|
## Reading binary in a Code node
|
|
|
|
Most workflows never read the bytes — they pass binary straight through to a consumer. When you genuinely need the bytes (hashing, parsing, text extraction), use `getBinaryDataBuffer` in a Code node. Do **not** grab `$binary.<key>.data` and base64-decode it yourself; the helper handles n8n's storage modes (in-memory vs filesystem) for you.
|
|
|
|
```javascript
|
|
// Code node, "Run Once for Each Item"
|
|
const buffer = await this.helpers.getBinaryDataBuffer(0, 'data'); // (itemIndex, propertyName)
|
|
|
|
const text = buffer.toString('utf-8'); // for text-like files
|
|
const length = buffer.length;
|
|
|
|
return [{
|
|
json: { ...$json, length },
|
|
binary: $input.item.binary, // ← pass the file through, or it's gone after this node
|
|
}];
|
|
```
|
|
|
|
`getBinaryDataBuffer(itemIndex, propertyName)` returns a Node `Buffer`. Treat it like any buffer — slice it, hash it, decode it. The language-level specifics (which helpers exist, execution modes, `$input` vs `$json`) belong to the **n8n-code-javascript** skill; the only binary-specific rule is the one in the comment above: **if you don't return `binary`, the file is dropped at this node.**
|
|
|
|
> Reading a PDF's text is not as simple as `buffer.toString('utf-8')` — PDF is a binary container, not UTF-8 text. You need a real parse step (an OCR/extract node, or a dedicated library in an environment that has one). The buffer gives you the bytes; turning them into readable text is a separate problem.
|
|
|
|
---
|
|
|
|
## Writing binary in a Code node
|
|
|
|
Build the slot yourself: base64 the bytes, then add a mime type and file name so consumers know what they're getting.
|
|
|
|
```javascript
|
|
const text = 'Hello, world!';
|
|
|
|
return [{
|
|
json: { ok: true },
|
|
binary: {
|
|
report: {
|
|
data: Buffer.from(text).toString('base64'),
|
|
mimeType: 'text/plain',
|
|
fileName: 'report.txt',
|
|
fileExtension: 'txt',
|
|
},
|
|
},
|
|
}];
|
|
```
|
|
|
|
Skip `mimeType` and downstream consumers may refuse the file or render it wrong (an email won't attach it cleanly, Slack shows a generic file icon instead of an inline image). Always set it.
|
|
|
|
---
|
|
|
|
## Mime types
|
|
|
|
`mimeType` is the contract between producer and consumer. A wrong value doesn't error — it makes the consumer misbehave: refuse to attach, render as a download instead of inline, or show a broken thumbnail.
|
|
|
|
| File type | Mime type |
|
|
|---|---|
|
|
| PDF | `application/pdf` |
|
|
| PNG | `image/png` |
|
|
| JPEG | `image/jpeg` |
|
|
| Plain text | `text/plain` |
|
|
| JSON | `application/json` |
|
|
| CSV | `text/csv` |
|
|
| XLSX | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` |
|
|
| ZIP | `application/zip` |
|
|
|
|
When the source doesn't tell you the type, sniff it from the leading bytes — PDF starts with `%PDF-`, PNG with `\x89PNG`, JPEG with `\xFF\xD8\xFF`. A few lines of magic-byte checking in a Code node is a reliable fallback when you can't trust the upstream metadata.
|
|
|
|
---
|
|
|
|
## File-size limits
|
|
|
|
Execution data is stored in n8n's database, and large base64 blobs bloat it and slow the instance down. Rough guidance:
|
|
|
|
| Size per slot | Verdict |
|
|
|---|---|
|
|
| A few MB | Fine |
|
|
| Tens of MB | Works, but slower; watch instance memory |
|
|
| 100 MB+ | Offload to external storage and pass a URL/ID instead |
|
|
|
|
For large files, the pattern is: upload to object storage as soon as the bytes exist, thread the URL or key through the workflow as plain JSON, and re-fetch only at the node that actually needs the bytes. This keeps the per-item payload small and the execution fast. (If a self-hosted instance uses filesystem binary-data mode rather than in-memory, the database pressure is lower, but the same offload advice holds for genuinely large files.)
|
|
|
|
---
|
|
|
|
## Inspecting binary in an execution
|
|
|
|
`validate_workflow` will not tell you whether binary survived a node — a dropped slot is a silent failure. The only reliable check is the execution itself:
|
|
|
|
1. Run the workflow (`n8n_test_workflow`, or trigger it for real).
|
|
2. Pull the execution with `n8n_executions` and look at per-node output for the `binary` slot.
|
|
3. The slot shows presence and metadata (name, mime type, size) even when the base64 is too large to render in full. Its presence or absence on each node is what you're checking.
|
|
|
|
The node where `binary` last appears, then vanishes on the next, is exactly where a pass-through or a Merge needs to go. (See `MERGE_FOR_CONTEXT.md`.)
|
|
|
|
---
|
|
|
|
## When binary is the trigger input
|
|
|
|
For workflows that receive a file — a multipart webhook upload, an email attachment, a watched folder — the binary arrives at the **trigger's output**:
|
|
|
|
- Reference it by its binary property name from the trigger onward.
|
|
- Pass it through every downstream node that needs it (each is a potential strip point).
|
|
|
|
If binary doesn't show up at the trigger output, check:
|
|
|
|
- **Content-type handling.** A Webhook receiving `multipart/form-data` puts files in `$binary` and form fields in `$json.body`; one receiving JSON has no binary at all. Expression-level detail on `$json.body` for webhooks lives in **n8n-expression-syntax**.
|
|
- **The trigger's binary settings.** Some triggers skip attachments unless explicitly told to download them.
|