diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json
index 766587e622f..ce377d8e158 100644
--- a/packages/frontend/@n8n/i18n/src/locales/en.json
+++ b/packages/frontend/@n8n/i18n/src/locales/en.json
@@ -2163,6 +2163,12 @@
"ndv.pinData.pin.binary": "Pin Data is disabled as this node's output contains binary data.",
"ndv.pinData.pin.link": "More info",
"ndv.pinData.unpin.title": "Unpin data",
+ "ndv.pinData.generate.title": "Generate sample data",
+ "ndv.pinData.generate.description": "Ask AI to produce realistic mock output for this node and pin it, so downstream nodes can be tested without running this one.",
+ "ndv.pinData.generate.success": "Sample data generated and pinned.",
+ "ndv.pinData.generate.error.invalidResponse": "AI did not return a valid JSON array. Try again.",
+ "ndv.pinData.generate.error.emptyResponse": "AI returned no items. Try again.",
+ "ndv.pinData.generate.error.generic": "Could not generate sample data",
"ndv.pinData.pin.multipleRuns.title": "Run #{index} was pinned",
"ndv.pinData.pin.multipleRuns.description": "This run will be outputted each time the node is run.",
"ndv.pinData.unpinAndExecute.title": "Unpin output data?",
diff --git a/packages/frontend/editor-ui/src/features/ndv/panel/components/__snapshots__/InputPanel.test.ts.snap b/packages/frontend/editor-ui/src/features/ndv/panel/components/__snapshots__/InputPanel.test.ts.snap
index 1f25c27c7ce..87da2bce51d 100644
--- a/packages/frontend/editor-ui/src/features/ndv/panel/components/__snapshots__/InputPanel.test.ts.snap
+++ b/packages/frontend/editor-ui/src/features/ndv/panel/components/__snapshots__/InputPanel.test.ts.snap
@@ -210,6 +210,7 @@ exports[`InputPanel > should render 1`] = `
+
hasNoData.value || hasBinaryData.value || isReadOnly.value,
);
+const showGenerateSampleButton = computed(
+ () =>
+ showPinButton.value &&
+ settingsStore.isAskAiEnabled &&
+ !isReadOnlyRoute.value &&
+ !readOnlyEnv.value &&
+ !isExecutionRedacted.value &&
+ sampleGenerator.canGenerate.value,
+);
+
+const generateSampleDisabled = computed(() => pinButtonDisabled.value || pinnedData.hasData.value);
+
const activeTaskMetadata = computed((): ITaskMetadata | null => {
if (!node.value) return null;
const errorMetadata = parseErrorMetadata(workflowRunErrorAsNodeError.value);
@@ -1638,6 +1655,13 @@ defineExpose({ enterEditMode });
@click="enterEditMode({ origin: 'editIconButton' })"
/>
+
+
{
+ beforeEach(cleanup);
+
+ it('renders an enabled button with the correct testid', () => {
+ const { getByTestId } = renderComponent();
+ expect(getByTestId('ndv-generate-sample-data')).toBeEnabled();
+ });
+
+ it('emits generate on click', async () => {
+ const { getByTestId, emitted } = renderComponent();
+ await userEvent.click(getByTestId('ndv-generate-sample-data'));
+ expect(emitted().generate).toBeDefined();
+ });
+
+ it('does not emit when disabled', async () => {
+ const { getByTestId, emitted } = renderComponent({
+ props: { disabled: true, loading: false },
+ });
+ await userEvent.click(getByTestId('ndv-generate-sample-data'));
+ expect(emitted().generate).toBeUndefined();
+ });
+
+ it('renders in loading state', () => {
+ const { getByTestId } = renderComponent({ props: { loading: true, disabled: false } });
+ expect(getByTestId('ndv-generate-sample-data')).toBeDisabled();
+ });
+
+ it('shows tooltip on hover', async () => {
+ const { getByTestId } = renderComponent();
+ await userEvent.hover(getByTestId('ndv-generate-sample-data'));
+ await waitFor(() => {
+ expect(getTooltip()).toHaveTextContent(/Generate sample data/);
+ });
+ });
+});
diff --git a/packages/frontend/editor-ui/src/features/ndv/runData/components/RunDataGenerateSampleButton.vue b/packages/frontend/editor-ui/src/features/ndv/runData/components/RunDataGenerateSampleButton.vue
new file mode 100644
index 00000000000..9eac83795a0
--- /dev/null
+++ b/packages/frontend/editor-ui/src/features/ndv/runData/components/RunDataGenerateSampleButton.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+ {{ locale.baseText('ndv.pinData.generate.title') }}
+
+ {{ locale.baseText('ndv.pinData.generate.description') }}
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/features/ndv/runData/composables/useGenerateSampleData.test.ts b/packages/frontend/editor-ui/src/features/ndv/runData/composables/useGenerateSampleData.test.ts
new file mode 100644
index 00000000000..c5937dd3676
--- /dev/null
+++ b/packages/frontend/editor-ui/src/features/ndv/runData/composables/useGenerateSampleData.test.ts
@@ -0,0 +1,161 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { setActivePinia, createPinia } from 'pinia';
+import { ref } from 'vue';
+import type { INodeUi } from '@/Interface';
+
+const { generateCodeForPromptMock, setDataMock, canPinNodeMock, showMessageMock, showErrorMock } =
+ vi.hoisted(() => ({
+ generateCodeForPromptMock: vi.fn(),
+ setDataMock: vi.fn(),
+ canPinNodeMock: vi.fn().mockReturnValue(true),
+ showMessageMock: vi.fn(),
+ showErrorMock: vi.fn(),
+ }));
+
+vi.mock('@/features/ai/assistant/assistant.api', () => ({
+ generateCodeForPrompt: generateCodeForPromptMock,
+}));
+
+vi.mock('@/app/composables/usePinnedData', () => ({
+ usePinnedData: vi.fn(() => ({
+ setData: setDataMock,
+ canPinNode: canPinNodeMock,
+ hasData: { value: false },
+ isValidNodeType: { value: true },
+ })),
+}));
+
+vi.mock('@/app/composables/useToast', () => ({
+ useToast: () => ({ showMessage: showMessageMock, showError: showErrorMock }),
+}));
+
+vi.mock('@/features/ndv/shared/ndv.store', () => ({
+ useNDVStore: () => ({ pushRef: 'ndv-push-test' }),
+}));
+
+vi.mock('@n8n/stores/useRootStore', () => ({
+ useRootStore: () => ({
+ pushRef: 'root-push-test',
+ restApiContext: { baseUrl: '/rest', sessionId: 's', pushRef: 'root-push-test' },
+ }),
+}));
+
+import { useGenerateSampleData } from './useGenerateSampleData';
+
+const node = ref({
+ id: 'n1',
+ name: 'Slack',
+ type: 'n8n-nodes-base.slack',
+ typeVersion: 2,
+ parameters: { resource: 'message', operation: 'post' },
+} as unknown as INodeUi);
+
+describe('useGenerateSampleData', () => {
+ beforeEach(() => {
+ setActivePinia(createPinia());
+ generateCodeForPromptMock.mockReset();
+ setDataMock.mockReset();
+ showMessageMock.mockReset();
+ showErrorMock.mockReset();
+ });
+
+ it('exposes generate() and isGenerating refs', () => {
+ const composable = useGenerateSampleData(node);
+ expect(typeof composable.generate).toBe('function');
+ expect(composable.isGenerating.value).toBe(false);
+ });
+
+ it('pins parsed array response and shows success toast', async () => {
+ generateCodeForPromptMock.mockResolvedValue({
+ code: '[{"channel":"#general","text":"hi"},{"channel":"#dev","text":"hello"}]',
+ });
+ const composable = useGenerateSampleData(node);
+ await composable.generate();
+ expect(setDataMock).toHaveBeenCalledWith(
+ [{ json: { channel: '#general', text: 'hi' } }, { json: { channel: '#dev', text: 'hello' } }],
+ 'pin-icon-click',
+ );
+ expect(showMessageMock).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }));
+ });
+
+ it('caps items to 3', async () => {
+ generateCodeForPromptMock.mockResolvedValue({
+ code: JSON.stringify(Array.from({ length: 10 }, (_, i) => ({ i }))),
+ });
+ await useGenerateSampleData(node).generate();
+ const [items] = setDataMock.mock.calls[0];
+ expect(items).toHaveLength(3);
+ });
+
+ it('shows error toast and skips pin when response is not a JSON array', async () => {
+ generateCodeForPromptMock.mockResolvedValue({ code: '{"not":"array"}' });
+ await useGenerateSampleData(node).generate();
+ expect(setDataMock).not.toHaveBeenCalled();
+ expect(showErrorMock).toHaveBeenCalled();
+ });
+
+ it('shows error toast and skips pin when array is empty', async () => {
+ generateCodeForPromptMock.mockResolvedValue({ code: '[]' });
+ await useGenerateSampleData(node).generate();
+ expect(setDataMock).not.toHaveBeenCalled();
+ expect(showErrorMock).toHaveBeenCalled();
+ });
+
+ it('shows error toast when API call rejects', async () => {
+ generateCodeForPromptMock.mockRejectedValue(new Error('boom'));
+ await useGenerateSampleData(node).generate();
+ expect(setDataMock).not.toHaveBeenCalled();
+ expect(showErrorMock).toHaveBeenCalled();
+ });
+
+ it('no-ops when node is null', async () => {
+ const empty = ref(null);
+ await useGenerateSampleData(empty).generate();
+ expect(generateCodeForPromptMock).not.toHaveBeenCalled();
+ });
+
+ it('strips markdown code fences before parsing', async () => {
+ generateCodeForPromptMock.mockResolvedValue({
+ code: '```json\n[{"id":"a"},{"id":"b"}]\n```',
+ });
+ await useGenerateSampleData(node).generate();
+ expect(setDataMock).toHaveBeenCalledWith(
+ [{ json: { id: 'a' } }, { json: { id: 'b' } }],
+ 'pin-icon-click',
+ );
+ expect(showErrorMock).not.toHaveBeenCalled();
+ });
+
+ it('ignores a second generate() while one is in flight', async () => {
+ let resolveFirst: (value: { code: string }) => void = () => {};
+ generateCodeForPromptMock.mockImplementationOnce(
+ () =>
+ new Promise<{ code: string }>((resolve) => {
+ resolveFirst = resolve;
+ }),
+ );
+
+ const composable = useGenerateSampleData(node);
+ const first = composable.generate();
+ const second = composable.generate();
+ await second;
+ expect(generateCodeForPromptMock).toHaveBeenCalledTimes(1);
+
+ resolveFirst({ code: '[{"x":1}]' });
+ await first;
+ expect(generateCodeForPromptMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('sends a payload that includes the node type, parameters, and forNode=transform', async () => {
+ generateCodeForPromptMock.mockResolvedValue({ code: '[{"ok":true}]' });
+ await useGenerateSampleData(node).generate();
+
+ expect(generateCodeForPromptMock).toHaveBeenCalledTimes(1);
+ const [, payload] = generateCodeForPromptMock.mock.calls[0];
+ expect(payload.forNode).toBe('transform');
+ expect(payload.context.inputSchema.nodeName).toBe('Slack');
+ expect(payload.question).toContain('n8n-nodes-base.slack');
+ expect(payload.question).toContain('"resource": "message"');
+ expect(payload.question).toContain('JSON array');
+ });
+});
diff --git a/packages/frontend/editor-ui/src/features/ndv/runData/composables/useGenerateSampleData.ts b/packages/frontend/editor-ui/src/features/ndv/runData/composables/useGenerateSampleData.ts
new file mode 100644
index 00000000000..9a0e0527550
--- /dev/null
+++ b/packages/frontend/editor-ui/src/features/ndv/runData/composables/useGenerateSampleData.ts
@@ -0,0 +1,116 @@
+import { computed, ref, unref } from 'vue';
+import type { MaybeRef } from 'vue';
+import { z } from 'zod';
+import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
+import { useI18n } from '@n8n/i18n';
+import { useRootStore } from '@n8n/stores/useRootStore';
+import type { INodeUi } from '@/Interface';
+import { useToast } from '@/app/composables/useToast';
+import { usePinnedData } from '@/app/composables/usePinnedData';
+import { useNDVStore } from '@/features/ndv/shared/ndv.store';
+import { generateCodeForPrompt } from '@/features/ai/assistant/assistant.api';
+import type { AskAiRequest } from '@/features/ai/assistant/assistant.types';
+
+const MAX_ITEMS = 3;
+
+const sampleItemsSchema = z.array(z.record(z.string(), z.unknown())).min(1, 'empty').max(50);
+
+function buildPrompt(node: INodeUi): string {
+ const params = JSON.stringify(node.parameters ?? {}, null, 2);
+ return [
+ 'You are generating realistic mock OUTPUT data for an n8n node so that downstream nodes can be tested without executing the upstream trigger.',
+ `Node type: ${node.type} (version ${node.typeVersion}).`,
+ `Node parameters:\n${params}`,
+ '',
+ 'Return ONLY a raw JSON array (no prose, no Markdown fences, no explanation) of 2 to 3 items.',
+ 'Each item MUST be a JSON object representing one realistic record this node would output.',
+ `Use field names a real ${node.type} response would use. Use realistic-looking IDs, timestamps in ISO 8601, and plausible string values. Do NOT include comments.`,
+ ].join('\n');
+}
+
+function stripCodeFences(raw: string): string {
+ const trimmed = raw.trim();
+ const fenceMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
+ return fenceMatch ? fenceMatch[1].trim() : trimmed;
+}
+
+export function useGenerateSampleData(node: MaybeRef) {
+ const i18n = useI18n();
+ const toast = useToast();
+ const rootStore = useRootStore();
+ const ndvStore = useNDVStore();
+ const pinnedData = usePinnedData(node);
+
+ const isGenerating = ref(false);
+
+ const canGenerate = computed(() => {
+ const target = unref(node);
+ return !!target && pinnedData.isValidNodeType.value && pinnedData.canPinNode(false);
+ });
+
+ async function generate(): Promise {
+ const target = unref(node);
+ if (!target || isGenerating.value) return;
+
+ isGenerating.value = true;
+ try {
+ const payload: AskAiRequest.RequestPayload = {
+ question: buildPrompt(target),
+ context: {
+ schema: [],
+ inputSchema: {
+ nodeName: target.name,
+ schema: { type: 'object', value: [], path: '' },
+ },
+ pushRef: rootStore.pushRef,
+ ndvPushRef: ndvStore.pushRef,
+ },
+ forNode: 'transform',
+ };
+
+ const { code } = await generateCodeForPrompt(rootStore.restApiContext, payload);
+
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(stripCodeFences(code));
+ } catch {
+ toast.showError(
+ new Error(i18n.baseText('ndv.pinData.generate.error.invalidResponse')),
+ i18n.baseText('ndv.pinData.generate.title'),
+ );
+ return;
+ }
+
+ const result = sampleItemsSchema.safeParse(parsed);
+ if (!result.success) {
+ const key =
+ result.error.issues[0]?.message === 'empty'
+ ? 'ndv.pinData.generate.error.emptyResponse'
+ : 'ndv.pinData.generate.error.invalidResponse';
+ toast.showError(new Error(i18n.baseText(key)), i18n.baseText('ndv.pinData.generate.title'));
+ return;
+ }
+
+ const items: INodeExecutionData[] = result.data
+ .slice(0, MAX_ITEMS)
+ .map((json) => ({ json: json as IDataObject }));
+
+ pinnedData.setData(items, 'pin-icon-click');
+
+ toast.showMessage({
+ title: i18n.baseText('ndv.pinData.generate.title'),
+ message: i18n.baseText('ndv.pinData.generate.success'),
+ type: 'success',
+ });
+ } catch (error) {
+ toast.showError(
+ error instanceof Error ? error : new Error(String(error)),
+ i18n.baseText('ndv.pinData.generate.error.generic'),
+ );
+ } finally {
+ isGenerating.value = false;
+ }
+ }
+
+ return { generate, isGenerating, canGenerate };
+}