Skip to content

Conversation

@michael-radency
Copy link
Contributor

@michael-radency michael-radency commented Dec 16, 2025

Summary

This PR introduces a new combined binary data mode for n8n workflows that changes how binary data is accessed and where it is included in items. Instead of storing binary data separately from the item's JSON data under a binary key, the combined mode automatically includes binary entries directly within the item's JSON data under a _files property, simplifying expressions for data access. Binary entries can be included at any level of nesting in the item's JSON data and can also be placed inside arrays. Items will be displayed in the UI in Schema and Table views with options to preview and download binary items.

image image image image

1. New Binary Data Mode

New Binary Mode Setting

  • Added BINARY_IN_JSON_PROPERTY = '_files' constant
  • New workflow setting: binaryMode: 'separate' | 'combined'
  • When binaryMode: 'combined', binary data is automatically converted to be accessible via $json._files instead of $binary

2. UI/UX Improvements

Binary Data Viewer Components

  • BinaryDataViewModal.vue: Modal for previewing binary files (images, videos, PDFs, JSON, HTML, markdown, text)
  • BinaryEntryDataTable.vue: Table row component for displaying binary file metadata with download and preview actions
  • VirtualSchemaItem.vue: Enhanced schema view to recognize and display binary data embedded in JSON

Workflow Settings

  • New execution logic selector with 3 modes:
    • v0 (legacy): Original execution order
    • v1 (recommended): Sequential branch execution
    • v2 (experimental): Sequential with combined binary mode enabled
  • Selecting v2 sets binaryMode: 'combined', which enables simplified expressions and inclusion of binaries into JSON

3. Expression Simplification

Workflow Data Proxy (packages/workflow/src/workflow-data-proxy.ts)

  • $item now behaves like $json and $data in combined mode, returning the JSON portion of items directly
  • $input.item, $input.first(), $input.last(), and $input.all() return JSON data directly in combined mode
  • $('nodeName').first(), $('nodeName').last(), $('nodeName').all(), and $('nodeName').pairedItem() return JSON data in combined mode
  • New optional fullItem parameter for $('nodeName', fullItem) to force returning full item structure even in combined mode
  • Maintains backward compatibility: separate mode returns full item structures as before

Code Node Autocomplete

  • New type declarations for combined mode: n8n-once-for-all-items-combined.d.ts, n8n-once-for-each-item-combined.d.ts
  • Autocomplete now omits the .json suggestion for item data access in combined mode
  • Suppresses await warnings, as it was falsely warning that await could not be used inside Code nodes

4. Backend Changes

Binary Data Conversion

  • Updated execution logic to process binaries based on the workflow's binaryMode setting
  • Added helper function convertBinaryData that moves binaries to the JSON _files property, automatically converting base64 data to entries based on filesystem mode

Helper Functions

  • Enhanced getBinaryDataBuffer() to work with both modes
  • assertBinaryData() now support binary data access by path from both binary and json properties

Related Linear tickets, Github issues, and Community forum posts

https://linear.app/n8n/issue/NODE-3854/add-binarydata-component-for-binaries-included-in-json-schema-view

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with release/backport (if the PR is an urgent fix that needs to be backported)

…add-binarydata-component-for-binaries-included-in-json-schema-view
…add-binarydata-component-for-binaries-included-in-json-schema-view
…add-binarydata-component-for-binaries-included-in-json-schema-view
node-3854-add-binarydata-component-for-binaries-included-in-json-schema-view
…add-binarydata-component-for-binaries-included-in-json-schema-view
…add-binarydata-component-for-binaries-included-in-json-schema-view
…add-binarydata-component-for-binaries-included-in-json-schema-view
…add-binarydata-component-for-binaries-included-in-json-schema-view
…add-binarydata-component-for-binaries-included-in-json-schema-view
… on default filesystem, binary data access by property fixes
…add-binarydata-component-for-binaries-included-in-json-schema-view
…add-binarydata-component-for-binaries-included-in-json-schema-view
…add-binarydata-component-for-binaries-included-in-json-schema-view
…add-binarydata-component-for-binaries-included-in-json-schema-view
…add-binarydata-component-for-binaries-included-in-json-schema-view
@blacksmith-sh

This comment has been minimized.

@blacksmith-sh

This comment has been minimized.

Copy link
Contributor

@dlavrenuek dlavrenuek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great and huge piece of work @michael-radency 💪 ! Thank you for implementing the bg improvements!

Adding my review so far, will continue tomorrow.

).rejects.toThrow('Provided parameter is not a string or binary data object.');
});

it('should throw error when path is undefined in combined mode', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name is a bit misleading because path is 'data.file' and also defined, but the value is undefined. May be something like?

Suggested change
it('should throw error when path is undefined in combined mode', async () => {
it('should throw error when path resolves to undefined in combined mode', async () => {

Comment on lines +795 to +801
it('should detect ASCII encoding', () => {
const asciiBuffer = Buffer.from('Simple ASCII text 123');
const encoding = detectBinaryEncoding(asciiBuffer);
expect(encoding).toBeDefined();
// ASCII is often detected as UTF-8, ASCII, or similar encodings
expect(['UTF-8', 'ASCII', 'ascii', 'windows-1252', 'ISO-8859-1']).toContain(encoding);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional suggestion: The test file is saved in utf8, so all text inside is also utf8. Ascii can be enforced the same way it is done in other tests with different encodings but there is also an easy way to just use Buffer.from().toString('ascii'). The used chardet library uses only uppercase for the charset. Also the readme does not state that it supports ascii, it obviously does support it as ASCII. Does the following also work on your machine?

Suggested change
it('should detect ASCII encoding', () => {
const asciiBuffer = Buffer.from('Simple ASCII text 123');
const encoding = detectBinaryEncoding(asciiBuffer);
expect(encoding).toBeDefined();
// ASCII is often detected as UTF-8, ASCII, or similar encodings
expect(['UTF-8', 'ASCII', 'ascii', 'windows-1252', 'ISO-8859-1']).toContain(encoding);
});
it('should detect ASCII encoding', () => {
const asciiBuffer = Buffer.from(Buffer.from('Simple ASCII text 123').toString('ascii'));
const encoding = detectBinaryEncoding(asciiBuffer);
expect(encoding).toBeDefined();
expect(encoding).toBe("ASCII");
});

});
});

describe('getBinaryDataBuffer with combined binary mode', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice test coverage 🏅

},
);
}
if (binaryMode === 'separate' || binaryMode === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this PR we will have two modes: "separate" and "combined". This condition suggests that "separate" is the default behavior if unset or specificly defined as "separate", otherwise it will be handled as "conbined". This is confusing in my opinion especially if the available modes are extended in the future.

The bahaviour should be more clear when the condition is reversed as

if (binaryMode === BINARY_MODE_COMBINED) {..} else {...}

or if used with switch instead of if:

switch (binaryMode) {
	case BINARY_MODE_COMBINED: { ... }
	case "separate":
    default: { ... }
}

What do you think?

} else {
const itemData = inputData.main[inputIndex]![itemIndex].json;
const binaryData = get(itemData, parameterData);
if (binaryData === undefined || !isBinaryValue(binaryData)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isBinaryValue already checks internally if the passed value is an object, so we can safely remove the check if it's undefined

Comment on lines +56 to +57
title: 'Error downloading file',
message: 'File could not be downloaded',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add these text to localization

Comment on lines +76 to +79
const isDownloadHovered = ref(false);
const downloadIconColor = computed(() => (isDownloadHovered.value ? 'primary' : 'text-base'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of complexity for a hover effect. We should simply use the CSS hover instead of using vue state+rendering logic.

Suggested change
const isDownloadHovered = ref(false);
const downloadIconColor = computed(() => (isDownloadHovered.value ? 'primary' : 'text-base'));

Comment on lines +104 to +111
<div
:class="$style.download"
@click="downloadBinaryData"
@mouseenter="isDownloadHovered = true"
@mouseleave="isDownloadHovered = false"
>
<N8nIcon icon="download" size="large" :color="downloadIconColor" />
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

color and hover color can be defined in the css class. Also I suggest using either a button or a link here to make it accessible

Suggested change
<div
:class="$style.download"
@click="downloadBinaryData"
@mouseenter="isDownloadHovered = true"
@mouseleave="isDownloadHovered = false"
>
<N8nIcon icon="download" size="large" :color="downloadIconColor" />
</div>
<a :class="$style.download" @click="downloadBinaryData">
<N8nIcon icon="download" size="large" />
</a>

Comment on lines +200 to +203
cursor: pointer;
flex-shrink: 0;
opacity: 0;
transition: opacity 0.2s ease-in-out;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a button or link, cursor is not needed. To keep the colors as you had them following will work:

Suggested change
cursor: pointer;
flex-shrink: 0;
opacity: 0;
transition: opacity 0.2s ease-in-out;
flex-shrink: 0;
opacity: 0;
transition: all 0.2s ease-in-out;
color: var(--color-text-base);
&:hover {
color: var(--color--primary--shade-1);
}

It would be even better to use a separate component "icon button" if such one exists

@@ -1,16 +1,35 @@
<script setup lang="ts">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: continue review here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

n8n team Authored by the n8n team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants