N8n Binary And Data
Romuald CzlonkowskiCommunityCreated by Romuald Czlonkowski
- Skill
- Guidelines
- Automation and integrations
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
In n8n, data is split into two distinct namespaces: $json for structured metadata and $binary for raw file bytes. This skill provides the essential rules for managing files, images, and PDFs across your workflows. It addresses common pitfalls like binary stripping during transformations, the strict JSON-only boundary of AI agent tools, and the requirement for CDN-hosted URLs when displaying media in chat interfaces.
What it does
- Explains the separation of $json and $binary slots.
- Provides patterns for preserving binary data across JSON-only transformation nodes.
- Defines the storage-key pattern for passing files to and from AI agent tools.
- Details the requirement for URL-based image rendering in chat platforms.
- Offers debugging strategies for identifying where binary data is silently dropped.
- Outlines proper configuration for HTTP requests and Code nodes to handle file buffers.
When to use it
- When your workflow involves processing files, PDFs, images, or attachments.
- When you need to pass a file to an AI agent tool or receive a generated file from one.
- When you are building chat-based automations that need to display images.
- When a file disappears mid-workflow after passing through a transformation node.
- When you need to read or write raw file bytes using a Code node.
Advantages
- Prevents silent data loss by ensuring binary slots are correctly re-attached.
- Enables seamless integration of file-based tools with AI agents.
- Standardizes the handling of media for chat-based user interfaces.
- Reduces workflow errors caused by misinterpreting binary as JSON strings.
- Provides clear architectural patterns for complex file-handling pipelines.
Usage examples
- “How do I keep an email attachment from disappearing after I use an Edit Fields node?”
- The skill explains how to use the pass-through option on the node or how to use a Merge node in combineByPosition mode to re-attach the binary slot.
- “How can I pass an uploaded PDF to an AI agent tool?”
- The skill instructs you to stage the file in storage, pass the storage key through the JSON tool argument, and have the tool download the file using that key.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/czlonkowski/n8n-binary-and-data/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip n8n-binary-and-data.zip -d .agents/skills
Documentation by Romuald Czlonkowski
n8n Binary and Data
Every n8n item carries two independent slots: $json for structured data and $binary for file bytes. They travel side by side through the workflow. File contents — the actual PDF, image, or zip — live in $binary, never in $json. Get that split wrong and you read an empty field, lose a file mid-flow, or hand an AI agent a tool input it can't use.
This skill covers where binary lives, how to read and write it, how to keep it from being silently stripped, the hard wall between binary and the AI-agent tool boundary, and why chat surfaces need a URL instead of raw bytes.
The three rules that prevent 90% of binary bugs
File contents are in
$binary, not$json. After an HTTP download, a "Read Files", or an email-attachment trigger, the bytes sit in$binary.<key>.$jsonholds metadata at most. Reading$json.datafor file contents gives you nothing.Binary cannot cross the AI-agent tool boundary — in either direction. Tool arguments and tool return values are JSON only. An uploaded image can't be passed into a tool as a file, and a tool can't return raw bytes. Pre-stage to storage and pass a key or URL through JSON instead. See
AGENT_TOOL_BINARY.md.Chat surfaces render images by URL, not by
$binary. Slack, Discord, Teams, Telegram, embedded webhook chat — none of them read the binary slot. The image has to live somewhere a URL can fetch it. SeeCDN_REQUIREMENT.md.
The two slots
Each item is shaped like this:
{
"json": { "customerId": 42, "status": "sent" },
"binary": {
"invoice": {
"data": "<base64-encoded bytes>",
"mimeType": "application/pdf",
"fileName": "invoice-42.pdf",
"fileExtension": "pdf"
}
}
}The key inside binary (invoice here) is the binary property name. Most file-handling nodes have a binaryPropertyName parameter that points at it — the producer names the slot, the consumer references it by that name. The default key across most nodes is data, so when nothing tells you otherwise, assume $binary.data.
$json and $binary are separate namespaces. An expression like {{ $binary.invoice.fileName }} reads file metadata; {{ $json.customerId }} reads data. They never mix.
This split also explains a webhook gotcha: a Webhook trigger receiving multipart/form-data puts the uploaded file in $binary and the accompanying form fields in $json.body — so an uploaded file is not somewhere under $json at all. (The $json.body nesting for webhooks is n8n-expression-syntax territory.)
See BINARY_BASICS.md for the full slot anatomy, mime types, and size limits.
Producing binary
You rarely build a $binary slot by hand — nodes populate it for you:
| Source | How binary appears |
|---|---|
HTTP Request with responseFormat: "file" | Response body lands in $binary.data (or the name you set) |
| Read/Write Files from Disk | File contents read into $binary |
| Storage downloads (S3, Google Drive, Dropbox, etc.) | Downloaded file in $binary.<key> |
| Email triggers with attachments | Each attachment arrives in $binary |
| Provider AI media nodes (image/audio gen) | Set options.binaryPropertyOutput so the bytes land where the next node looks |
For an HTTP download, the one field that matters is responseFormat. Confirm it with get_node on nodes-base.httpRequest — leaving it as the default JSON/string format is the classic reason a downloaded file ends up as garbled text in $json instead of clean bytes in $binary.
Reading and writing binary in a Code node
Most workflows never need to crack open the bytes — they just pass binary through to a consumer (email attachment, file upload, Slack file). Most byte jobs have a native node: Extract From File (CSV, XLSX, PDF text, JSON, plain text → items or a field), Convert To File (items → CSV / XLSX / JSON / text file), Crypto (hash a file with binaryData: true), Compression (zip / gzip). Read or write raw bytes in a Code node only when no node handles the format, with a Why Code: note.
Read with getBinaryDataBuffer — do not try to base64-decode $binary.<key>.data by hand:
// Code node, "Run Once for Each Item"
const buffer = await this.helpers.getBinaryDataBuffer(0, 'data'); // (itemIndex, propertyName)
const text = buffer.toString('utf-8');
const length = buffer.length;
return [{
json: { ...$json, length },
binary: $input.item.binary, // pass the binary through, or it's gone
}];Write by building the slot yourself — base64 the bytes plus a mime type and file name:
const text = 'Hello, world!';
return [{
json: { ok: true },
binary: {
report: {
data: Buffer.from(text).toString('base64'),
mimeType: 'text/plain',
fileName: 'report.txt',
fileExtension: 'txt',
},
},
}];The Code-node sandbox, helpers, and execution modes are the domain of n8n-code-javascript (and n8n-code-python) — use those for the language-level detail. The one binary-specific thing to remember here: a Code node that returns [{ json: {...} }] without re-attaching binary silently drops the file. See BINARY_BASICS.md.
Keeping binary alive across transforms
JSON-only nodes — Edit Fields (Set), Code, IF, and others — can drop the $binary slot from their output. The workflow validates clean and runs without error; the file just isn't there downstream when the email node goes to attach it.
Two ways to keep it:
Pass-through option on the transforming node. Edit Fields has
includeOtherFields; a Code node can returnbinary: $input.item.binaryexplicitly. Cheapest fix when it's available.Fan out and Merge by position. Route the source into both the transform and a bypass branch, then recombine with a Merge in
combineByPositionmode. The JSON comes from the transform side, the binary survives on the bypass side.
[Source with binary] ─┬─→ [Edit Fields: change JSON] ─┐
│ (binary stripped here) ├─→ [Merge: combineByPosition] ─→ [Email: attach]
└──────────────────────────────────┘
(bypass — binary passes through untouched)combineByPosition pairs item N from each input, so the field counts must line up. The connection wiring and the alternatives for many-strip-point chains (upload-early, sub-workflow) are in MERGE_FOR_CONTEXT.md.
The agent-tool binary boundary
This is the sharpest edge. An AI Agent talks to its tools (Custom Code Tool, Call n8n Workflow Tool, HTTP Request Tool, MCP tools) over JSON. Binary does not fit through that pipe in either direction. The fix is the same shape both ways: stage the bytes in storage, pass a key/URL through JSON, fetch on the other side.
Inbound — a user uploads a file the agent's tool must operate on:
The chat trigger gives you a
files[]array. Split it out and upload each file to private storage under a hashed key.Re-merge that branch before the agent runs (it's a synchronization barrier, not decoration), and set
executeOnce: trueon the agent so N files don't trigger N agent runs.Inject the keys into the agent's system prompt, listing both the original name (human context) and the storage key (what the tool needs), with an explicit "use EXACTLY this key".
The tool receives the key as a string argument and downloads the file from storage itself.
Outbound — a tool generates a file the agent must return:
The tool sub-workflow generates the binary, uploads it to storage, and returns JSON like
{ "ok": true, "key": "...", "url": "https://...", "mimeType": "image/png" }.The agent embeds the URL in its reply (or passes the key to another tool).
passthroughBinaryImages: true on the agent only changes what the LLM sees for vision — it does not let tools receive the file, and it's image-only (no PDFs, audio, or video). You still need the upload-and-pass-key pattern for any tool. Full patterns, hash strategy, storage choices, and the long-running-tool variant are in AGENT_TOOL_BINARY.md.
Building the tool itself? See n8n-code-tool for the Custom Code Tool contract and n8n-workflow-patterns for the AI-Agent-with-tools shape.
The CDN requirement for chat surfaces
When a workflow generates an image and the user wants it shown inside a chat message:
Binary on the item isn't enough. The chat client renders messages that reference images by URL (or pushes bytes through the platform's own file-upload API). It never reads
$binary.The bytes have to live somewhere a URL can fetch over HTTPS. Upload to an object store or drive first, then embed the returned URL.
n8n has no built-in CDN. The user provides the storage.
Ask which storage they already use rather than defaulting to S3 — object storage (S3, R2, GCS, Azure Blob, Backblaze B2, Supabase Storage) and drive-style services (Dropbox, Google Drive, OneDrive, Box) all work and all change the URL shape. Cloudflare R2 is the lowest-friction starting point if they have nothing. For sensitive content, use a signed URL with an expiry rather than a permanently public one. See CDN_REQUIREMENT.md.
What's NOT available
$fromAI()cannot carry binary. It fills tool parameters with strings, numbers, booleans, and objects — never file bytes. Pass a storage key instead.Tool arguments and returns are JSON only. There is no "binary parameter" on an agent tool, in or out.
n8n ships no CDN or public file host. Serving a file over a URL is always something the user's storage does, not n8n.
getBinaryDataBufferis a Code-node helper. It isn't available in the Custom Code Tool sandbox (see n8n-code-tool).
Where Data Tables live
For persistent tabular storage — reference-counting staged files, tracking which keys are live, dedup — that's the n8n_manage_datatable surface, owned by n8n-mcp-tools-expert. This skill does not cover Data Tables.
Anti-patterns
| Anti-pattern | What goes wrong | Fix |
|---|---|---|
Reading file contents from $json | Bytes live in $binary; $json is empty or metadata only | Read $binary.<key>, or getBinaryDataBuffer in a Code node |
HTTP download without responseFormat: "file" | Bytes arrive as mangled text in $json, not clean binary | Set responseFormat: "file" on the HTTP Request node |
Code node returns [{json:{...}}], no binary | The file is silently dropped downstream | Re-attach binary: $input.item.binary in the return |
| JSON transform (Edit Fields/IF) eats the binary | Email/upload node finds nothing to attach | Pass-through option, or fan out + Merge by position |
Passing an uploaded file into a tool via $fromAI | $fromAI can't carry binary; the tool gets nothing | Pre-stage to storage, inject the key in the system prompt, tool fetches by key |
Assuming passthroughBinaryImages lets tools see the file | It only affects what the LLM sees, and only for images | Still need the upload-and-pass-key pattern for tools |
| Tool returns raw binary to the agent | Tool output is JSON; bytes don't survive (and bloat context) | Upload, return { key, url } in JSON |
Posting $binary to a chat surface and expecting an image | Chat clients render by URL, not raw bytes | Upload to storage/CDN, embed the URL or use the platform file API |
| Hardcoding base64 in a Code node | Huge workflow JSON, slow, leaky | Reference via $binary, or upload and reference by URL |
Reference files
| File | Read when |
|---|---|
BINARY_BASICS.md | First time handling binary, or reading/writing the $binary slot, mime types, size limits |
AGENT_TOOL_BINARY.md | An agent tool needs an uploaded file, or produces one — the boundary in either direction |
MERGE_FOR_CONTEXT.md | Binary disappears after a JSON transform and you need to re-attach it |
CDN_REQUIREMENT.md | Showing images in a chat surface or anywhere that needs URL-referenced images |
Integration with Other Skills
n8n-code-javascript / n8n-code-python: the Code node is where you read/write raw bytes (getBinaryDataBuffer, Buffer.from(...).toString('base64')). Those skills own the sandbox, helpers, and execution-mode detail — this skill owns the rule that binary must be re-attached on return.
n8n-code-tool: the Custom Code Tool sandbox is narrower — no $binary, no getBinaryDataBuffer, no $fromAI. When a tool needs a file, this skill's storage-key pattern is how it gets one.
n8n-workflow-patterns: the agent-tool binary boundary sits inside the AI-Agent-with-tools pattern; the CDN flow is a generate → upload → reply chain.
n8n-node-configuration: responseFormat, binaryPropertyName, includeOtherFields, binaryPropertyOutput are all conditional fields — use get_node to confirm the exact names on the user's version.
n8n-expression-syntax: addressing $binary.<key>.fileName vs $json.body (webhook uploads in particular) is expression territory.
n8n-validation-expert: a dropped binary slot is a silent failure — validate_workflow won't flag it. Confirm presence by inspecting the execution.
n8n-mcp-tools-expert: owns n8n_manage_datatable (Data Tables) and n8n_executions — use the latter to confirm a binary slot actually survived a given node.
n8n-error-handling: storage uploads and downloads fail; the inbound/outbound staging steps need error branches so a missing key doesn't 404 silently.
using-n8n-mcp-skills: the index of how these skills fit together.
Verifying binary survived
Validation won't catch a stripped binary slot — it's a silent failure. Confirm it ran correctly:
n8n_test_workflow(or trigger a real run) to produce an execution.n8n_executionsto pull that execution, and inspect per-node output for thebinaryslot — it shows presence and metadata even if the base64 is too large to render.The node where
binarylast appears is the node before the strip. That's where the pass-through or Merge goes.
Quick Reference Checklist
[ ] File contents read from
$binary.<key>— never$json[ ] HTTP downloads use
responseFormat: "file"[ ] Code nodes re-attach
binaryon return when the file must continue[ ] JSON transforms either pass binary through or Merge it back (
combineByPosition)[ ] No attempt to pass binary into/out of an agent tool — keys/URLs through JSON instead
[ ]
passthroughBinaryImagesused only for LLM vision, not as a tool channel[ ] Chat-surface images uploaded to storage; the URL is embedded, not the bytes
[ ] Storage backend chosen with the user (not defaulted to S3); signed URLs for sensitive content
[ ] Binary presence confirmed by inspecting the execution, not by validation
Remember: two slots, side by side. Data rides in $json, files ride in $binary — and the moment a file has to cross an agent tool or reach a chat surface, it travels as a URL, not as bytes.
Files
- AGENT_TOOL_BINARY.md14.5 KiB
- ATTRIBUTION.mdAdded by AutoGineer895 B
- BINARY_BASICS.md9.1 KiB
- CDN_REQUIREMENT.md6.1 KiB
- LICENSELicence file added by AutoGineer1.1 KiB
- MERGE_FOR_CONTEXT.md7.2 KiB
- README.md8.4 KiB
- SKILL.md15.5 KiB
AGENT_TOOL_BINARY.md
Agent Tools and Binary
The hard wall: an AI Agent and its tools talk to each other in JSON. Binary doesn't fit through that pipe in either direction, and it catches people twice.
Inbound — a user uploads a file. The agent can see an image via vision, but tool calls don't carry the file.
Outbound — a tool generates a file. Its result back to the agent is JSON, so it can't return raw bytes.
The workaround has the same shape both ways: stage the bytes in storage, pass a key or URL through the JSON boundary, fetch on the other side.
Contents
Why the boundary exists
Inbound: an uploaded file into a tool
The two pieces of plumbing that look optional
What the system prompt and the tool argument look like
passthroughBinaryImages
Outbound: a tool that produces a file
Storage choices
Hashing, cleanup, long-running tools
Surface-specific seams
Common mistakes
Why the boundary exists
A tool call is a function call the LLM makes by emitting JSON arguments; the result comes back as a JSON observation. Tool parameters are filled by $fromAI(), which only produces strings, numbers, booleans, and objects — never file bytes. And a tool's return is a string/JSON the model reads as text. Base64-stuffing a 2 MB image into a JSON field would bloat every tool call and the agent's context window, and some runtimes reject oversized observations outright. So in practice: binary never crosses the boundary.
Inbound: an uploaded file into a tool
The user pastes an image into chat. The chat trigger exposes a files[] array. If the agent only needs to look at the image, passthroughBinaryImages: true on the agent handles that (vision). But the moment a tool must operate on the file — OCR, image edit, document parse — the tool can't receive it directly. You pre-stage it.
[Chat Trigger]
│ files[]
▼
[IF: files empty?]
├── empty ────────────────────────────────────────────► [AI Agent]
└── not empty:
[Split Out files]
↓
[Crypto: hash → storage key]
↓
[HTTP Request / S3 / Drive: upload to PRIVATE storage by key]
↓
[Merge: combineByPosition] ← synchronization barrier, see below
↓
[AI Agent] ← executeOnce: true; system prompt is told the keys
│ tool call: imageKey = "sess12-abc123.png"
▼
[Call n8n Workflow Tool → sub-workflow]
↓
[Download from storage by key]
↓
[Operate on bytes: edit / OCR / parse]
↓
[Upload result, return JSON { key, url }]Building this with the community MCP server, the wiring goes in as n8n_update_partial_workflow operations — addNode for each step, addConnection to thread them, and updateNode/patchNodeField to set executeOnce and the system prompt. The agent's tool is a Call n8n Workflow Tool node pointed at the sub-workflow; the sub-workflow itself is a normal workflow that starts with an Execute Workflow Trigger.
The Execute Workflow Trigger's input mode matters here. The default typed-input mode carries only named JSON fields and drops
$binaryat the boundary; for a sub-workflow that needs to receive binary directly, use the passthrough input mode. (When the sub-workflow downloads by key instead of receiving bytes, this is moot — which is exactly why the key pattern is cleaner.)
The two pieces of plumbing that look optional
Both of these are silent-failure traps — leave them out and the workflow runs, then misbehaves.
The Merge is a synchronization barrier, not decoration. The chat trigger fans out to the IF branch and the upload branch in parallel. Without merging the upload branch back before the agent, the agent fires while uploads are still in flight. The system prompt's key template then renders against partial state, the model gets keys that don't exist in storage yet, and the tool's download 404s. The Merge forces the agent to wait for the upload to finish.
executeOnce: true on the AI Agent node. When files split out and merge back, the merged item count equals the file count. Without executeOnce, the agent runs once per file — N agent runs, N replies, N times the token cost — for what is one logical user message. Set it on the agent node:
{ "executeOnce": true }(Apply with patchNodeField on the agent node, or include it in the updateNode payload.)
What the system prompt and the tool argument look like
The agent has to know which keys exist for this turn. Inject them into the system prompt, listing both the original name (human context for the model) and the storage key (what the tool needs):
## File Handling
Files passed in this turn:
{{ JSON.stringify($('Chat Trigger').first().json.files.map((f, i) => ({
originalFileName: f.fileName,
storageKey: $('Crypto').all()[i].json.hash + '.' + f.fileExtension
})), null, 2) }}
CRITICAL: Use EXACTLY the `storageKey` value above when calling a tool. Do not paraphrase or reconstruct it.Two details earn their keep:
Both names are listed. The original (
photo.png) tells the model what kind of file it is; the storage key is what the tool can actually resolve.The "use EXACTLY". Without it, the model paraphrases — "the user's image", "photo.png" — and the tool can't find the file.
On the tool side, the storage-key parameter is bound with $fromAI and described so the model fills it correctly:
$fromAI('imageKey', 'Storage key of an existing uploaded image to operate on, taken verbatim from the system prompt (e.g. "sess12-abc123.png"). Leave empty to generate a new image. Do not invent or reconstruct keys.', 'string')The description is the model's only guidance on the value's shape — match it to the storage backend the workflow actually uses, and name only that one shape (not a menu of possibilities).
Generate vs edit in one tool. If the tool serves both "make a new image" and "edit this one", branch inside the sub-workflow on whether imageKey is empty — empty means generate, present means download-then-edit. One tool with an internal IF is usually clearer for the model than two near-identical tools. If the model keeps misfiring on that discriminator, the viable alternative is two Call n8n Workflow Tool nodes pointing at the same sub-workflow with different parameter wiring (one hardcodes an empty key, the other lets the model fill it) — one sub-workflow, two front doors with sharply different descriptions.
passthroughBinaryImages
Set passthroughBinaryImages: true on the agent when the model should be able to see uploaded images (multimodal vision). It adds the image to the LLM's prompt context.
Two limits to keep straight:
Image-only. It does nothing for PDFs, audio, or video. For those, the model only knows what the system prompt tells it (name, type, storage key) and must call a tool to extract content. For PDFs, that means an OCR/parse tool.
It does not feed tools. Tools still receive only their
$fromAIparameters, regardless of this flag. Vision and tool access are separate channels:passthroughBinaryImages: true→ the model can see and reason about the image.Pre-staged storage + key in the prompt → the model can ask a tool to do something with the file.
You usually want both at once.
Outbound: a tool that produces a file
A tool generates a PDF, image, or document. Its result to the agent is JSON, so it returns a reference, not the bytes.
[Agent calls tool]
▼
[Sub-workflow]
↓ generate or transform binary
↓ (provider AI node: set options.binaryPropertyOutput so bytes land in the slot)
[Upload to storage by key]
↓
[Respond with JSON: { ok, key, url, mimeType, sizeBytes, expiresAt }]
▼
[Agent receives JSON — embeds the URL in its reply, or passes the key to another tool]A useful return shape:
{
"ok": true,
"key": "sess12-9f3c1a.png",
"url": "https://storage.example.com/files/sess12-9f3c1a.png",
"mimeType": "image/png",
"sizeBytes": 184320,
"expiresAt": "2026-06-25T12:00:00Z"
}Then tell the agent how to present it, in the system prompt — and be explicit about images vs video, because the model will copy the image pattern onto video and produce a broken thumbnail:
## Display Protocol
Show generated images inline using markdown: 
Share generated VIDEO as a plain link, NOT an embed: [title](url)(The ![]() markdown is the canvas chat trigger's syntax — production surfaces differ; see Surface-specific seams.)
When you don't need any of this: if one node generates binary and another consumes it in the same workflow with no agent involved, just pass binary through normally — there's no boundary. And a plain webhook API that returns a file can use Respond to Webhook with binary in the body. The upload-and-return-key dance is specifically for the agent-calls-tool-and-tool-produces-a-file case.
Storage choices
Ask which service before building. n8n has native nodes for many backends, and defaulting to S3 is presumptuous.
Object storage: Amazon S3, Cloudflare R2, Google Cloud Storage, Azure Blob, Backblaze B2, Supabase Storage. Most expose S3-compatible APIs (the S3 node with the right endpoint, or HTTP Request with AWS auth) or ship a dedicated node. Keys, optional public buckets, signed URLs, lifecycle rules for TTL.
Drive-style: Dropbox, Google Drive, OneDrive, Box. File IDs and share links instead of keys, folder permissions instead of bucket ACLs, no built-in TTL (cleanup is its own workflow).
Self-hosted / FTP / SFTP: when the user has on-prem infrastructure.
Caller-supplied URL: the agent's caller provides the storage location as input.
A common production split: a private bucket/folder for inbound user files, and a public (or signed-URL) bucket/folder for outbound results so the agent can return a fetchable URL. The choice changes credential setup, URL shape, and how the tool's $fromAI description should explain the key/URL format — don't pick on the user's behalf.
Hashing, cleanup, long-running tools
Hash strategy differs by direction:
Inbound files may be referenced repeatedly within a session, so use a stable key — re-uploading the same file lands at the same key and the agent's reference doesn't break. A session-and-filename composite hash works.
Outbound artifacts are single-use, so use a fresh random key every time, or concurrent generations overwrite each other. Pattern:
<session-suffix>-<random-hex>.<ext>.
Two Crypto nodes in one of these workflows is usually deliberate, not a copy-paste error — one for the inbound stable hash, one for the outbound unique suffix.
Cleanup keeps the bill down. Object storage has lifecycle rules (auto-delete after 7–30 days). Drive-style backends need a scheduled cleanup workflow. For precise control, track live keys in a Data Table (the n8n_manage_datatable surface — see n8n-mcp-tools-expert) and delete unreferenced files.
Long-running tools (video generation, large batches): agent tool calls have no agent-layer timeout — a sub-workflow tool returns whenever it returns and the agent waits. The one real timeout is on the HTTP Request node itself (default ~5 minutes). If the tool is an HTTP Request Tool calling a slow external API, bump options.timeout past the expected duration, or the HTTP call aborts mid-job while the work keeps running and the agent gets nothing. Error-branch these steps so a failed upload or a storage 404 surfaces instead of vanishing — see n8n-error-handling.
Surface-specific seams
The examples above use the canvas Chat Trigger's conventions: $('Chat Trigger').first().json.files[] inbound, ![]() markdown outbound. These shapes are not universal. Production surfaces (Slack, Discord, Microsoft Teams, Telegram, WhatsApp Business, custom webhooks) each differ on:
Inbound file event shape — where the file lives in the trigger payload, and whether the file URL needs a bearer/bot token to download.
Outbound rendering — markdown image, Block Kit image block, adaptive card, Discord embed, or a dedicated file-upload API that pushes bytes natively.
Before wiring an inbound or outbound binary path on a real surface, check the platform's official API docs and the n8n node docs for two things: the exact path to the file in the trigger event (and whether downloading it needs auth), and the exact shape the platform expects for an image/file in a reply. Get those right and the patterns here carry over; guess from the canvas examples and the workflow ships looking correct, then fails on real messages.
Common mistakes
| Mistake | Consequence | Fix |
|---|---|---|
Passing binary through $fromAI() | Can't carry binary; tool gets nothing | Pass a key/URL, re-fetch on the other side |
| Forgetting to inject keys into the system prompt | Agent hallucinates names or refuses | List original + storage key, "use EXACTLY" |
| Skipping the Merge synchronization barrier | Agent fires before uploads finish; tool 404s | Merge the upload branch back before the agent |
Forgetting executeOnce: true when files split | N files → N agent runs → N replies | Set executeOnce: true on the agent |
Forgetting options.binaryPropertyOutput on provider AI nodes | Produced bytes don't land where upload looks | Set it explicitly on image/audio/video gen nodes |
| Public bucket for inbound user files | Privacy hole | Private bucket, session-scoped keys, short TTL |
| Returning binary in the tool response | Bloated context, some runtimes reject | Upload, return { key, url } |
Assuming passthroughBinaryImages feeds tools | Tools still get only $fromAI params | Use the upload-and-pass-key pattern |
| Default HTTP timeout on a slow generation endpoint | Call aborts mid-job, agent gets nothing | Bump options.timeout past expected duration |
Embedding video as ![]() | Broken thumbnail on most surfaces | Use [title](url) link form for video |
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Python, Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Romuald Czlonkowski
- Publisher
- Romuald Czlonkowski (Community)
- Source
- czlonkowski/n8n-skills · skills/n8n-binary-and-data · commit e037563
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Romuald Czlonkowski or Romuald Czlonkowski.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download n8n-binary-and-data.zip