Repository: n8n-io/n8n
Pull request #13507: feat(n8n Form Trigger Node): Respond with File
Author: login: dana-gill
Labels: name: node/improvement
description: New feature or request
name: core
description: Enhancement outside /nodes-base and /editor-ui
name: n8n team
description: Authored by the n8n team
name: Released
Created: 2025-02-25T17:23:31Z | Merged: 2025-03-19T11:56:40Z


## Summary

Allow users to respond to the completion of a form with a binary file, which gets automatically downloaded by the end user.

## Related Linear tickets, Github issues, and Community forum posts

https://linear.app/n8n/issue/NODE-2342/respond-with-file


## Review / Merge checklist

- [ ] PR title and summary are descriptive. ([conventions](../blob/master/.github/pull_request_title_conventions.md)) <!--
   **Remember, the title automatically goes into the changelog.
   Use `(no-changelog)` otherwise.**
-->
- [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created.
- [x] Tests included. <!--
   A bug is not considered fixed, unless a test is added to prevent it from happening again.
   A feature is not complete without tests.
-->
- [x] PR Labeled with `release/backport` (if the PR is an urgent fix that needs to be backported)



Diff:
diff --git a/packages/cli/templates/form-trigger-completion.handlebars b/packages/cli/templates/form-trigger-completion.handlebars
index 55f43fa990c85..aa9200ae160e4 100644
--- a/packages/cli/templates/form-trigger-completion.handlebars
+++ b/packages/cli/templates/form-trigger-completion.handlebars
@@ -157,6 +157,31 @@
 			<a id='redirectUrl' href='{{redirectUrl}}' style='display: none;'></a>
 		{{/if}}
 	<script>
+		document.addEventListener('DOMContentLoaded', function () {
+			const binary = "{{{responseBinary}}}"
+				?	JSON.parse(decodeURIComponent("{{{responseBinary}}}"))
+				: '';
+
+			const byteArray = binary.data.type === 'Buffer'
+				? new Uint8Array(binary.data.data)
+				: Uint8Array.from(binary.data, c => c.charCodeAt(0));
+
+			if (binary) {
+				const blob = new Blob(
+					[byteArray],
+					{ type: binary.type }
+				);
+
+				const link = document.createElement("a");
+				link.href = URL.createObjectURL(blob);
+				link.download = binary.fileName;
+				document.body.appendChild(link);
+
+				link.click();
+				document.body.removeChild(link);
+			}
+		});
+
 			fetch('', {
 					method: 'POST',
 					body: {}
diff --git a/packages/nodes-base/nodes/Form/Form.node.ts b/packages/nodes-base/nodes/Form/Form.node.ts
index 5e77b01ca1eb0..4351559efaec9 100644
--- a/packages/nodes-base/nodes/Form/Form.node.ts
+++ b/packages/nodes-base/nodes/Form/Form.node.ts
@@ -153,6 +153,11 @@ const completionProperties = updateDisplayOptions(
 					value: 'showText',
 					description: 'Display simple text or HTML',
 				},
+				{
+					name: 'Return Binary File',
+					value: 'returnBinary',
+					description: 'Return incoming binary file',
+				},
 			],
 		},
 		{
@@ -176,7 +181,7 @@ const completionProperties = updateDisplayOptions(
 			required: true,
 			displayOptions: {
 				show: {
-					respondWith: ['text'],
+					respondWith: ['text', 'returnBinary'],
 				},
 			},
 		},
@@ -190,7 +195,7 @@ const completionProperties = updateDisplayOptions(
 			},
 			displayOptions: {
 				show: {
-					respondWith: ['text'],
+					respondWith: ['text', 'returnBinary'],
 				},
 			},
 		},
@@ -210,6 +215,21 @@ const completionProperties = updateDisplayOptions(
 			placeholder: 'e.g. Thanks for filling the form',
 			description: 'The text to display on the page. Use HTML to show a customized web page.',
 		},
+		{
+			displayName: 'Input Data Field Name',
+			name: 'inputDataFieldName',
+			type: 'string',
+			displayOptions: {
+				show: {
+					respondWith: ['returnBinary'],
+				},
+			},
+			default: 'data',
+			placeholder: 'e.g. data',
+			description:
+				'Find the name of input field containing the binary data to return in the Input panel on the left, in the Binary tab',
+			hint: 'The name of the input field containing the binary file data to be returned',
+		},
 		...waitTimeProperties,
 		{
 			displayName: 'Options',
@@ -233,7 +253,7 @@ const completionProperties = updateDisplayOptions(
 			],
 			displayOptions: {
 				show: {
-					respondWith: ['text'],
+					respondWith: ['text', 'returnBinary'],
 				},
 			},
 		},
diff --git a/packages/nodes-base/nodes/Form/formCompletionUtils.ts b/packages/nodes-base/nodes/Form/formCompletionUtils.ts
index 9f1e6e3f0db6b..b794b5605de20 100644
--- a/packages/nodes-base/nodes/Form/formCompletionUtils.ts
+++ b/packages/nodes-base/nodes/Form/formCompletionUtils.ts
@@ -3,10 +3,43 @@ import {
 	type NodeTypeAndVersion,
 	type IWebhookFunctions,
 	type IWebhookResponseData,
+	type IBinaryData,
+	type IDataObject,
+	OperationalError,
 } from 'n8n-workflow';
 
 import { sanitizeCustomCss, sanitizeHtml } from './utils';
 
+const getBinaryDataFromNode = (context: IWebhookFunctions, nodeName: string): IDataObject => {
+	return context.evaluateExpression(`{{ $('${nodeName}').first().binary }}`) as IDataObject;
+};
+
+export const binaryResponse = async (
+	context: IWebhookFunctions,
+): Promise<{ data: string | Buffer; fileName: string; type: string }> => {
+	const inputDataFieldName = context.getNodeParameter('inputDataFieldName', '') as string;
+	const parentNodes = context.getParentNodes(context.getNode().name);
+	const binaryNode = parentNodes.find((node) =>
+		getBinaryDataFromNode(context, node?.name)?.hasOwnProperty(inputDataFieldName),
+	);
+	if (!binaryNode) {
+		throw new OperationalError(`No binary data with field ${inputDataFieldName} found.`);
+	}
+	const binaryData = getBinaryDataFromNode(context, binaryNode?.name)[
+		inputDataFieldName
+	] as IBinaryData;
+
+	return {
+		// If a binaryData has an id, the following field is set:
+		// N8N_DEFAULT_BINARY_DATA_MODE=filesystem
+		data: binaryData.id
+			? await context.helpers.binaryToBuffer(await context.helpers.getBinaryStream(binaryData.id))
+			: atob(binaryData.data),
+		fileName: binaryData.fileName ?? 'file',
+		type: binaryData.mimeType,
+	};
+};
+
 export const renderFormCompletion = async (
 	context: IWebhookFunctions,
 	res: Response,
@@ -20,6 +53,10 @@ export const renderFormCompletion = async (
 		customCss?: string;
 	};
 	const responseText = context.getNodeParameter('responseText', '') as string;
+	const binary =
+		context.getNodeParameter('respondWith', '') === 'returnBinary'
+			? await binaryResponse(context)
+			: '';
 
 	let title = options.formTitle;
 	if (!title) {
@@ -35,6 +72,7 @@ export const renderFormCompletion = async (
 		formTitle: title,
 		appendAttribution,
 		responseText: sanitizeHtml(responseText),
+		responseBinary: encodeURIComponent(JSON.stringify(binary)),
 		dangerousCustomCss: sanitizeCustomCss(options.customCss),
 		redirectUrl,
 	});
diff --git a/packages/nodes-base/nodes/Form/test/Form.node.test.ts b/packages/nodes-base/nodes/Form/test/Form.node.test.ts
index 5998f2d20adf1..1a097f52ac1c4 100644
--- a/packages/nodes-base/nodes/Form/test/Form.node.test.ts
+++ b/packages/nodes-base/nodes/Form/test/Form.node.test.ts
@@ -232,6 +232,7 @@ describe('Form Node', () => {
 						message: 'Test Message',
 						redirectUrl: '',
 						title: 'Test Title',
+						responseBinary: encodeURIComponent(JSON.stringify('')),
 						responseText: '',
 					},
 				},
@@ -246,6 +247,7 @@ describe('Form Node', () => {
 						redirectUrl: '',
 						title: 'Test Title',
 						responseText: '<div>hey</div>',
+						responseBinary: encodeURIComponent(JSON.stringify('')),
 					},
 				},
 				{
@@ -257,6 +259,7 @@ describe('Form Node', () => {
 						formTitle: 'test',
 						message: 'Test Message',
 						redirectUrl: '',
+						responseBinary: encodeURIComponent(JSON.stringify('')),
 						title: 'Test Title',
 						responseText: 'my text over here',
 					},
@@ -434,6 +437,7 @@ describe('Form Node', () => {
 				redirectUrl: 'https://n8n.io',
 				responseText: '',
 				title: 'Test Title',
+				responseBinary: encodeURIComponent(JSON.stringify('')),
 			});
 		});
 	});
diff --git a/packages/nodes-base/nodes/Form/test/formCompletionUtils.test.ts b/packages/nodes-base/nodes/Form/test/formCompletionUtils.test.ts
new file mode 100644
index 0000000000000..a3ef06bf9aecb
--- /dev/null
+++ b/packages/nodes-base/nodes/Form/test/formCompletionUtils.test.ts
@@ -0,0 +1,254 @@
+import { type Response } from 'express';
+import { type MockProxy, mock } from 'jest-mock-extended';
+import { type INode, type IWebhookFunctions } from 'n8n-workflow';
+
+import { renderFormCompletion } from '../formCompletionUtils';
+
+describe('formCompletionUtils', () => {
+	let mockWebhookFunctions: MockProxy<IWebhookFunctions>;
+
+	const mockNode: INode = mock<INode>({
+		id: 'test-node',
+		name: 'Test Node',
+		type: 'test',
+		typeVersion: 1,
+		position: [0, 0],
+		parameters: {},
+	});
+
+	const nodeNameWithFileToDownload = 'prevNode0';
+	const nodeNameWithFile = 'prevNode2';
+
+	const parentNodesWithAndWithoutFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: 'prevNode1',
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithMultipleBinaryFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: nodeNameWithFile,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithSingleNodeFile = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesTestCases = [
+		parentNodesWithAndWithoutFiles,
+		parentNodesWithMultipleBinaryFiles,
+		parentNodesWithSingleNodeFile,
+	];
+
+	beforeEach(() => {
+		mockWebhookFunctions = mock<IWebhookFunctions>();
+
+		mockWebhookFunctions.getNode.mockReturnValue(mockNode);
+	});
+
+	afterEach(() => {
+		jest.resetAllMocks();
+	});
+
+	describe('renderFormCompletion', () => {
+		const mockResponse: Response = mock<Response>({
+			send: jest.fn(),
+			render: jest.fn(),
+		});
+
+		const trigger = {
+			name: 'triggerNode',
+			type: 'trigger',
+			typeVersion: 1,
+			disabled: false,
+		};
+
+		afterEach(() => {
+			jest.resetAllMocks();
+		});
+
+		it('should render the form completion', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+				};
+				return params[parameterName];
+			});
+
+			await renderFormCompletion(mockWebhookFunctions, mockResponse, trigger);
+
+			expect(mockResponse.render).toHaveBeenCalledWith('form-trigger-completion', {
+				appendAttribution: undefined,
+				formTitle: 'Form Title',
+				message: 'Form has been submitted successfully',
+				redirectUrl: undefined,
+				responseBinary: encodeURIComponent(JSON.stringify('')),
+				responseText: '',
+				title: 'Form Completion',
+			});
+		});
+
+		it('throw an error if no binary data with the field name is found', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+					respondWith: 'returnBinary',
+					inputDataFieldName: 'inputData',
+				};
+				return params[parameterName];
+			});
+			mockWebhookFunctions.getParentNodes.mockReturnValueOnce([]);
+
+			await expect(
+				renderFormCompletion(mockWebhookFunctions, mockResponse, trigger),
+			).rejects.toThrowError('No binary data with field inputData found.');
+		});
+
+		it('should render if respond with binary is set and binary mode is filesystem', async () => {
+			const expectedBinaryResponse = {
+				inputData: {
+					data: 'IyAxLiBHbyBpbiBwb3N0Z3',
+					fileExtension: 'txt',
+					fileName: 'file.txt',
+					fileSize: '458 B',
+					fileType: 'text',
+					mimeType: 'text/plain',
+					id: 555,
+				},
+			};
+
+			const buffer = Buffer.from(expectedBinaryResponse.inputData.data);
+
+			for (const parentNodes of parentNodesTestCases) {
+				mockWebhookFunctions.getParentNodes.mockReturnValueOnce(parentNodes);
+				mockWebhookFunctions.evaluateExpression.mockImplementation((arg) => {
+					if (arg === `{{ $('${nodeNameWithFileToDownload}').first().binary }}`) {
+						return expectedBinaryResponse;
+					} else if (arg === `{{ $('${nodeNameWithFile}').first().binary }}`) {
+						return { someData: {} };
+					} else {
+						return undefined;
+					}
+				});
+				mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+					const params: { [key: string]: any } = {
+						inputDataFieldName: 'inputData',
+						completionTitle: 'Form Completion',
+						completionMessage: 'Form has been submitted successfully',
+						options: { formTitle: 'Form Title' },
+						respondWith: 'returnBinary',
+					};
+					return params[parameterName];
+				});
+
+				mockWebhookFunctions.helpers.getBinaryStream = jest
+					.fn()
+					.mockResolvedValue(Promise.resolve({}));
+
+				mockWebhookFunctions.helpers.binaryToBuffer = jest
+					.fn()
+					.mockResolvedValue(Promise.resolve(buffer));
+
+				await renderFormCompletion(mockWebhookFunctions, mockResponse, trigger);
+
+				expect(mockResponse.render).toHaveBeenCalledWith('form-trigger-completion', {
+					appendAttribution: undefined,
+					formTitle: 'Form Title',
+					message: 'Form has been submitted successfully',
+					redirectUrl: undefined,
+					responseBinary: encodeURIComponent(
+						JSON.stringify({
+							data: buffer,
+							fileName: expectedBinaryResponse.inputData.fileName,
+							type: expectedBinaryResponse.inputData.mimeType,
+						}),
+					),
+					responseText: '',
+					title: 'Form Completion',
+				});
+			}
+		});
+
+		it('should render if respond with binary is set and binary mode is default', async () => {
+			const expectedBinaryResponse = {
+				inputData: {
+					data: 'IyAxLiBHbyBpbiBwb3N0Z3',
+					fileExtension: 'txt',
+					fileName: 'file.txt',
+					fileSize: '458 B',
+					fileType: 'text',
+					mimeType: 'text/plain',
+				},
+			};
+
+			for (const parentNodes of parentNodesTestCases) {
+				mockWebhookFunctions.getParentNodes.mockReturnValueOnce(parentNodes);
+				mockWebhookFunctions.evaluateExpression.mockImplementation((arg) => {
+					if (arg === `{{ $('${nodeNameWithFileToDownload}').first().binary }}`) {
+						return expectedBinaryResponse;
+					} else if (arg === `{{ $('${nodeNameWithFile}').first().binary }}`) {
+						return { someData: {} };
+					} else {
+						return undefined;
+					}
+				});
+				mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+					const params: { [key: string]: any } = {
+						inputDataFieldName: 'inputData',
+						completionTitle: 'Form Completion',
+						completionMessage: 'Form has been submitted successfully',
+						options: { formTitle: 'Form Title' },
+						respondWith: 'returnBinary',
+					};
+					return params[parameterName];
+				});
+
+				await renderFormCompletion(mockWebhookFunctions, mockResponse, trigger);
+
+				expect(mockResponse.render).toHaveBeenCalledWith('form-trigger-completion', {
+					appendAttribution: undefined,
+					formTitle: 'Form Title',
+					message: 'Form has been submitted successfully',
+					redirectUrl: undefined,
+					responseBinary: encodeURIComponent(
+						JSON.stringify({
+							data: atob(expectedBinaryResponse.inputData.data),
+							fileName: expectedBinaryResponse.inputData.fileName,
+							type: expectedBinaryResponse.inputData.mimeType,
+						}),
+					),
+					responseText: '',
+					title: 'Form Completion',
+				});
+			}
+		});
+	});
+});


Reviews:
user: login: ShireenMissi
body: I tested locally with N8N_DEFAULT_BINARY_DATA_MODE set to filesystem and default
it works 👌
left some questions apart from that looks good 👏
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T17:30:37Z
user: login: dana-gill
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T17:33:09Z
user: login: dana-gill
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T17:33:33Z
user: login: dana-gill
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T17:35:21Z
user: login: netroy
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T10:35:36Z
user: login: ShireenMissi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T13:26:06Z
user: login: ShireenMissi
body: LGTM 🎉
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T11:33:40Z

Review comments:
diff_hunk: @@ -0,0 +1,254 @@
+import { type Response } from 'express';
+import { type MockProxy, mock } from 'jest-mock-extended';
+import { type INode, type IWebhookFunctions } from 'n8n-workflow';
+
+import { renderFormCompletion } from '../formCompletionUtils';
+
+describe('formCompletionUtils', () => {
+	let mockWebhookFunctions: MockProxy<IWebhookFunctions>;
+
+	const mockNode: INode = mock<INode>({
+		id: 'test-node',
+		name: 'Test Node',
+		type: 'test',
+		typeVersion: 1,
+		position: [0, 0],
+		parameters: {},
+	});
+
+	const nodeNameWithFileToDownload = 'prevNode0';
+	const nodeNameWithFile = 'prevNode2';
+
+	const parentNodesWithAndWithoutFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: 'prevNode1',
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithMultipleBinaryFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: nodeNameWithFile,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithSingleNodeFile = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesTestCases = [
+		parentNodesWithAndWithoutFiles,
+		parentNodesWithMultipleBinaryFiles,
+		parentNodesWithSingleNodeFile,
+	];
+
+	beforeEach(() => {
+		mockWebhookFunctions = mock<IWebhookFunctions>();
+
+		mockWebhookFunctions.getNode.mockReturnValue(mockNode);
+	});
+
+	afterEach(() => {
+		jest.resetAllMocks();
+	});
+
+	describe('renderFormCompletion', () => {
+		const mockResponse: Response = mock<Response>({
+			send: jest.fn(),
+			render: jest.fn(),
+		});
+
+		const trigger = {
+			name: 'triggerNode',
+			type: 'trigger',
+			typeVersion: 1,
+			disabled: false,
+		};
+
+		afterEach(() => {
+			jest.resetAllMocks();
+		});
+
+		it('should render the form completion', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+				};
+				return params[parameterName];
+			});
+
+			await renderFormCompletion(mockWebhookFunctions, mockResponse, trigger);
+
+			expect(mockResponse.render).toHaveBeenCalledWith('form-trigger-completion', {
+				appendAttribution: undefined,
+				formTitle: 'Form Title',
+				message: 'Form has been submitted successfully',
+				redirectUrl: undefined,
+				responseBinary: encodeURIComponent(JSON.stringify('')),
+				responseText: '',
+				title: 'Form Completion',
+			});
+		});
+
+		it('throw an error if no binary data with the field name is found', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+					respondWith: 'returnBinary',
+					inputDataFieldName: 'inputData',
+				};
+				return params[parameterName];
+			});
+			mockWebhookFunctions.getParentNodes.mockReturnValueOnce([]);
+
+			await expect(
+				renderFormCompletion(mockWebhookFunctions, mockResponse, trigger),
+			).rejects.toThrowError('No binary data with field inputData found.');
+		});
+
+		it('should render if respond with binary is set and binary mode is filesystem', async () => {
path: packages/nodes-base/nodes/Form/test/formCompletionUtils.test.ts
user: login: ShireenMissi
body: Are you mocking the environment variable somewhere?
created_at: 2025-03-14T17:27:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,254 @@
+import { type Response } from 'express';
+import { type MockProxy, mock } from 'jest-mock-extended';
+import { type INode, type IWebhookFunctions } from 'n8n-workflow';
+
+import { renderFormCompletion } from '../formCompletionUtils';
+
+describe('formCompletionUtils', () => {
+	let mockWebhookFunctions: MockProxy<IWebhookFunctions>;
+
+	const mockNode: INode = mock<INode>({
+		id: 'test-node',
+		name: 'Test Node',
+		type: 'test',
+		typeVersion: 1,
+		position: [0, 0],
+		parameters: {},
+	});
+
+	const nodeNameWithFileToDownload = 'prevNode0';
+	const nodeNameWithFile = 'prevNode2';
+
+	const parentNodesWithAndWithoutFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: 'prevNode1',
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithMultipleBinaryFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: nodeNameWithFile,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithSingleNodeFile = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesTestCases = [
+		parentNodesWithAndWithoutFiles,
+		parentNodesWithMultipleBinaryFiles,
+		parentNodesWithSingleNodeFile,
+	];
+
+	beforeEach(() => {
+		mockWebhookFunctions = mock<IWebhookFunctions>();
+
+		mockWebhookFunctions.getNode.mockReturnValue(mockNode);
+	});
+
+	afterEach(() => {
+		jest.resetAllMocks();
+	});
+
+	describe('renderFormCompletion', () => {
+		const mockResponse: Response = mock<Response>({
+			send: jest.fn(),
+			render: jest.fn(),
+		});
+
+		const trigger = {
+			name: 'triggerNode',
+			type: 'trigger',
+			typeVersion: 1,
+			disabled: false,
+		};
+
+		afterEach(() => {
+			jest.resetAllMocks();
+		});
+
+		it('should render the form completion', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+				};
+				return params[parameterName];
+			});
+
+			await renderFormCompletion(mockWebhookFunctions, mockResponse, trigger);
+
+			expect(mockResponse.render).toHaveBeenCalledWith('form-trigger-completion', {
+				appendAttribution: undefined,
+				formTitle: 'Form Title',
+				message: 'Form has been submitted successfully',
+				redirectUrl: undefined,
+				responseBinary: encodeURIComponent(JSON.stringify('')),
+				responseText: '',
+				title: 'Form Completion',
+			});
+		});
+
+		it('throw an error if no binary data with the field name is found', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+					respondWith: 'returnBinary',
+					inputDataFieldName: 'inputData',
+				};
+				return params[parameterName];
+			});
+			mockWebhookFunctions.getParentNodes.mockReturnValueOnce([]);
+
+			await expect(
+				renderFormCompletion(mockWebhookFunctions, mockResponse, trigger),
+			).rejects.toThrowError('No binary data with field inputData found.');
+		});
+
+		it('should render if respond with binary is set and binary mode is filesystem', async () => {
+			const expectedBinaryResponse = {
+				inputData: {
+					data: 'IyAxLiBHbyBpbiBwb3N0Z3',
+					fileExtension: 'txt',
+					fileName: 'file.txt',
+					fileSize: '458 B',
+					fileType: 'text',
+					mimeType: 'text/plain',
+					id: 555,
+				},
+			};
+
+			const buffer = Buffer.from(expectedBinaryResponse.inputData.data);
+
+			for (const parentNodes of parentNodesTestCases) {
+				mockWebhookFunctions.getParentNodes.mockReturnValueOnce(parentNodes);
+				mockWebhookFunctions.evaluateExpression.mockImplementation((arg) => {
+					if (arg === `{{ $('${nodeNameWithFileToDownload}').first().binary }}`) {
+						return expectedBinaryResponse;
+					} else if (arg === `{{ $('${nodeNameWithFile}').first().binary }}`) {
+						return { someData: {} };
+					} else {
+						return undefined;
+					}
+				});
+				mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+					const params: { [key: string]: any } = {
+						inputDataFieldName: 'inputData',
+						completionTitle: 'Form Completion',
+						completionMessage: 'Form has been submitted successfully',
+						options: { formTitle: 'Form Title' },
+						respondWith: 'returnBinary',
+					};
+					return params[parameterName];
+				});
+
+				mockWebhookFunctions.helpers.getBinaryStream = jest
+					.fn()
+					.mockResolvedValue(Promise.resolve({}));
+
+				mockWebhookFunctions.helpers.binaryToBuffer = jest
+					.fn()
+					.mockResolvedValue(Promise.resolve(buffer));
+
+				await renderFormCompletion(mockWebhookFunctions, mockResponse, trigger);
+
+				expect(mockResponse.render).toHaveBeenCalledWith('form-trigger-completion', {
+					appendAttribution: undefined,
+					formTitle: 'Form Title',
+					message: 'Form has been submitted successfully',
+					redirectUrl: undefined,
+					responseBinary: encodeURIComponent(
+						JSON.stringify({
+							data: buffer,
+							fileName: expectedBinaryResponse.inputData.fileName,
+							type: expectedBinaryResponse.inputData.mimeType,
+						}),
+					),
+					responseText: '',
+					title: 'Form Completion',
+				});
+			}
+		});
+
+		it('should render if respond with binary is set and binary mode is default', async () => {
path: packages/nodes-base/nodes/Form/test/formCompletionUtils.test.ts
user: login: ShireenMissi
body: same here
created_at: 2025-03-14T17:28:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,10 +3,43 @@ import {
 	type NodeTypeAndVersion,
 	type IWebhookFunctions,
 	type IWebhookResponseData,
+	type IBinaryData,
+	type IDataObject,
+	OperationalError,
 } from 'n8n-workflow';
 
 import { sanitizeCustomCss, sanitizeHtml } from './utils';
 
+const getBinaryDataFromNode = (context: IWebhookFunctions, nodeName: string): IDataObject => {
+	return context.evaluateExpression(`{{ $('${nodeName}').first().binary }}`) as IDataObject;
+};
+
+export const binaryResponse = async (
+	context: IWebhookFunctions,
+): Promise<{ data: string | Buffer; fileName: string; type: string }> => {
+	const inputDataFieldName = context.getNodeParameter('inputDataFieldName', '') as string;
+	const parentNodes = context.getParentNodes(context.getNode().name);
+	const binaryNode = parentNodes.find((node) =>
+		getBinaryDataFromNode(context, node?.name)?.hasOwnProperty(inputDataFieldName),
+	);
+	if (!binaryNode) {
+		throw new OperationalError(`No binary data with field ${inputDataFieldName} found.`);
+	}
+	const binaryData = getBinaryDataFromNode(context, binaryNode?.name)[
+		inputDataFieldName
+	] as IBinaryData;
+
+	return {
+		// If a binaryData has an id, the following field is set:
+		// N8N_DEFAULT_BINARY_DATA_MODE=filesystem
path: packages/nodes-base/nodes/Form/formCompletionUtils.ts
user: login: ShireenMissi
body: I am not sure I understand the comment, is there a different implementation based on N8N_DEFAULT_BINARY_DATA_MODE? 
created_at: 2025-03-14T17:29:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,254 @@
+import { type Response } from 'express';
+import { type MockProxy, mock } from 'jest-mock-extended';
+import { type INode, type IWebhookFunctions } from 'n8n-workflow';
+
+import { renderFormCompletion } from '../formCompletionUtils';
+
+describe('formCompletionUtils', () => {
+	let mockWebhookFunctions: MockProxy<IWebhookFunctions>;
+
+	const mockNode: INode = mock<INode>({
+		id: 'test-node',
+		name: 'Test Node',
+		type: 'test',
+		typeVersion: 1,
+		position: [0, 0],
+		parameters: {},
+	});
+
+	const nodeNameWithFileToDownload = 'prevNode0';
+	const nodeNameWithFile = 'prevNode2';
+
+	const parentNodesWithAndWithoutFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: 'prevNode1',
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithMultipleBinaryFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: nodeNameWithFile,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithSingleNodeFile = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesTestCases = [
+		parentNodesWithAndWithoutFiles,
+		parentNodesWithMultipleBinaryFiles,
+		parentNodesWithSingleNodeFile,
+	];
+
+	beforeEach(() => {
+		mockWebhookFunctions = mock<IWebhookFunctions>();
+
+		mockWebhookFunctions.getNode.mockReturnValue(mockNode);
+	});
+
+	afterEach(() => {
+		jest.resetAllMocks();
+	});
+
+	describe('renderFormCompletion', () => {
+		const mockResponse: Response = mock<Response>({
+			send: jest.fn(),
+			render: jest.fn(),
+		});
+
+		const trigger = {
+			name: 'triggerNode',
+			type: 'trigger',
+			typeVersion: 1,
+			disabled: false,
+		};
+
+		afterEach(() => {
+			jest.resetAllMocks();
+		});
+
+		it('should render the form completion', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+				};
+				return params[parameterName];
+			});
+
+			await renderFormCompletion(mockWebhookFunctions, mockResponse, trigger);
+
+			expect(mockResponse.render).toHaveBeenCalledWith('form-trigger-completion', {
+				appendAttribution: undefined,
+				formTitle: 'Form Title',
+				message: 'Form has been submitted successfully',
+				redirectUrl: undefined,
+				responseBinary: encodeURIComponent(JSON.stringify('')),
+				responseText: '',
+				title: 'Form Completion',
+			});
+		});
+
+		it('throw an error if no binary data with the field name is found', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+					respondWith: 'returnBinary',
+					inputDataFieldName: 'inputData',
+				};
+				return params[parameterName];
+			});
+			mockWebhookFunctions.getParentNodes.mockReturnValueOnce([]);
+
+			await expect(
+				renderFormCompletion(mockWebhookFunctions, mockResponse, trigger),
+			).rejects.toThrowError('No binary data with field inputData found.');
+		});
+
+		it('should render if respond with binary is set and binary mode is filesystem', async () => {
path: packages/nodes-base/nodes/Form/test/formCompletionUtils.test.ts
user: login: dana-gill
body: No because I'm not sure how I would do that. The indicator that the binary mode is filesystem is if the binary object that we get from a node has an `id` to it. So this test has a binary object with an `id` set.
created_at: 2025-03-14T17:33:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,254 @@
+import { type Response } from 'express';
+import { type MockProxy, mock } from 'jest-mock-extended';
+import { type INode, type IWebhookFunctions } from 'n8n-workflow';
+
+import { renderFormCompletion } from '../formCompletionUtils';
+
+describe('formCompletionUtils', () => {
+	let mockWebhookFunctions: MockProxy<IWebhookFunctions>;
+
+	const mockNode: INode = mock<INode>({
+		id: 'test-node',
+		name: 'Test Node',
+		type: 'test',
+		typeVersion: 1,
+		position: [0, 0],
+		parameters: {},
+	});
+
+	const nodeNameWithFileToDownload = 'prevNode0';
+	const nodeNameWithFile = 'prevNode2';
+
+	const parentNodesWithAndWithoutFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: 'prevNode1',
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithMultipleBinaryFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: nodeNameWithFile,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithSingleNodeFile = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesTestCases = [
+		parentNodesWithAndWithoutFiles,
+		parentNodesWithMultipleBinaryFiles,
+		parentNodesWithSingleNodeFile,
+	];
+
+	beforeEach(() => {
+		mockWebhookFunctions = mock<IWebhookFunctions>();
+
+		mockWebhookFunctions.getNode.mockReturnValue(mockNode);
+	});
+
+	afterEach(() => {
+		jest.resetAllMocks();
+	});
+
+	describe('renderFormCompletion', () => {
+		const mockResponse: Response = mock<Response>({
+			send: jest.fn(),
+			render: jest.fn(),
+		});
+
+		const trigger = {
+			name: 'triggerNode',
+			type: 'trigger',
+			typeVersion: 1,
+			disabled: false,
+		};
+
+		afterEach(() => {
+			jest.resetAllMocks();
+		});
+
+		it('should render the form completion', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+				};
+				return params[parameterName];
+			});
+
+			await renderFormCompletion(mockWebhookFunctions, mockResponse, trigger);
+
+			expect(mockResponse.render).toHaveBeenCalledWith('form-trigger-completion', {
+				appendAttribution: undefined,
+				formTitle: 'Form Title',
+				message: 'Form has been submitted successfully',
+				redirectUrl: undefined,
+				responseBinary: encodeURIComponent(JSON.stringify('')),
+				responseText: '',
+				title: 'Form Completion',
+			});
+		});
+
+		it('throw an error if no binary data with the field name is found', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+					respondWith: 'returnBinary',
+					inputDataFieldName: 'inputData',
+				};
+				return params[parameterName];
+			});
+			mockWebhookFunctions.getParentNodes.mockReturnValueOnce([]);
+
+			await expect(
+				renderFormCompletion(mockWebhookFunctions, mockResponse, trigger),
+			).rejects.toThrowError('No binary data with field inputData found.');
+		});
+
+		it('should render if respond with binary is set and binary mode is filesystem', async () => {
+			const expectedBinaryResponse = {
+				inputData: {
+					data: 'IyAxLiBHbyBpbiBwb3N0Z3',
+					fileExtension: 'txt',
+					fileName: 'file.txt',
+					fileSize: '458 B',
+					fileType: 'text',
+					mimeType: 'text/plain',
+					id: 555,
+				},
+			};
+
+			const buffer = Buffer.from(expectedBinaryResponse.inputData.data);
+
+			for (const parentNodes of parentNodesTestCases) {
+				mockWebhookFunctions.getParentNodes.mockReturnValueOnce(parentNodes);
+				mockWebhookFunctions.evaluateExpression.mockImplementation((arg) => {
+					if (arg === `{{ $('${nodeNameWithFileToDownload}').first().binary }}`) {
+						return expectedBinaryResponse;
+					} else if (arg === `{{ $('${nodeNameWithFile}').first().binary }}`) {
+						return { someData: {} };
+					} else {
+						return undefined;
+					}
+				});
+				mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+					const params: { [key: string]: any } = {
+						inputDataFieldName: 'inputData',
+						completionTitle: 'Form Completion',
+						completionMessage: 'Form has been submitted successfully',
+						options: { formTitle: 'Form Title' },
+						respondWith: 'returnBinary',
+					};
+					return params[parameterName];
+				});
+
+				mockWebhookFunctions.helpers.getBinaryStream = jest
+					.fn()
+					.mockResolvedValue(Promise.resolve({}));
+
+				mockWebhookFunctions.helpers.binaryToBuffer = jest
+					.fn()
+					.mockResolvedValue(Promise.resolve(buffer));
+
+				await renderFormCompletion(mockWebhookFunctions, mockResponse, trigger);
+
+				expect(mockResponse.render).toHaveBeenCalledWith('form-trigger-completion', {
+					appendAttribution: undefined,
+					formTitle: 'Form Title',
+					message: 'Form has been submitted successfully',
+					redirectUrl: undefined,
+					responseBinary: encodeURIComponent(
+						JSON.stringify({
+							data: buffer,
+							fileName: expectedBinaryResponse.inputData.fileName,
+							type: expectedBinaryResponse.inputData.mimeType,
+						}),
+					),
+					responseText: '',
+					title: 'Form Completion',
+				});
+			}
+		});
+
+		it('should render if respond with binary is set and binary mode is default', async () => {
path: packages/nodes-base/nodes/Form/test/formCompletionUtils.test.ts
user: login: dana-gill
body: No because I'm not sure how I would do that. The indicator that the binary mode is filesystem is if the binary object that we get from a node has an `id` to it. So in these tests, I do not have an `id` in the expected binary object
created_at: 2025-03-14T17:33:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,10 +3,43 @@ import {
 	type NodeTypeAndVersion,
 	type IWebhookFunctions,
 	type IWebhookResponseData,
+	type IBinaryData,
+	type IDataObject,
+	OperationalError,
 } from 'n8n-workflow';
 
 import { sanitizeCustomCss, sanitizeHtml } from './utils';
 
+const getBinaryDataFromNode = (context: IWebhookFunctions, nodeName: string): IDataObject => {
+	return context.evaluateExpression(`{{ $('${nodeName}').first().binary }}`) as IDataObject;
+};
+
+export const binaryResponse = async (
+	context: IWebhookFunctions,
+): Promise<{ data: string | Buffer; fileName: string; type: string }> => {
+	const inputDataFieldName = context.getNodeParameter('inputDataFieldName', '') as string;
+	const parentNodes = context.getParentNodes(context.getNode().name);
+	const binaryNode = parentNodes.find((node) =>
+		getBinaryDataFromNode(context, node?.name)?.hasOwnProperty(inputDataFieldName),
+	);
+	if (!binaryNode) {
+		throw new OperationalError(`No binary data with field ${inputDataFieldName} found.`);
+	}
+	const binaryData = getBinaryDataFromNode(context, binaryNode?.name)[
+		inputDataFieldName
+	] as IBinaryData;
+
+	return {
+		// If a binaryData has an id, the following field is set:
+		// N8N_DEFAULT_BINARY_DATA_MODE=filesystem
path: packages/nodes-base/nodes/Form/formCompletionUtils.ts
user: login: dana-gill
body: In a way, yes. If our binary data mode is filesystem, then the data does not exist explicitly on the binary object we get back. We would have to use the webhook function helpers to retrieve the data based on the id
created_at: 2025-03-14T17:35:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,10 +3,43 @@ import {
 	type NodeTypeAndVersion,
 	type IWebhookFunctions,
 	type IWebhookResponseData,
+	type IBinaryData,
+	type IDataObject,
+	OperationalError,
 } from 'n8n-workflow';
 
 import { sanitizeCustomCss, sanitizeHtml } from './utils';
 
+const getBinaryDataFromNode = (context: IWebhookFunctions, nodeName: string): IDataObject => {
+	return context.evaluateExpression(`{{ $('${nodeName}').first().binary }}`) as IDataObject;
+};
+
+export const binaryResponse = async (
+	context: IWebhookFunctions,
+): Promise<{ data: string | Buffer; fileName: string; type: string }> => {
+	const inputDataFieldName = context.getNodeParameter('inputDataFieldName', '') as string;
+	const parentNodes = context.getParentNodes(context.getNode().name);
+	const binaryNode = parentNodes.find((node) =>
+		getBinaryDataFromNode(context, node?.name)?.hasOwnProperty(inputDataFieldName),
+	);
+	if (!binaryNode) {
+		throw new OperationalError(`No binary data with field ${inputDataFieldName} found.`);
+	}
+	const binaryData = getBinaryDataFromNode(context, binaryNode?.name)[
+		inputDataFieldName
+	] as IBinaryData;
+
+	return {
+		// If a binaryData has an id, the following field is set:
+		// N8N_DEFAULT_BINARY_DATA_MODE=filesystem
+		data: binaryData.id
+			? await context.helpers.binaryToBuffer(await context.helpers.getBinaryStream(binaryData.id))
path: packages/nodes-base/nodes/Form/formCompletionUtils.ts
user: login: netroy
body: This means that we are buffering everything in memory before returning it to the client. We could use streaming here, and avoid OOM issues.
created_at: 2025-03-17T10:35:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,254 @@
+import { type Response } from 'express';
+import { type MockProxy, mock } from 'jest-mock-extended';
+import { type INode, type IWebhookFunctions } from 'n8n-workflow';
+
+import { renderFormCompletion } from '../formCompletionUtils';
+
+describe('formCompletionUtils', () => {
+	let mockWebhookFunctions: MockProxy<IWebhookFunctions>;
+
+	const mockNode: INode = mock<INode>({
+		id: 'test-node',
+		name: 'Test Node',
+		type: 'test',
+		typeVersion: 1,
+		position: [0, 0],
+		parameters: {},
+	});
+
+	const nodeNameWithFileToDownload = 'prevNode0';
+	const nodeNameWithFile = 'prevNode2';
+
+	const parentNodesWithAndWithoutFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: 'prevNode1',
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithMultipleBinaryFiles = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+		{
+			name: nodeNameWithFile,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesWithSingleNodeFile = [
+		{
+			name: nodeNameWithFileToDownload,
+			type: '',
+			typeVersion: 0,
+			disabled: false,
+		},
+	];
+
+	const parentNodesTestCases = [
+		parentNodesWithAndWithoutFiles,
+		parentNodesWithMultipleBinaryFiles,
+		parentNodesWithSingleNodeFile,
+	];
+
+	beforeEach(() => {
+		mockWebhookFunctions = mock<IWebhookFunctions>();
+
+		mockWebhookFunctions.getNode.mockReturnValue(mockNode);
+	});
+
+	afterEach(() => {
+		jest.resetAllMocks();
+	});
+
+	describe('renderFormCompletion', () => {
+		const mockResponse: Response = mock<Response>({
+			send: jest.fn(),
+			render: jest.fn(),
+		});
+
+		const trigger = {
+			name: 'triggerNode',
+			type: 'trigger',
+			typeVersion: 1,
+			disabled: false,
+		};
+
+		afterEach(() => {
+			jest.resetAllMocks();
+		});
+
+		it('should render the form completion', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+				};
+				return params[parameterName];
+			});
+
+			await renderFormCompletion(mockWebhookFunctions, mockResponse, trigger);
+
+			expect(mockResponse.render).toHaveBeenCalledWith('form-trigger-completion', {
+				appendAttribution: undefined,
+				formTitle: 'Form Title',
+				message: 'Form has been submitted successfully',
+				redirectUrl: undefined,
+				responseBinary: encodeURIComponent(JSON.stringify('')),
+				responseText: '',
+				title: 'Form Completion',
+			});
+		});
+
+		it('throw an error if no binary data with the field name is found', async () => {
+			mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
+				const params: { [key: string]: any } = {
+					completionTitle: 'Form Completion',
+					completionMessage: 'Form has been submitted successfully',
+					options: { formTitle: 'Form Title' },
+					respondWith: 'returnBinary',
+					inputDataFieldName: 'inputData',
+				};
+				return params[parameterName];
+			});
+			mockWebhookFunctions.getParentNodes.mockReturnValueOnce([]);
+
+			await expect(
+				renderFormCompletion(mockWebhookFunctions, mockResponse, trigger),
+			).rejects.toThrowError('No binary data with field inputData found.');
+		});
+
+		it('should render if respond with binary is set and binary mode is filesystem', async () => {
path: packages/nodes-base/nodes/Form/test/formCompletionUtils.test.ts
user: login: ShireenMissi
body: you could do process.env.N8N_DEFAULT_BINARY_DATA_MODE='filesystem' but probably it won't have an effect on this test
created_at: 2025-03-18T13:26:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-02-26T10:06:59Z
body: ## [Codecov](https://app.codecov.io/gh/n8n-io/n8n/pull/13507?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) Report
All modified and coverable lines are covered by tests :white_check_mark:

:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-19T11:46:29Z
body: :white_check_mark: All Cypress E2E specs passed
author_association: CONTRIBUTOR
user: login: cypress[bot]
created_at: 2025-03-19T11:46:31Z
body: 
<h3>
  <a href="https://cloud.cypress.io/projects/5hbsdn?utm_source=github&utm_medium=flaky&utm_campaign=project">n8n</a>&nbsp;
  <img src="https://assets.cypress.io/github-pr-comment-icons/chevron.svg" alt="" height="14" width="14" />&nbsp;
  <a href="https://cloud.cypress.io/projects/5hbsdn/runs/9836/overview?utm_source=github&utm_medium=flaky&utm_campaign=run%20number">Run #9836</a>
</h3>

<details>
  <summary>
    <strong>Run Properties:</strong>&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="status check passed" height="10" width="10" />&nbsp;Passed&nbsp;<a href="https://cloud.cypress.io/projects/5hbsdn/runs/9836/overview?utm_source=github&utm_medium=flaky&utm_campaign=run%20number">#9836</a></code> &nbsp;&bull;&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/n8n-io/n8n/commit/36f4d4c758037d55d098f20cdf7d7da43cd30f20">36f4d4c758</a>: 🌳  🖥️ browsers:node18.12.0-chrome107 🤖 dana-gill 🗃️ e2e/*</code>
  </summary><br />

  <table width="744">
    <tbody>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Project</strong>
        </td>
        <td width="520">
          <code><a href="https://cloud.cypress.io/projects/5hbsdn?utm_source=github&utm_medium=flaky&utm_campaign=project">n8n</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Branch Review</strong>
        </th>
        <td>
          <code><a href="https://cloud.cypress.io/projects/5hbsdn/branches/node-2342-respond-with-file/review?utm_source=github&utm_medium=flaky&utm_campaign=review%20changes&changeRequest=13507">node-2342-respond-with-file</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run status</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="status check passed" height="10" width="10" />&nbsp;Passed&nbsp;<a href="https://cloud.cypress.io/projects/5hbsdn/runs/9836/overview?utm_source=github&utm_medium=flaky&utm_campaign=run%20number">#9836</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run duration</strong></td>
        <td><code>04m 42s</code></td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Commit</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/n8n-io/n8n/commit/36f4d4c758037d55d098f20cdf7d7da43cd30f20">36f4d4c758</a>: 🌳  🖥️ browsers:node18.12.0-chrome107 🤖 dana-gill 🗃️ e2e/*</code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Committer</strong></td>
        <td><code>Dana Lee</code></td>
      </tr>
    </tbody>
    <tfoot>
      <tr />
      <tr height="48">
        <td colspan="2">
          <a href="https://cloud.cypress.io/projects/5hbsdn/runs/9836/properties?utm_source=github&utm_medium=flaky&utm_campaign=properties">View all properties for this run ↗︎</a>
        </td>
      </tr>
    </tfoot>
  </table>
  <hr />
</details>

<table width="744">
  <thead>
    <tr />
    <tr height="48">
      <th colspan="2" align="left" width="744">Test results</th>
    </tr>
  </thead>
  <tbody>
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/failed.svg" alt="Tests that failed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5hbsdn/runs/9836/overview?reviewViewBy=FAILED&utm_source=github&utm_medium=flaky&utm_campaign=failed%20tests">
          <strong>Failures</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/flaky-v3.svg" alt="Tests that were flaky" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5hbsdn/runs/9836/overview?reviewViewBy=FLAKY&utm_source=github&utm_medium=flaky&utm_campaign=flaky%20tests">
          <strong>Flaky</strong>
        </a>
      </td>
      <td>
        <code><strong>2</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/pending.svg" alt="Tests that did not run due to a developer annotating a test with .skip" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5hbsdn/runs/9836/test-results?utm_source=github&utm_medium=flaky&utm_campaign=pending%20tests&statuses=%5B%7B%22value%22%3A%22PENDING%22%2C%22label%22%3A%22PENDING%22%7D%5D">
          <strong>Pending</strong>
        </a>
      </td>
      <td>
        <code><strong>5</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/skipped-v3.svg" alt="Tests that did not run due to a failure in a mocha hook" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5hbsdn/runs/9836/test-results?utm_source=github&utm_medium=flaky&utm_campaign=skipped%20tests&statuses=%5B%7B%22value%22%3A%22SKIPPED%22%2C%22label%22%3A%22SKIPPED%22%7D%5D">
          <strong>Skipped</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="Tests that passed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/5hbsdn/runs/9836/overview?utm_source=github&utm_medium=flaky&utm_campaign=passed%20tests">
          <strong>Passing</strong>
        </a>
      </td>
      <td>
        <code><strong>458</strong></code>
      </td>
    </tr><tfoot>
    <tr height="48"><td colspan="2"><a href="https://cloud.cypress.io/projects/5hbsdn/branches/node-2342-respond-with-file/review?utm_source=github&utm_medium=flaky&utm_campaign=review%20changes&changeRequest=13507">View all changes introduced in this branch ↗︎</a></td></tr></tfoot>
  </tbody>
</table>






author_association: NONE
user: login: janober
created_at: 2025-03-24T14:27:23Z
body: Got released with [`n8n@1.85.0`](https://github.com/n8n-io/n8n/releases/tag/n8n%401.85.0)
author_association: MEMBER