feat(editor): Ready to run workflows experiment (no-changelog) (#17946)

This commit is contained in:
Romeo Balta
2025-08-04 09:24:15 +01:00
committed by GitHub
parent ce98f7c175
commit 508c636c2c
11 changed files with 1846 additions and 21 deletions

View File

@@ -2718,6 +2718,11 @@
"workflows.ai.starter.collection.card": "Learn how to build AI Agents", "workflows.ai.starter.collection.card": "Learn how to build AI Agents",
"workflows.ai.starter.collection.folder.name": "🎁 n8n basics: Learn how to build Agents in n8n", "workflows.ai.starter.collection.folder.name": "🎁 n8n basics: Learn how to build Agents in n8n",
"workflows.ai.starter.collection.error": "Error loading AI Agent starter collection. Please try again later.", "workflows.ai.starter.collection.error": "Error loading AI Agent starter collection. Please try again later.",
"workflows.readyToRunWorkflows.card": "Try a workflow - no setup neeeded",
"workflows.readyToRunWorkflows.callout": "See n8n in action - no setup needed",
"workflows.readyToRunWorkflows.cta": "Run a workflow",
"workflows.readyToRunWorkflows.folder.name": "🚀 Ready-to-run workflows",
"workflows.readyToRunWorkflows.error": "Error loading n8n collection. Please try again later.",
"workflowSelectorParameterInput.createNewSubworkflow.name": "My Sub-Workflow", "workflowSelectorParameterInput.createNewSubworkflow.name": "My Sub-Workflow",
"importCurlModal.title": "Import cURL command", "importCurlModal.title": "Import cURL command",
"importCurlModal.input.label": "cURL Command", "importCurlModal.input.label": "cURL Command",

View File

@@ -33,4 +33,5 @@ export const STORES = {
FOCUS_PANEL: 'focusPanel', FOCUS_PANEL: 'focusPanel',
AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection', AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection',
PERSONALIZED_TEMPLATES: 'personalizedTemplates', PERSONALIZED_TEMPLATES: 'personalizedTemplates',
EXPERIMENT_READY_TO_RUN_WORKFLOWS: 'readyToRunWorkflows',
} as const; } as const;

View File

@@ -1,33 +1,34 @@
import type { ExecutionFinished } from '@n8n/api-types/push/execution';
import { useUIStore } from '@/stores/ui.store';
import type { IExecutionResponse } from '@/Interface'; import type { IExecutionResponse } from '@/Interface';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { useTelemetry } from '@/composables/useTelemetry';
import { useToast } from '@/composables/useToast';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants'; import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
import { codeNodeEditorEventBus, globalLinkActionsEventBus } from '@/event-bus';
import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store';
import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils'; import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils';
import { import {
clearPopupWindowState, clearPopupWindowState,
getExecutionErrorMessage,
getExecutionErrorToastConfiguration,
hasTrimmedData, hasTrimmedData,
hasTrimmedItem, hasTrimmedItem,
getExecutionErrorToastConfiguration,
getExecutionErrorMessage,
} from '@/utils/executionUtils'; } from '@/utils/executionUtils';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useTelemetry } from '@/composables/useTelemetry';
import { parse } from 'flatted';
import { useToast } from '@/composables/useToast';
import type { useRouter } from 'vue-router';
import { useI18n } from '@n8n/i18n';
import { TelemetryHelpers, EVALUATION_TRIGGER_NODE_TYPE } from 'n8n-workflow';
import type { IWorkflowBase, ExpressionError, IDataObject, IRunExecutionData } from 'n8n-workflow';
import { codeNodeEditorEventBus, globalLinkActionsEventBus } from '@/event-bus';
import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils'; import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils';
import { useExternalHooks } from '@/composables/useExternalHooks'; import type { ExecutionFinished } from '@n8n/api-types/push/execution';
import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useI18n } from '@n8n/i18n';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { parse } from 'flatted';
import { useRunWorkflow } from '@/composables/useRunWorkflow'; import type { ExpressionError, IDataObject, IRunExecutionData, IWorkflowBase } from 'n8n-workflow';
import { useWorkflowSaving } from '@/composables/useWorkflowSaving'; import { EVALUATION_TRIGGER_NODE_TYPE, TelemetryHelpers } from 'n8n-workflow';
import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store'; import type { useRouter } from 'vue-router';
export type SimplifiedExecution = Pick< export type SimplifiedExecution = Pick<
IExecutionResponse, IExecutionResponse,
@@ -44,6 +45,7 @@ export async function executionFinished(
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore(); const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore();
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
workflowsStore.lastAddedExecutingNode = null; workflowsStore.lastAddedExecutingNode = null;
@@ -71,6 +73,12 @@ export async function executionFinished(
data.status, data.status,
); );
} }
if (workflow.meta.templateId.startsWith('37_onboarding_experiments_batch_aug11')) {
readyToRunWorkflowsStore.trackExecuteWorkflow(
workflow.meta.templateId.split('-').pop() ?? '',
data.status,
);
}
} }
uiStore.setProcessingExecutionResults(true); uiStore.setProcessingExecutionResults(true);

View File

@@ -767,12 +767,20 @@ export const TEMPLATE_ONBOARDING_EXPERIMENT = {
variantSuggestedTemplates: 'variant-suggested-templates', variantSuggestedTemplates: 'variant-suggested-templates',
}; };
export const BATCH_11AUG_EXPERIMENT = {
name: '37_onboarding_experiments_batch_aug11',
control: 'control',
variantReadyToRun: 'variant-ready-to-run-workflows',
variantStarterPack: 'variant-starter-pack-v2',
};
export const EXPERIMENTS_TO_TRACK = [ export const EXPERIMENTS_TO_TRACK = [
WORKFLOW_BUILDER_EXPERIMENT.name, WORKFLOW_BUILDER_EXPERIMENT.name,
RAG_STARTER_WORKFLOW_EXPERIMENT.name, RAG_STARTER_WORKFLOW_EXPERIMENT.name,
EXTRA_TEMPLATE_LINKS_EXPERIMENT.name, EXTRA_TEMPLATE_LINKS_EXPERIMENT.name,
TEMPLATE_ONBOARDING_EXPERIMENT.name, TEMPLATE_ONBOARDING_EXPERIMENT.name,
NDV_UI_OVERHAUL_EXPERIMENT.name, NDV_UI_OVERHAUL_EXPERIMENT.name,
BATCH_11AUG_EXPERIMENT.name,
]; ];
export const MFA_FORM = { export const MFA_FORM = {

View File

@@ -0,0 +1,109 @@
import { useTelemetry } from '@/composables/useTelemetry';
import { BATCH_11AUG_EXPERIMENT } from '@/constants';
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useFoldersStore } from '@/stores/folders.store';
import { usePostHog } from '@/stores/posthog.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useI18n } from '@n8n/i18n';
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
import { STORES } from '@n8n/stores';
import { useLocalStorage } from '@vueuse/core';
import { defineStore } from 'pinia';
import { computed } from 'vue';
import { PLAYGROUND_1 } from '../workflows/1_playground';
import { PLAYGROUND_2 } from '../workflows/2_playground';
import { PLAYGROUND_3 } from '../workflows/3_playground';
import { PLAYGROUND_4 } from '../workflows/4_playground';
const LOCAL_STORAGE_SETTING_KEY = 'N8N_READY_TO_RUN_WORKFLOWS_DISMISSED';
export const useReadyToRunWorkflowsStore = defineStore(
STORES.EXPERIMENT_READY_TO_RUN_WORKFLOWS,
() => {
const telemetry = useTelemetry();
const i18n = useI18n();
const foldersStore = useFoldersStore();
const workflowsStore = useWorkflowsStore();
const posthogStore = usePostHog();
const cloudPlanStore = useCloudPlanStore();
const isFeatureEnabled = computed(() => {
return (
posthogStore.getVariant(BATCH_11AUG_EXPERIMENT.name) ===
BATCH_11AUG_EXPERIMENT.variantReadyToRun && cloudPlanStore.userIsTrialing
);
});
const calloutDismissedRef = useLocalStorage(LOCAL_STORAGE_SETTING_KEY, false);
const isCalloutDismissed = computed(() => calloutDismissedRef.value);
const dismissCallout = () => {
calloutDismissedRef.value = true;
};
const trackCreateWorkflows = (source: 'card' | 'callout') => {
telemetry.track('User created ready to run workflows', {
source,
});
};
const trackDismissCallout = () => {
telemetry.track('User dismissed ready to run workflows callout');
};
const trackOpenWorkflow = (template: string) => {
telemetry.track('User opened ready to run workflow', {
template,
});
};
const trackExecuteWorkflow = (template: string, status: string) => {
telemetry.track('User executed ready to run workflow', {
template,
status,
});
};
const createWorkflows = async (projectId: string, parentFolderId?: string) => {
const collectionFolder = await foldersStore.createFolder(
i18n.baseText('workflows.readyToRunWorkflows.folder.name'),
projectId,
parentFolderId,
);
const playground1: WorkflowDataCreate = {
...PLAYGROUND_1,
parentFolderId: collectionFolder.id,
};
const playground2: WorkflowDataCreate = {
...PLAYGROUND_2,
parentFolderId: collectionFolder.id,
};
const playground3: WorkflowDataCreate = {
...PLAYGROUND_3,
parentFolderId: collectionFolder.id,
};
const playground4: WorkflowDataCreate = {
...PLAYGROUND_4,
parentFolderId: collectionFolder.id,
};
await workflowsStore.createNewWorkflow(playground4);
await workflowsStore.createNewWorkflow(playground3);
await workflowsStore.createNewWorkflow(playground2);
await workflowsStore.createNewWorkflow(playground1);
dismissCallout();
return collectionFolder;
};
return {
isFeatureEnabled,
isCalloutDismissed,
createWorkflows,
dismissCallout,
trackCreateWorkflows,
trackDismissCallout,
trackOpenWorkflow,
trackExecuteWorkflow,
};
},
);

View File

@@ -0,0 +1,321 @@
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
export const PLAYGROUND_1: WorkflowDataCreate = {
meta: {
templateId: '37_onboarding_experiments_batch_aug11-1_filter_data',
},
name: '▶️ 1. Filter data coming from an API',
nodes: [
{
parameters: {},
type: 'n8n-nodes-base.merge',
typeVersion: 3.2,
position: [448, 176],
id: '01f2f222-4ff2-41ec-afd9-68496d2e0cb3',
name: 'Merge',
},
{
parameters: {},
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [-768, 192],
id: 'e5ef1b32-ce8a-4c71-aa14-6885a09feabe',
name: 'When clicking Execute workflow',
},
{
parameters: {
assignments: {
assignments: [
{
id: '2fd0b039-7dd9-4666-bc24-d3a81e6d4b68',
name: 'quote_category',
value: 'Personal',
type: 'string',
},
],
},
options: {},
},
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [160, 272],
id: '42d065cd-1580-4b2a-8cc6-796be9a1da2a',
name: 'Set Category = Personal',
},
{
parameters: {
assignments: {
assignments: [
{
id: '1ff91e4a-8460-4991-a273-c5f24b4038e9',
name: 'quote_category',
value: 'team',
type: 'string',
},
],
},
options: {},
},
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [160, 64],
id: 'a5259694-11ed-442c-be2a-e0000926fb30',
name: 'Set Category = Team',
},
{
parameters: {
assignments: {
assignments: [
{
id: 'c014a174-3f17-41bf-9fa5-19e822427346',
name: 'author',
value: "={{ $('Make an API request to get a random quote').item.json.author }}",
type: 'string',
},
{
id: '1d60a497-d964-406b-96fc-0206c82d5742',
name: 'quote',
value: "={{ $('Make an API request to get a random quote').item.json.quote }}",
type: 'string',
},
{
id: '5faf3496-8aa3-4a71-934c-6c5e3f08100b',
name: 'quote_category',
value: '={{ $json.quote_category }}',
type: 'string',
},
],
},
options: {},
},
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [736, 176],
id: '82b78022-f986-4bb0-aac8-f34558d46e5d',
name: 'Quote with category',
},
{
parameters: {
content:
'The node below is an `HTTP Node`. It makes a request to an API, which returns a single random quote. ',
height: 512,
width: 304,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-512, -48],
id: 'd6145c80-0504-4a0b-bfd7-5e85867b3d76',
name: 'Sticky Note3',
},
{
parameters: {
content:
'The `Filter node` checks if the quote contains "you" or "your" , to categorise the quote.\n\nIf matched, we create a `quote_category` variable in the `Set node` , with the value to "Team"',
height: 512,
width: 496,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-160, -48],
id: '75efc471-b2a7-4cb0-8299-f7ad710ff67e',
name: 'Sticky Note5',
},
{
parameters: {
content:
'You can reference data input from earlier nodes. We use here the `author` variable which was returned from the first node, the API request.\n\n',
height: 512,
width: 288,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [640, -48],
id: '9df256c6-a437-4a53-a760-e6964e73c5c3',
name: 'Sticky Note6',
},
{
parameters: {
content: 'The `Merge` combines the outputs of both branches into a single list.\n',
height: 512,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [368, -48],
id: 'ad7ec1f3-7a95-444f-a5b8-363fc1224f95',
name: 'Sticky Note8',
},
{
parameters: {
content:
'### ⏩ Next up: \n\n- Tweak and edit this workflow. It\'s made for you to hack up! \n*Example: Try adding the quote `id` to the final output in the "Quote with category" node.*\n\n- Try out the other workflows in the Playground \n\n\n\n\n',
height: 240,
width: 400,
color: 4,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [960, 96],
id: '0d9362fd-e399-4ce5-ba47-c9ab59b452de',
name: 'Sticky Note9',
},
{
parameters: {
content:
'**Tip: Ressources**\n- Use the `n8n Assistant` or any LLM like `ChatGPT` to explain a screenshot, fix issues, or create workflows for you\n- Learn and get inspired with [templates](https://n8n.io/workflows/)\n- Follow the [n8n Courses](https://docs.n8n.io/courses/) or find tutorials on Youtube\n- Ask [the community](https://community.n8n.io/) for help \n',
height: 176,
width: 400,
color: 5,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [960, 480],
id: '724338d8-3e2b-43a1-8a3d-a38be2a50ed3',
name: 'Sticky Note7',
},
{
parameters: {
content:
'## ▶ Click to start\n\n1. Click the orange `Execute Workflow` button \n2. Double-click nodes to view data flows\n2. Re-run to see results change',
height: 448,
width: 368,
color: 4,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-912, -48],
id: 'a05fb79b-a6e6-498d-bdb2-c73ff7898233',
name: 'Sticky Note11',
},
{
parameters: {
conditions: {
options: {
caseSensitive: false,
leftValue: '',
typeValidation: 'strict',
version: 2,
},
conditions: [
{
id: '4b2c3ebb-ad22-4d62-a64e-fdc2837565bc',
leftValue: '={{ $json.quote }}',
rightValue: 'you',
operator: {
type: 'string',
operation: 'contains',
},
},
{
id: 'da268099-9c51-4e93-bc02-d8f0eda03ffd',
leftValue: '={{ $json.quote }}',
rightValue: 'your',
operator: {
type: 'string',
operation: 'contains',
},
},
],
combinator: 'or',
},
options: {
ignoreCase: true,
},
},
type: 'n8n-nodes-base.if',
typeVersion: 2.2,
position: [-96, 192],
id: 'f2dda369-4176-493e-8c6e-c3390a6968a3',
name: 'Filter the quote',
},
{
parameters: {
url: 'https://dummyjson.com/quotes/random',
options: {},
},
id: 'c4437d01-a813-48a7-ac9a-366740d44428',
name: 'Make an API request to get a random quote',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 1,
position: [-400, 192],
},
],
connections: {
Merge: {
main: [
[
{
node: 'Quote with category',
type: 'main',
index: 0,
},
],
],
},
'When clicking Execute workflow': {
main: [
[
{
node: 'Make an API request to get a random quote',
type: 'main',
index: 0,
},
],
],
},
'Set Category = Personnal': {
main: [
[
{
node: 'Merge',
type: 'main',
index: 1,
},
],
],
},
'Set Category = Team': {
main: [
[
{
node: 'Merge',
type: 'main',
index: 0,
},
],
],
},
'Filter the quote': {
main: [
[
{
node: 'Set Category = Team',
type: 'main',
index: 0,
},
],
[
{
node: 'Set Category = Personnal',
type: 'main',
index: 0,
},
],
],
},
'Make an API request to get a random quote': {
main: [
[
{
node: 'Filter the quote',
type: 'main',
index: 0,
},
],
],
},
},
};

View File

@@ -0,0 +1,487 @@
/* eslint-disable n8n-local-rules/no-interpolation-in-regular-string */
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
export const PLAYGROUND_2: WorkflowDataCreate = {
meta: {
templateId: '37_onboarding_experiments_batch_aug11-2_process_user_answers',
},
settings: {
executionOrder: 'v1',
},
name: '▶️ 2. Process user answers from a form',
nodes: [
{
parameters: {
numberInputs: 3,
},
type: 'n8n-nodes-base.merge',
typeVersion: 3.2,
position: [960, 336],
id: 'f6f94912-64a3-4671-9bda-abb03f4dc42e',
name: 'Merge',
},
{
parameters: {
content:
'## ▶ Click to start \n\n1. Click the orange `Execute Worfklow` button\n2. Submit the form\n3. Double-click nodes to view data flows\n4. Try different answers',
height: 432,
width: 352,
color: 4,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-992, 160],
id: 'e5382a25-e813-4fba-bd8e-c16918521da7',
name: 'Sticky Note11',
},
{
parameters: {
content:
'The `Switch` node routes the workflow based on the selected meal type: chicken, vegetarian, or surprise.',
height: 432,
width: 272,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-592, 160],
id: 'ab955ee5-03ea-45cb-afbb-4b36ccfdbda6',
name: 'Sticky Note',
},
{
parameters: {
rules: {
values: [
{
conditions: {
options: {
caseSensitive: false,
leftValue: '',
typeValidation: 'strict',
version: 2,
},
conditions: [
{
id: 'c90e7527-e4ff-41a8-9177-ffdf7d25c72e',
leftValue:
"={{ $json['What type of meal would you like to cook tonight ? '] }}",
rightValue: 'chicken',
operator: {
type: 'string',
operation: 'contains',
},
},
],
combinator: 'and',
},
renameOutput: true,
outputKey: 'chicken',
},
{
conditions: {
options: {
caseSensitive: false,
leftValue: '',
typeValidation: 'strict',
version: 2,
},
conditions: [
{
leftValue:
"={{ $json['What type of meal would you like to cook tonight ? '] }}",
rightValue: 'vegetarian',
operator: {
type: 'string',
operation: 'contains',
},
id: '18bd7d98-f5e3-46df-96c6-8d1c5fea7cf2',
},
],
combinator: 'and',
},
renameOutput: true,
outputKey: 'vegetarian',
},
{
conditions: {
options: {
caseSensitive: false,
leftValue: '',
typeValidation: 'strict',
version: 2,
},
conditions: [
{
id: '74441f58-e5e5-487c-972d-ec7f9107436d',
leftValue:
"={{ $json['What type of meal would you like to cook tonight ? '] }}",
rightValue: 'surprise',
operator: {
type: 'string',
operation: 'contains',
},
},
],
combinator: 'and',
},
renameOutput: true,
outputKey: 'surprise',
},
],
},
options: {
ignoreCase: true,
},
},
type: 'n8n-nodes-base.switch',
typeVersion: 3.2,
position: [-512, 336],
id: '6cbc178b-a420-4d57-a107-247600ae00c8',
name: 'Route based on meal preference',
},
{
parameters: {
url: '=https://dummyjson.com/recipes/{{$today.weekday}}',
options: {},
},
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [-160, 560],
id: '98e4c862-5bf8-4cd9-8548-62d30b3c548a',
name: 'Get a random recipe from the API',
},
{
parameters: {
url: "=https://dummyjson.com/recipes/search?q={{ $json['What type of meal would you like to cook tonight ? '] }}",
options: {},
},
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [-160, 352],
id: '52d53f34-9036-48f8-a71c-f501cc7e37b0',
name: 'Get vegetarian recipes from the API',
},
{
parameters: {
url: '=https://dummyjson.com/recipes/search?q=chicken',
options: {},
},
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [-160, 144],
id: '240a9947-81fb-48ba-ac46-27381f4f67b6',
name: 'Get chicken recipes from the API',
},
{
parameters: {
content: 'These `HTTP nodes` call the API to get recipes based on the users choice.',
height: 816,
width: 304,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-272, -48],
id: 'f27030aa-9cca-4872-b672-b7ab8f307584',
name: 'Sticky Note2',
},
{
parameters: {
content:
'In the Chicken branch, the API returns an array with 8 chicken recipes.\n\nThe `Split Out` node splits the array into separate items: 8 items, one per recipe.',
height: 576,
width: 224,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [80, -48],
id: 'e4bc5319-ae92-4a72-878e-8c83ca80d203',
name: 'Sticky Note4',
},
{
parameters: {
content: 'The `Form node` displays the selected recipe on the completion screen',
height: 384,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [1152, 208],
id: '6eb7f690-756d-46df-b927-91426e9635da',
name: 'Sticky Note5',
},
{
parameters: {
content: 'The `Merge` node combines data from all three recipe branches.',
height: 384,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [864, 208],
id: '8e52483c-c740-454b-898b-cf457af693a8',
name: 'Sticky Note6',
},
{
parameters: {
formTitle: 'n8n Form',
formFields: {
values: [
{
fieldLabel: 'What type of meal would you like to cook tonight ? ',
fieldType: 'dropdown',
fieldOptions: {
values: [
{
option: 'Chicken-based',
},
{
option: 'Vegetarian',
},
{
option: 'Surprise me! ',
},
],
},
requiredField: true,
},
{
fieldLabel: "What's your name? ",
requiredField: true,
},
],
},
options: {},
},
type: 'n8n-nodes-base.formTrigger',
typeVersion: 2.2,
position: [-864, 352],
id: '3e5b2d36-5126-45f7-a81c-c1cfc82a392d',
name: 'Trigger when user submits form',
webhookId: 'd9a8c65e-486f-4304-a34d-87e9d68aa868',
},
{
parameters: {
content:
'We want to suggest 1 recipe only, not 8.\nSo we sort by cooking time with the `Sort` node, and then pick the recipe with lowest cooking time using `Limit` node',
height: 384,
width: 416,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [368, -48],
id: '520e150e-9b22-4ed8-b3f2-1d128ed9e4c2',
name: 'Sticky Note7',
},
{
parameters: {
sortFieldsUi: {
sortField: [
{
fieldName: 'cookTimeMinutes',
},
],
},
options: {},
},
type: 'n8n-nodes-base.sort',
typeVersion: 1,
position: [432, 144],
id: '17834d09-5d83-420f-9109-105e92baffef',
name: 'Sort by cooking time',
},
{
parameters: {},
type: 'n8n-nodes-base.limit',
typeVersion: 1,
position: [624, 144],
id: '9db21680-c947-44c1-8194-48e42f995755',
name: 'Limit to recipe with lowest cooking time',
},
{
parameters: {
operation: 'completion',
completionTitle: 'Recipe',
completionMessage:
"=Hey {{ $('Trigger when user submits form').item.json['What\\'s your name? '] }}, <br /><br />\n\nWhat about cooking a <b>{{ $json.name }}</b> tonight? <br /><br />\n\nIt shouldn't take you more than {{ $json.prepTimeMinutes }} minutes to prepare.<br /><br />\n\n<h3>What you'll need:</h3><br />\n\n<ul>{{ $json.ingredients.map(ingredient => `<li>${ingredient}</li>`).join('') }}</ul><br /><br />\n\n<h3>Instructions</h3><br />\n\n{{ $json.instructions.map((instruction, index) => `${index + 1}. ${instruction}`).join('<br />') }}\n",
options: {
customCss:
":root {\n\t--font-family: 'Open Sans', sans-serif;\n\t--font-weight-normal: 400;\n\t--font-weight-bold: 600;\n\t--font-size-body: 12px;\n\t--font-size-label: 14px;\n\t--font-size-test-notice: 12px;\n\t--font-size-input: 14px;\n\t--font-size-header: 20px;\n\t--font-size-paragraph: 14px;\n\t--font-size-link: 12px;\n\t--font-size-error: 12px;\n\t--font-size-html-h1: 28px;\n\t--font-size-html-h2: 20px;\n\t--font-size-html-h3: 16px;\n\t--font-size-html-h4: 14px;\n\t--font-size-html-h5: 12px;\n\t--font-size-html-h6: 10px;\n\t--font-size-subheader: 14px;\n\n\t/* Colors */\n\t--color-background: #fbfcfe;\n\t--color-test-notice-text: #e6a23d;\n\t--color-test-notice-bg: #fefaf6;\n\t--color-test-notice-border: #f6dcb7;\n\t--color-card-bg: #ffffff;\n\t--color-card-border: #dbdfe7;\n\t--color-card-shadow: rgba(99, 77, 255, 0.06);\n\t--color-link: #7e8186;\n\t--color-header: #525356;\n\t--color-label: #555555;\n\t--color-input-border: #dbdfe7;\n\t--color-input-text: #71747A;\n\t--color-focus-border: rgb(90, 76, 194);\n\t--color-submit-btn-bg: #ff6d5a;\n\t--color-submit-btn-text: #ffffff;\n\t--color-error: #ea1f30;\n\t--color-required: #ff6d5a;\n\t--color-clear-button-bg: #7e8186;\n\t--color-html-text: #555;\n\t--color-html-link: #ff6d5a;\n\t--color-header-subtext: #7e8186;\n\n\t/* Border Radii */\n\t--border-radius-card: 8px;\n\t--border-radius-input: 6px;\n\t--border-radius-clear-btn: 50%;\n\t--card-border-radius: 8px;\n\n\t/* Spacing */\n\t--padding-container-top: 24px;\n\t--padding-card: 24px;\n\t--padding-test-notice-vertical: 12px;\n\t--padding-test-notice-horizontal: 24px;\n\t--margin-bottom-card: 16px;\n\t--padding-form-input: 12px;\n\t--card-padding: 24px;\n\t--card-margin-bottom: 16px;\n\n\t/* Dimensions */\n\t--container-width: 448px;\n\t--submit-btn-height: 48px;\n\t--checkbox-size: 18px;\n\n\t/* Others */\n\t--box-shadow-card: 0px 4px 16px 0px var(--color-card-shadow);\n\t--opacity-placeholder: 0.5;\n}\n\n.card {\n text-align: left;\n}\n\nul {\n padding-left: 20px;\n}\n\nh4, ul, li {\n text-color: #7e8186!important;\n}\n\n",
},
},
type: 'n8n-nodes-base.form',
typeVersion: 1,
position: [1216, 352],
id: 'f1291ce7-161a-49bf-9c0c-5bc1227b986c',
name: 'Show completion screen with the recipe suggestion',
webhookId: '593b279d-6426-48a0-b28c-44056660bca9',
},
{
parameters: {
content:
'**Tip: Send data to n8n**\nYou can trigger a workflow in many ways not just with forms. For example, using a webhook or when a new row is added to a Google Sheet.',
height: 96,
width: 352,
color: 5,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-992, 608],
id: '16f3bf2a-943d-4399-946f-6444616e8e57',
name: 'Sticky Note8',
},
{
parameters: {
fieldToSplitOut: 'recipes',
options: {},
},
type: 'n8n-nodes-base.splitOut',
typeVersion: 1,
position: [144, 352],
id: '05c93482-4bd4-464c-b7ee-909a0fd0bada',
name: 'Split Out the results into separate items',
},
{
parameters: {
fieldToSplitOut: 'recipes',
options: {},
},
type: 'n8n-nodes-base.splitOut',
typeVersion: 1,
position: [144, 144],
id: '503526fd-c317-4d77-916c-2ff5eb6f63f9',
name: 'Split Out the array into 8 items',
},
],
connections: {
Merge: {
main: [
[
{
node: 'Show completion screen with the recipe suggestion',
type: 'main',
index: 0,
},
],
],
},
'Route based on meal preference': {
main: [
[
{
node: 'Get chicken recipes from the API',
type: 'main',
index: 0,
},
],
[
{
node: 'Get vegetarian recipes from the API',
type: 'main',
index: 0,
},
],
[
{
node: 'Get a random recipe from the API',
type: 'main',
index: 0,
},
],
],
},
'Get a random recipe from the API': {
main: [
[
{
node: 'Merge',
type: 'main',
index: 2,
},
],
],
},
'Get vegetarian recipes from the API': {
main: [
[
{
node: 'Split Out the results into separate items',
type: 'main',
index: 0,
},
],
],
},
'Get chicken recipes from the API': {
main: [
[
{
node: 'Split Out the array into 8 items',
type: 'main',
index: 0,
},
],
],
},
'Trigger when user submits form': {
main: [
[
{
node: 'Route based on meal preference',
type: 'main',
index: 0,
},
],
],
},
'Sort by cooking time': {
main: [
[
{
node: 'Limit to recipe with lowest cooking time',
type: 'main',
index: 0,
},
],
],
},
'Limit to recipe with lowest cooking time': {
main: [
[
{
node: 'Merge',
type: 'main',
index: 0,
},
],
],
},
'Show completion screen with the recipe suggestion': {
main: [[]],
},
'Split Out the results into separate items': {
main: [
[
{
node: 'Merge',
type: 'main',
index: 1,
},
],
],
},
'Split Out the array into 8 items': {
main: [
[
{
node: 'Sort by cooking time',
type: 'main',
index: 0,
},
],
],
},
},
};

View File

@@ -0,0 +1,253 @@
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
export const PLAYGROUND_3: WorkflowDataCreate = {
meta: {
templateId: '37_onboarding_experiments_batch_aug11-3_check_weather_by_location',
},
name: '▶️ 3. Check weather based on user location',
settings: {
executionOrder: 'v1',
},
nodes: [
{
parameters: {
jsCode:
'const today = new Date().toISOString().slice(0, 10);\nconst daily = $json.daily;\nconst index = daily.time.indexOf(today);\n\nif (index === -1) {\n throw new Error("Today\'s forecast not found in response.");\n}\n\nreturn [{\n date: today,\n temp_max: daily.temperature_2m_max[index],\n temp_min: daily.temperature_2m_min[index]\n}];\n',
},
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [336, -128],
id: 'd6463e70-9921-4e6f-acaa-c8d6153254ea',
name: "Get today's high and low",
},
{
parameters: {
content:
'**Tip: n8n 🧡 LLM**\n\nUse the n8n Assistant or ChatGPT, Claude, etc. to explain, edit, or create Javascript code for you.',
height: 112,
width: 272,
color: 5,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [240, 96],
id: '30ebd1b4-55c2-45fd-ae30-2b974eee226f',
name: 'Sticky Note1',
},
{
parameters: {
content:
'## ▶ Start here \n\n1. Click the orange `Execute Worfklow` button \n2. Double-click nodes to view data flows\n3. Note: The form doesnt show up because it runs with [pinned](https://docs.n8n.io/data/data-pinning/) test data (purple highlights)\n3. Unpin the data to run the form normally',
height: 208,
width: 352,
color: 4,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-1056, -512],
id: '152278ea-a817-4f6d-8053-f129d9038604',
name: 'Sticky Note2',
},
{
parameters: {
content:
'The `Code` node lets you run Javascript code in your workflow.\n\nWe use it here to extract todays date, max temp, and min temp from the API response.',
height: 352,
width: 272,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [240, -272],
id: '5fcff69f-e059-4827-9241-081e4a7e4a6b',
name: 'Sticky Note3',
},
{
parameters: {
content:
'This `HTTP node` calls an API to get the citys latitude and longitude.\n\nThe user city input is used as a URL variable.',
height: 352,
width: 272,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-704, -272],
id: '28813de7-3311-4c46-8b88-946c4c14a99e',
name: 'Sticky Note4',
},
{
parameters: {
content:
'Another `HTTP node` calls a weather API using the latitude and longitude in the URL.',
height: 352,
width: 272,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-96, -272],
id: 'bc28c149-c4bd-4d68-b597-f7cf2f0ab0c4',
name: 'Sticky Note5',
},
{
parameters: {
content: 'This `Limit` node keeps only the first item returned by the API.',
height: 352,
width: 256,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-400, -272],
id: '1d23e68c-5d5c-4383-b6b8-ca552ad3a889',
name: 'Sticky Note6',
},
{
parameters: {
formTitle: "What's the weather where you live",
formFields: {
values: [
{
fieldLabel: 'Which city do you live in ? ',
fieldType: 'textarea',
placeholder: 'Paris',
requiredField: true,
},
],
},
options: {},
},
type: 'n8n-nodes-base.formTrigger',
typeVersion: 2.2,
position: [-944, -128],
id: 'beba8fc9-9213-43bb-8f78-08800d629270',
name: 'Trigger when user submits form',
webhookId: 'a460df1e-c73b-4654-9a21-2987cea55b14',
},
{
parameters: {},
type: 'n8n-nodes-base.limit',
typeVersion: 1,
position: [-320, -128],
id: 'aa0b1683-0586-465a-bcf2-38cf0617a6fc',
name: 'Limit to first item',
},
{
parameters: {
url: '=https://api.open-meteo.com/v1/forecast?latitude={{ $json.lat }}&longitude={{ $json.lon }}&daily=temperature_2m_max,temperature_2m_min',
options: {
response: {
response: {
responseFormat: 'json',
},
},
},
},
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [0, -128],
id: '73b40790-3ef6-4091-9cd3-cab673276a7a',
name: 'Get weather for that latitude and longitude',
},
{
parameters: {
content:
'**Tip: Edit pinned data**\n\nWhen data is pinned, click the ✏ icon in the top right to edit it. Try changing “London” to “Berlin” and rerun the workflow.',
height: 128,
width: 288,
color: 5,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-1056, 96],
id: 'd9390b71-3bf2-4094-b603-3f43988675e9',
name: 'Sticky Note',
},
{
parameters: {
url: "=https://nominatim.openstreetmap.org/search?q={{ $json['Which city do you live in ? '] }}&format=json",
options: {
response: {
response: {},
},
},
},
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [-624, -128],
id: 'e3ce382a-c21b-4ee8-b366-b28cfe2b1b37',
name: 'Get city latitude and longitude',
},
{
parameters: {
content: 'This `Limit` node keeps only the first item returned by the API.',
height: 352,
width: 288,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-1056, -272],
id: '305eea3e-c853-4a8c-a7d4-dd10d81bf099',
name: 'Sticky Note7',
},
],
pinData: {
'Trigger when user submits form': [
{
json: {
'Which city do you live in ? ': 'London',
submittedAt: '2025-08-01T12:06:25.400+02:00',
formMode: 'test',
},
},
],
},
connections: {
'Trigger when user submits form': {
main: [
[
{
node: 'Get city latitude and longitude',
type: 'main',
index: 0,
},
],
],
},
'Limit to first item': {
main: [
[
{
node: 'Get weather for that latitude and longitude',
type: 'main',
index: 0,
},
],
],
},
'Get weather for that latitude and longitude': {
main: [
[
{
node: "Get today's high and low",
type: 'main',
index: 0,
},
],
],
},
'Get city latitude and longitude': {
main: [
[
{
node: 'Limit to first item',
type: 'main',
index: 0,
},
],
],
},
},
};

View File

@@ -0,0 +1,518 @@
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
export const PLAYGROUND_4: WorkflowDataCreate = {
meta: {
templateId: '37_onboarding_experiments_batch_aug11-4_create_personalized_email',
},
name: '🔌 4. Create a personalized email',
settings: {
executionOrder: 'v1',
},
nodes: [
{
parameters: {
url: '=https://nominatim.openstreetmap.org/search?q={{ $json.City }}&format=json',
options: {
response: {
response: {},
},
},
},
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [-336, -16],
id: 'd6ea9452-7fa4-41f6-b9cd-7f12b8e3b686',
name: 'Get city latitude and longitude',
},
{
parameters: {
url: '=https://api.open-meteo.com/v1/forecast?latitude={{ $json.lat }}&longitude={{ $json.lon }}&daily=temperature_2m_max,temperature_2m_min',
options: {
response: {
response: {
responseFormat: 'json',
},
},
},
},
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [48, -16],
id: 'db233160-3f63-47fa-998a-41977a51c2f6',
name: 'Get weather',
},
{
parameters: {
assignments: {
assignments: [
{
id: 'ea110fbf-b67b-4270-8e55-ffec2f9ddafe',
name: 'City',
value: 'Paris',
type: 'string',
},
],
},
options: {},
},
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [-640, -16],
id: 'e3c93ca5-2628-41f4-81ea-50c87403b285',
name: 'Set your location',
},
{
parameters: {
numberInputs: 3,
},
type: 'n8n-nodes-base.merge',
typeVersion: 3.2,
position: [416, -208],
id: '21021c33-7b93-4cbe-a592-f7b277ad332e',
name: 'Merge',
notesInFlow: false,
},
{
parameters: {
aggregate: 'aggregateAllItemData',
options: {},
},
type: 'n8n-nodes-base.aggregate',
typeVersion: 1,
position: [688, -192],
id: '65312cc9-b31e-4ad3-b5b9-7ccfe066f0fb',
name: 'Aggregate',
},
{
parameters: {
jsCode:
'const today = new Date().toISOString().slice(0, 10);\nconst daily = $json.daily;\nconst index = daily.time.indexOf(today);\n\nif (index === -1) {\n throw new Error("Today\'s forecast not found in response.");\n}\n\nreturn [{\n date: today,\n temp_max: daily.temperature_2m_max[index],\n temp_min: daily.temperature_2m_min[index]\n}];\n',
},
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [224, -16],
id: '8defc8b4-a50f-41ff-bfb0-bd114e5fcd7c',
name: "Get today's high and low",
},
{
parameters: {},
type: 'n8n-nodes-base.limit',
typeVersion: 1,
position: [-112, -16],
id: 'a346a114-2981-4c82-91a5-4656b345b0d1',
name: 'Limit',
},
{
parameters: {
content:
'### Bonus task!\n\nUse the `Gmail node` to send the workflow result via email:\n\n1. Connect the "output" node to the Gmail node\n2. Create your credentials to connect to Gmail ',
height: 448,
width: 288,
color: 4,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [1168, -464],
id: '5398ab1d-4d6b-4308-8129-a3e1ebe7c11c',
name: 'Sticky Note5',
},
{
parameters: {},
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [-1008, -192],
id: 'e378d189-974a-4d8e-a5bc-dd84fe4bc1c1',
name: "1. Click 'Execute workflow",
notesInFlow: false,
notes: '\n',
},
{
parameters: {
content:
'## ▶ Start here \n\n1. Click the orange `Execute Worfklow` button \n2. Double-click nodes to view data flows\n3. Try changing variables in `Set your location` node (e.g. set location to London instead of Berlin)',
height: 416,
width: 336,
color: 4,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-1136, -416],
id: '6ef3cee5-4c05-4611-9372-233b89aedbb9',
name: 'Sticky Note6',
},
{
parameters: {
content:
'The `Set nodes` below define variables used later in the workflow:\n\n1. Todays day and month\n2. Currency: EUR to USD\n3. Location: Berlin',
height: 784,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-720, -624],
id: '8d1e0f28-328c-46e7-86a4-d7a063eab933',
name: 'Sticky Note',
},
{
parameters: {
assignments: {
assignments: [
{
id: '904f0394-6f4a-498f-98a7-b0526dfd63f0',
name: 'current month',
value: "={{$now.format('M')}}",
type: 'string',
},
{
id: 'c4fd79aa-e889-49c1-86f1-4e5ad672048f',
name: 'current day',
value: "={{$now.format('d')}}",
type: 'string',
},
],
},
options: {},
},
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [-640, -384],
id: '503e303a-99ca-4a88-885b-2738f2067aac',
name: 'Set current day and month',
},
{
parameters: {
assignments: {
assignments: [
{
id: '8c17e5ba-1747-46e3-ae41-8f1e9046aa7a',
name: 'Convert from currency',
value: 'EUR',
type: 'string',
},
{
id: 'e14ae5dd-7559-4e14-8e25-e627f11d8094',
name: 'Convert to currency',
value: 'USD',
type: 'string',
},
],
},
options: {},
},
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [-640, -192],
id: 'bc6f48f0-21a1-417a-9724-64eafce2ec1f',
name: 'Set exchange currency',
},
{
parameters: {
content:
'The `HTTP nodes` below call separate APIs using the previously defined variables to retrieve:\n\n1. Historical events on the same day/month\n2. Currency exchange rate\n3. Weather data ',
height: 784,
width: 256,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-416, -624],
id: '02b09b86-be1b-47fd-b083-ee6c31b5a83f',
name: 'Sticky Note1',
},
{
parameters: {
url: '=http://numbersapi.com/{{ $json["current month"] }}/{{ $json["current day"] }}/date?json',
options: {},
},
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [-336, -384],
id: '0cb9f4df-42db-49b8-b545-e3a7bad28a8f',
name: 'Get a historical fact',
},
{
parameters: {
url: "=https://api.frankfurter.dev/v1/latest?base={{ $json['Convert from currency'] }}&symbols={{ $json['Convert to currency'] }}",
options: {},
},
type: 'n8n-nodes-base.httpRequest',
typeVersion: 4.2,
position: [-336, -192],
id: 'df332c72-8600-4458-a40f-020422bf7818',
name: 'Get exchange rates',
},
{
parameters: {
content:
'These nodes are explained in the previous workflow: \n▶ 3. Check weather based on user location',
height: 80,
width: 480,
color: 5,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-128, 144],
id: 'e3c29b04-39d5-4d4d-9021-f4eb26e54d95',
name: 'Sticky Note7',
},
{
parameters: {
content:
'The `Merge` node combines data from the 3 streams, once data for all streams is available.',
height: 352,
width: 224,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [352, -368],
id: '02c84a53-c933-4ad7-a0ce-9a53fbd6b407',
name: 'Sticky Note2',
},
{
parameters: {
content:
'The `Aggregate` node take the 3 separate items and groups them together into a single item.',
height: 352,
width: 208,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [624, -368],
id: '0ce70667-9587-49d5-96b3-d477fef517b1',
name: 'Sticky Note3',
},
{
parameters: {
content: 'The `Set node` let us pick the data',
height: 352,
width: 192,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [880, -368],
id: '3587d9df-9ccc-462f-a4b6-e2b3ddaa0e89',
name: 'Sticky Note8',
},
{
parameters: {
assignments: {
assignments: [
{
id: '4b651566-5b0d-4f7e-b1d4-ea95dee7fbb5',
name: 'On this day',
value: '={{ $json.data[0].text }}',
type: 'string',
},
{
id: 'cef5bfc7-be8b-4ac4-911d-2b9721c8666d',
name: 'Exchange rate',
value:
'={{ $json.data[1].rates[$("Set exchange currency").item.json["Convert to currency"]] }}',
type: 'string',
},
{
id: '02358dea-7351-49ea-923c-9695ba7003c8',
name: 'Daily high',
value: '={{ $json.data[2].temp_max }}°C',
type: 'string',
},
{
id: 'a8886281-896b-4713-82dd-8a1573b3d1df',
name: 'Daily low',
value: '={{ $json.data[2].temp_min }}°C',
type: 'string',
},
],
},
options: {},
},
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [928, -192],
id: 'cef89e73-fa8b-48c1-ad2d-293ac61be2ff',
name: 'Select output for the email',
},
{
parameters: {
content:
'**Tip: Use credentials**\nAdd [credentials](https://docs.n8n.io/credentials) in n8n to connect apps like Gmail, Slack, or OpenAI and use them in your workflows.',
height: 112,
width: 288,
color: 5,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [1168, 16],
id: '14969990-acce-46f3-a1ab-219ab4a1a858',
name: 'Sticky Note9',
},
{
parameters: {
subject: 'Daily fact and data',
emailType: 'text',
message:
"=Hey,\n\nToday in {{ $('Set your location').item.json.City }}:\n- Daily high: {{ $json['Daily high'] }}\n- Daily low: {{ $json['Daily low'] }}\n\n{{ $('Set exchange currency').item.json['Convert from currency'] }} to {{ $('Set exchange currency').item.json['Convert to currency'] }} exchange rate: {{ $json['Exchange rate'] }}\n\nImpress your colleagues by reminding them that: {{ $json['On this day'] }}\n\n(Historical fact generated by numbersapi.com)\n",
options: {},
},
type: 'n8n-nodes-base.gmail',
typeVersion: 2.1,
position: [1264, -192],
id: '4a23b2b5-21f7-4a63-a1ab-9f18d6ce2e6a',
name: 'Send output via email using Gmail',
webhookId: '65c7b462-bb4a-400c-a556-ef408efcd208',
notesInFlow: true,
notes: 'Double-click here to connect!',
},
],
pinData: {},
connections: {
'Get city latitude and longitude': {
main: [
[
{
node: 'Limit',
type: 'main',
index: 0,
},
],
],
},
'Get weather': {
main: [
[
{
node: "Get today's high and low",
type: 'main',
index: 0,
},
],
],
},
'Set your location': {
main: [
[
{
node: 'Get city latitude and longitude',
type: 'main',
index: 0,
},
],
],
},
Merge: {
main: [
[
{
node: 'Aggregate',
type: 'main',
index: 0,
},
],
],
},
Aggregate: {
main: [
[
{
node: 'Select output for the email',
type: 'main',
index: 0,
},
],
],
},
"Get today's high and low": {
main: [
[
{
node: 'Merge',
type: 'main',
index: 2,
},
],
],
},
Limit: {
main: [
[
{
node: 'Get weather',
type: 'main',
index: 0,
},
],
],
},
"1. Click 'Execute workflow": {
main: [
[
{
node: 'Set current day and month',
type: 'main',
index: 0,
},
{
node: 'Set your location',
type: 'main',
index: 0,
},
{
node: 'Set exchange currency',
type: 'main',
index: 0,
},
],
],
},
'Set current day and month': {
main: [
[
{
node: 'Get a historical fact',
type: 'main',
index: 0,
},
],
],
},
'Set exchange currency': {
main: [
[
{
node: 'Get exchange rates',
type: 'main',
index: 0,
},
],
],
},
'Get a historical fact': {
main: [
[
{
node: 'Merge',
type: 'main',
index: 0,
},
],
],
},
'Get exchange rates': {
main: [
[
{
node: 'Merge',
type: 'main',
index: 1,
},
],
],
},
'Select output for the email': {
main: [[]],
},
},
};

View File

@@ -138,6 +138,7 @@ import { canvasEventBus } from '@/event-bus/canvas';
import CanvasChatButton from '@/components/canvas/elements/buttons/CanvasChatButton.vue'; import CanvasChatButton from '@/components/canvas/elements/buttons/CanvasChatButton.vue';
import { useFocusPanelStore } from '@/stores/focusPanel.store'; import { useFocusPanelStore } from '@/stores/focusPanel.store';
import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store'; import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store';
import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store';
defineOptions({ defineOptions({
name: 'NodeView', name: 'NodeView',
@@ -199,6 +200,7 @@ const posthogStore = usePostHog();
const agentRequestStore = useAgentRequestStore(); const agentRequestStore = useAgentRequestStore();
const logsStore = useLogsStore(); const logsStore = useLogsStore();
const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore(); const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore();
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({ const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({
route, route,
@@ -505,6 +507,12 @@ async function initializeWorkspaceForExistingWorkflow(id: string) {
); );
} }
if (workflowData.meta?.templateId?.startsWith('37_onboarding_experiments_batch_aug11')) {
readyToRunWorkflowsStore.trackOpenWorkflow(
workflowData.meta.templateId.split('-').pop() ?? '',
);
}
await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflowData.homeProject); await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflowData.homeProject);
} catch (error) { } catch (error) {
if (error.httpStatusCode === 404) { if (error.httpStatusCode === 404) {

View File

@@ -24,6 +24,7 @@ import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesS
import SuggestedWorkflowCard from '@/experiments/personalizedTemplates/components/SuggestedWorkflowCard.vue'; import SuggestedWorkflowCard from '@/experiments/personalizedTemplates/components/SuggestedWorkflowCard.vue';
import SuggestedWorkflows from '@/experiments/personalizedTemplates/components/SuggestedWorkflows.vue'; import SuggestedWorkflows from '@/experiments/personalizedTemplates/components/SuggestedWorkflows.vue';
import { usePersonalizedTemplatesStore } from '@/experiments/personalizedTemplates/stores/personalizedTemplates.store'; import { usePersonalizedTemplatesStore } from '@/experiments/personalizedTemplates/stores/personalizedTemplates.store';
import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store';
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue'; import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
import { useInsightsStore } from '@/features/insights/insights.store'; import { useInsightsStore } from '@/features/insights/insights.store';
import type { import type {
@@ -118,6 +119,7 @@ const insightsStore = useInsightsStore();
const templatesStore = useTemplatesStore(); const templatesStore = useTemplatesStore();
const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore(); const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore();
const personalizedTemplatesStore = usePersonalizedTemplatesStore(); const personalizedTemplatesStore = usePersonalizedTemplatesStore();
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
const documentTitle = useDocumentTitle(); const documentTitle = useDocumentTitle();
const { callDebounced } = useDebounce(); const { callDebounced } = useDebounce();
@@ -346,6 +348,20 @@ const projectPermissions = computed(() => {
); );
}); });
const showReadyToRunWorkflowsCallout = computed(() => {
const isEnabled = readyToRunWorkflowsStore.isFeatureEnabled;
const isDismissed = readyToRunWorkflowsStore.isCalloutDismissed;
return (
isEnabled &&
!isDismissed &&
!loading.value &&
!readOnlyEnv.value &&
(projectPages.isOverviewSubPage ||
(hasPermissionToCreateFolders.value && hasPermissionToCreateWorkflows.value))
);
});
const emptyListDescription = computed(() => { const emptyListDescription = computed(() => {
if (readOnlyEnv.value) { if (readOnlyEnv.value) {
return i18n.baseText('workflows.empty.description.readOnlyEnv'); return i18n.baseText('workflows.empty.description.readOnlyEnv');
@@ -840,6 +856,46 @@ const createAIStarterWorkflows = async (source: 'card' | 'callout') => {
} }
}; };
const handleCreateReadyToRunWorkflows = async (source: 'card' | 'callout') => {
try {
const projectId = projectPages.isOverviewSubPage
? personalProject.value?.id
: (route.params.projectId as string);
if (typeof projectId !== 'string') {
toast.showError(new Error(), i18n.baseText('workflows.readyToRunWorkflows.error'));
return;
}
const newFolder = await readyToRunWorkflowsStore.createWorkflows(
projectId,
currentFolderId.value ?? undefined,
);
readyToRunWorkflowsStore.trackCreateWorkflows(source);
// If we are on the overview page, navigate to the new folder
if (projectPages.isOverviewSubPage) {
await router.push({
name: VIEWS.PROJECTS_FOLDERS,
params: { projectId, folderId: newFolder.id },
});
} else {
// If we are in a specific folder, just add the new folder to the list
workflowsAndFolders.value.unshift({
id: newFolder.id,
name: newFolder.name,
resource: 'folder',
createdAt: newFolder.createdAt,
updatedAt: newFolder.updatedAt,
subFolderCount: 0,
workflowCount: 4,
parentFolder: newFolder.parentFolder,
});
}
} catch (error) {
toast.showError(error, i18n.baseText('workflows.readyToRunWorkflows.error'));
return;
}
};
const openAIWorkflow = async (source: string) => { const openAIWorkflow = async (source: string) => {
dismissEasyAICallout(); dismissEasyAICallout();
telemetry.track('User clicked test AI workflow', { telemetry.track('User clicked test AI workflow', {
@@ -864,6 +920,11 @@ const dismissEasyAICallout = () => {
easyAICalloutVisible.value = false; easyAICalloutVisible.value = false;
}; };
const handleDismissReadyToRunCallout = () => {
readyToRunWorkflowsStore.dismissCallout();
readyToRunWorkflowsStore.trackDismissCallout();
};
const onWorkflowActiveToggle = (data: { id: string; active: boolean }) => { const onWorkflowActiveToggle = (data: { id: string; active: boolean }) => {
const workflow: WorkflowListItem | undefined = workflowsAndFolders.value.find( const workflow: WorkflowListItem | undefined = workflowsAndFolders.value.find(
(w): w is WorkflowListItem => w.id === data.id, (w): w is WorkflowListItem => w.id === data.id,
@@ -1741,6 +1802,33 @@ const onNameSubmit = async (name: string) => {
</div> </div>
</template> </template>
</N8nCallout> </N8nCallout>
<N8nCallout
v-if="showReadyToRunWorkflowsCallout"
theme="secondary"
icon="bolt-filled"
:class="$style['easy-ai-workflow-callout']"
>
{{ i18n.baseText('workflows.readyToRunWorkflows.callout') }}
<template #trailingContent>
<div :class="$style['callout-trailing-content']">
<N8nButton
data-test-id="easy-ai-button"
size="small"
type="secondary"
@click="handleCreateReadyToRunWorkflows('callout')"
>
{{ i18n.baseText('workflows.readyToRunWorkflows.cta') }}
</N8nButton>
<N8nIcon
size="small"
icon="x"
:title="i18n.baseText('generic.dismiss')"
class="clickable"
@click="handleDismissReadyToRunCallout"
/>
</div>
</template>
</N8nCallout>
</template> </template>
<template #breadcrumbs> <template #breadcrumbs>
<div v-if="breadcrumbsLoading" :class="$style['breadcrumbs-loading']"> <div v-if="breadcrumbsLoading" :class="$style['breadcrumbs-loading']">
@@ -1946,6 +2034,25 @@ const onNameSubmit = async (name: string) => {
</N8nText> </N8nText>
</div> </div>
</N8nCard> </N8nCard>
<N8nCard
v-if="showReadyToRunWorkflowsCallout"
:class="$style.emptyStateCard"
hoverable
data-test-id="ready-to-run-workflows-card"
@click="handleCreateReadyToRunWorkflows('card')"
>
<div :class="$style.emptyStateCardContent">
<N8nIcon
:class="$style.emptyStateCardIcon"
:stroke-width="1.5"
icon="zap"
color="foreground-dark"
/>
<N8nText size="large" class="mt-xs pl-2xs pr-2xs">
{{ i18n.baseText('workflows.readyToRunWorkflows.card') }}
</N8nText>
</div>
</N8nCard>
<N8nCard <N8nCard
v-if="templatesCardEnabled" v-if="templatesCardEnabled"
:class="$style.emptyStateCard" :class="$style.emptyStateCard"