From 267a62d9c1332642c8a1e22b210acb2355cbc4a1 Mon Sep 17 00:00:00 2001
From: Romeo Balta <7095569+romeobalta@users.noreply.github.com>
Date: Mon, 15 Sep 2025 11:21:25 +0100
Subject: [PATCH] feat(editor): Implement Ready to Run Workflows V2 experiment
(no-changelog) (#19468)
---
.../src/components/N8nIcon/icons.ts | 2 +
.../frontend/@n8n/i18n/src/locales/en.json | 1 +
.../frontend/@n8n/stores/src/constants.ts | 1 +
.../src/components/Projects/ProjectHeader.vue | 32 ++-
.../handlers/executionFinished.ts | 11 +
packages/frontend/editor-ui/src/constants.ts | 8 +
.../components/ReadyToRunV2Button.vue | 74 +++++
.../components/SimplifiedEmptyLayout.vue | 222 +++++++++++++++
.../composables/useEmptyStateDetection.ts | 66 +++++
.../stores/readyToRunWorkflowsV2.store.ts | 188 +++++++++++++
.../utils/workflowSamples.ts | 48 ++++
.../workflows/ai-workflow-v2.ts | 253 ++++++++++++++++++
.../workflows/ai-workflow.ts | 240 +++++++++++++++++
.../features/dataStore/DataStoreView.test.ts | 1 +
.../src/utils/templates/workflowSamples.ts | 2 +
.../editor-ui/src/views/WorkflowsView.vue | 24 +-
16 files changed, 1160 insertions(+), 13 deletions(-)
create mode 100644 packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/ReadyToRunV2Button.vue
create mode 100644 packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/SimplifiedEmptyLayout.vue
create mode 100644 packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/composables/useEmptyStateDetection.ts
create mode 100644 packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store.ts
create mode 100644 packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/utils/workflowSamples.ts
create mode 100644 packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow-v2.ts
create mode 100644 packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow.ts
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts
index e9a6318f74..44d205b4ad 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts
@@ -169,6 +169,7 @@ import IconLucideSettings from '~icons/lucide/settings';
import IconLucideShare from '~icons/lucide/share';
import IconLucideSlidersHorizontal from '~icons/lucide/sliders-horizontal';
import IconLucideSmile from '~icons/lucide/smile';
+import IconLucideSparkles from '~icons/lucide/sparkles';
import IconLucideSquare from '~icons/lucide/square';
import IconLucideSquareCheck from '~icons/lucide/square-check';
import IconLucideSquarePen from '~icons/lucide/square-pen';
@@ -583,6 +584,7 @@ export const updatedIconSet = {
share: IconLucideShare,
'sliders-horizontal': IconLucideSlidersHorizontal,
smile: IconLucideSmile,
+ sparkles: IconLucideSparkles,
square: IconLucideSquare,
'square-check': IconLucideSquareCheck,
'square-pen': IconLucideSquarePen,
diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json
index bce0c0f889..6fd4c74f65 100644
--- a/packages/frontend/@n8n/i18n/src/locales/en.json
+++ b/packages/frontend/@n8n/i18n/src/locales/en.json
@@ -2713,6 +2713,7 @@
"workflows.empty.preBuiltAgents": "Try a pre-built agent",
"workflows.empty.shared-with-me": "No {resource} has been shared with you",
"workflows.empty.shared-with-me.link": "Back to Personal",
+ "workflows.empty.readyToRunV2": "Try an AI workflow",
"workflows.list.easyAI": "Test the power of AI in n8n with this simple AI Agent Workflow",
"workflows.list.error.fetching": "Error fetching workflows",
"workflows.shareModal.title": "Share '{name}'",
diff --git a/packages/frontend/@n8n/stores/src/constants.ts b/packages/frontend/@n8n/stores/src/constants.ts
index 3a7f5795dd..061d91afd8 100644
--- a/packages/frontend/@n8n/stores/src/constants.ts
+++ b/packages/frontend/@n8n/stores/src/constants.ts
@@ -34,5 +34,6 @@ export const STORES = {
AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection',
PERSONALIZED_TEMPLATES: 'personalizedTemplates',
EXPERIMENT_READY_TO_RUN_WORKFLOWS: 'readyToRunWorkflows',
+ EXPERIMENT_READY_TO_RUN_WORKFLOWS_V2: 'readyToRunWorkflowsV2',
EXPERIMENT_TEMPLATE_RECO_V2: 'templateRecoV2',
} as const;
diff --git a/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue b/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue
index d11bd4bcc9..a5d92c1a77 100644
--- a/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue
+++ b/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue
@@ -21,6 +21,7 @@ import type { IUser } from 'n8n-workflow';
import { type IconOrEmoji, isIconOrEmoji } from '@n8n/design-system/components/N8nIconPicker/types';
import { useUIStore } from '@/stores/ui.store';
import { PROJECT_DATA_STORES } from '@/features/dataStore/constants';
+import ReadyToRunV2Button from '@/experiments/readyToRunWorkflowsV2/components/ReadyToRunV2Button.vue';
const route = useRoute();
const router = useRouter();
@@ -32,6 +33,10 @@ const uiStore = useUIStore();
const projectPages = useProjectPages();
+const props = defineProps<{
+ hasActiveCallouts?: boolean;
+}>();
+
const emit = defineEmits<{
createFolder: [];
}>();
@@ -341,18 +346,21 @@ const onSelect = (action: string) => {
:disabled="!sourceControlStore.preferences.branchReadOnly"
:content="i18n.baseText('readOnlyEnv.cantAdd.any')"
>
-
-
-
+
diff --git a/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts b/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts
index 9d79832e7a..460f1cd107 100644
--- a/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts
+++ b/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts
@@ -10,6 +10,7 @@ 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 { useReadyToRunWorkflowsV2Store } from '@/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store';
@@ -48,6 +49,7 @@ export async function executionFinished(
const uiStore = useUIStore();
const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore();
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
+ const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store();
workflowsStore.lastAddedExecutingNode = null;
@@ -76,6 +78,15 @@ export async function executionFinished(
);
} else if (templateId.startsWith('37_onboarding_experiments_batch_aug11')) {
readyToRunWorkflowsStore.trackExecuteWorkflow(templateId.split('-').pop() ?? '', data.status);
+ } else if (
+ templateId === 'ready-to-run-ai-workflow-v1' ||
+ templateId === 'ready-to-run-ai-workflow-v2'
+ ) {
+ if (data.status === 'success') {
+ readyToRunWorkflowsV2Store.trackExecuteAiWorkflowSuccess();
+ } else {
+ readyToRunWorkflowsV2Store.trackExecuteAiWorkflow(data.status);
+ }
} else if (isPrebuiltAgentTemplateId(templateId)) {
telemetry.track('User executed pre-built Agent', {
template: templateId,
diff --git a/packages/frontend/editor-ui/src/constants.ts b/packages/frontend/editor-ui/src/constants.ts
index 2941c2d0c6..6a512970a1 100644
--- a/packages/frontend/editor-ui/src/constants.ts
+++ b/packages/frontend/editor-ui/src/constants.ts
@@ -810,6 +810,13 @@ export const TEMPLATE_RECO_V2 = {
variant: 'variant',
};
+export const READY_TO_RUN_V2_EXPERIMENT = {
+ name: '042_ready-to-run-worfklow_v2',
+ control: 'control',
+ variant1: 'variant-1-singlebox',
+ variant2: 'variant-2-twoboxes',
+};
+
export const EXPERIMENTS_TO_TRACK = [
WORKFLOW_BUILDER_EXPERIMENT.name,
EXTRA_TEMPLATE_LINKS_EXPERIMENT.name,
@@ -818,6 +825,7 @@ export const EXPERIMENTS_TO_TRACK = [
BATCH_11AUG_EXPERIMENT.name,
PRE_BUILT_AGENTS_EXPERIMENT.name,
TEMPLATE_RECO_V2.name,
+ READY_TO_RUN_V2_EXPERIMENT.name,
];
export const MFA_FORM = {
diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/ReadyToRunV2Button.vue b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/ReadyToRunV2Button.vue
new file mode 100644
index 0000000000..b59d71a6a5
--- /dev/null
+++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/ReadyToRunV2Button.vue
@@ -0,0 +1,74 @@
+
+
+
+
+ {{ i18n.baseText('workflows.empty.readyToRunV2') }}
+
+
diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/SimplifiedEmptyLayout.vue b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/SimplifiedEmptyLayout.vue
new file mode 100644
index 0000000000..fb97c1cf54
--- /dev/null
+++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/SimplifiedEmptyLayout.vue
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+ {{
+ currentUser.firstName
+ ? i18n.baseText('workflows.empty.heading', {
+ interpolate: { name: currentUser.firstName },
+ })
+ : i18n.baseText('workflows.empty.heading.userNotSetup')
+ }}
+
+
+ {{ emptyListDescription }}
+
+
+
+
+
+
+
+
+ {{ i18n.baseText('workflows.empty.readyToRunV2') }}
+
+
+
+
+
+
+
+
+ {{ i18n.baseText('workflows.empty.startFromScratch') }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/composables/useEmptyStateDetection.ts b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/composables/useEmptyStateDetection.ts
new file mode 100644
index 0000000000..364c18d7ce
--- /dev/null
+++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/composables/useEmptyStateDetection.ts
@@ -0,0 +1,66 @@
+import type { RouteLocationNormalized } from 'vue-router';
+import { useFoldersStore } from '@/stores/folders.store';
+import { useProjectPages } from '@/composables/useProjectPages';
+import { useRoute } from 'vue-router';
+
+/**
+ * Determines if the instance is truly empty and should show the simplified layout
+ */
+export function useEmptyStateDetection() {
+ const foldersStore = useFoldersStore();
+ const projectPages = useProjectPages();
+ const route = useRoute();
+
+ /**
+ * Checks if the current state qualifies as "truly empty"
+ * - No workflows exist in the instance
+ * - User is on the main workflows view (not in a specific folder)
+ * - User is on overview page or personal project workflows
+ * - No search filters are applied
+ * - Not currently refreshing data
+ */
+ const isTrulyEmpty = (currentRoute: RouteLocationNormalized = route) => {
+ const hasNoWorkflows = foldersStore.totalWorkflowCount === 0;
+ const isNotInSpecificFolder = !currentRoute.params?.folderId;
+ const isMainWorkflowsPage = projectPages.isOverviewSubPage || !projectPages.isSharedSubPage;
+
+ // Check for any search or filter parameters that would indicate filtering is active
+ const hasSearchQuery = !!currentRoute.query?.search;
+ const hasFilters = !!(
+ currentRoute.query?.status ||
+ currentRoute.query?.tags ||
+ currentRoute.query?.showArchived ||
+ currentRoute.query?.homeProject
+ );
+
+ return (
+ hasNoWorkflows &&
+ isNotInSpecificFolder &&
+ isMainWorkflowsPage &&
+ !hasSearchQuery &&
+ !hasFilters
+ );
+ };
+
+ /**
+ * Checks if we're in a state where the simplified layout should be shown
+ * This matches the logic from ResourcesListLayout's showEmptyState computed property
+ */
+ const shouldShowSimplifiedLayout = (
+ currentRoute: RouteLocationNormalized,
+ isFeatureEnabled: boolean,
+ loading: boolean,
+ ) => {
+ // Don't show simplified layout if loading or feature is disabled
+ if (loading || !isFeatureEnabled) {
+ return false;
+ }
+
+ return isTrulyEmpty(currentRoute);
+ };
+
+ return {
+ isTrulyEmpty,
+ shouldShowSimplifiedLayout,
+ };
+}
diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store.ts b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store.ts
new file mode 100644
index 0000000000..c186d53c77
--- /dev/null
+++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store.ts
@@ -0,0 +1,188 @@
+import { useTelemetry } from '@/composables/useTelemetry';
+import { useToast } from '@/composables/useToast';
+import { READY_TO_RUN_V2_EXPERIMENT, VIEWS } from '@/constants';
+import { useCloudPlanStore } from '@/stores/cloudPlan.store';
+import { useCredentialsStore } from '@/stores/credentials.store';
+import { usePostHog } from '@/stores/posthog.store';
+import { useSettingsStore } from '@/stores/settings.store';
+import { useUsersStore } from '@/stores/users.store';
+import { useI18n } from '@n8n/i18n';
+import { STORES } from '@n8n/stores';
+import { useLocalStorage } from '@vueuse/core';
+import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
+import { defineStore } from 'pinia';
+import { computed, ref } from 'vue';
+import { useRouter, type RouteLocationNormalized } from 'vue-router';
+import { READY_TO_RUN_WORKFLOW_V1 } from '../workflows/ai-workflow';
+import { READY_TO_RUN_WORKFLOW_V2 } from '../workflows/ai-workflow-v2';
+import { useEmptyStateDetection } from '../composables/useEmptyStateDetection';
+
+const LOCAL_STORAGE_CREDENTIAL_KEY = 'N8N_READY_TO_RUN_V2_OPENAI_CREDENTIAL_ID';
+
+export const useReadyToRunWorkflowsV2Store = defineStore(
+ STORES.EXPERIMENT_READY_TO_RUN_WORKFLOWS_V2,
+ () => {
+ const telemetry = useTelemetry();
+ const i18n = useI18n();
+ const toast = useToast();
+ const router = useRouter();
+ const credentialsStore = useCredentialsStore();
+ const usersStore = useUsersStore();
+ const settingsStore = useSettingsStore();
+ const posthogStore = usePostHog();
+ const cloudPlanStore = useCloudPlanStore();
+
+ const isFeatureEnabled = computed(() => {
+ const variant = posthogStore.getVariant(READY_TO_RUN_V2_EXPERIMENT.name);
+ return (
+ (variant === READY_TO_RUN_V2_EXPERIMENT.variant1 ||
+ variant === READY_TO_RUN_V2_EXPERIMENT.variant2) &&
+ cloudPlanStore.userIsTrialing
+ );
+ });
+
+ const claimedCredentialIdRef = useLocalStorage(LOCAL_STORAGE_CREDENTIAL_KEY, '');
+
+ const claimingCredits = ref(false);
+
+ const userHasOpenAiCredentialAlready = computed(
+ () =>
+ !!credentialsStore.allCredentials.filter(
+ (credential) => credential.type === OPEN_AI_API_CREDENTIAL_TYPE,
+ ).length,
+ );
+
+ const userHasClaimedAiCreditsAlready = computed(
+ () => !!usersStore.currentUser?.settings?.userClaimedAiCredits,
+ );
+
+ const userCanClaimOpenAiCredits = computed(() => {
+ return (
+ settingsStore.isAiCreditsEnabled &&
+ !userHasOpenAiCredentialAlready.value &&
+ !userHasClaimedAiCreditsAlready.value
+ );
+ });
+
+ const getCurrentVariant = () => {
+ return posthogStore.getVariant(READY_TO_RUN_V2_EXPERIMENT.name);
+ };
+
+ const trackExecuteAiWorkflow = (status: string) => {
+ const variant = getCurrentVariant();
+ telemetry.track('User executed ready to run AI workflow', {
+ status,
+ variant,
+ });
+ };
+
+ const trackExecuteAiWorkflowSuccess = () => {
+ const variant = getCurrentVariant();
+ telemetry.track('User executed ready to run AI workflow successfully', {
+ variant,
+ });
+ };
+
+ const claimFreeAiCredits = async (projectId?: string) => {
+ claimingCredits.value = true;
+
+ try {
+ const credential = await credentialsStore.claimFreeAiCredits(projectId);
+
+ if (usersStore?.currentUser?.settings) {
+ usersStore.currentUser.settings.userClaimedAiCredits = true;
+ }
+
+ claimedCredentialIdRef.value = credential.id;
+
+ telemetry.track('User claimed OpenAI credits');
+ return credential;
+ } catch (e) {
+ toast.showError(
+ e,
+ i18n.baseText('freeAi.credits.showError.claim.title'),
+ i18n.baseText('freeAi.credits.showError.claim.message'),
+ );
+ throw e;
+ } finally {
+ claimingCredits.value = false;
+ }
+ };
+
+ const openAiWorkflow = async (source: 'card' | 'button', parentFolderId?: string) => {
+ const variant = getCurrentVariant();
+ telemetry.track('User opened ready to run AI workflow', {
+ source,
+ variant,
+ });
+
+ const workflow =
+ variant === READY_TO_RUN_V2_EXPERIMENT.variant2
+ ? READY_TO_RUN_WORKFLOW_V2
+ : READY_TO_RUN_WORKFLOW_V1;
+
+ await router.push({
+ name: VIEWS.TEMPLATE_IMPORT,
+ params: { id: workflow.meta?.templateId },
+ query: { fromJson: 'true', parentFolderId },
+ });
+ };
+
+ const claimCreditsAndOpenWorkflow = async (
+ source: 'card' | 'button',
+ parentFolderId?: string,
+ projectId?: string,
+ ) => {
+ await claimFreeAiCredits(projectId);
+ await openAiWorkflow(source, parentFolderId);
+ };
+
+ const getCardVisibility = (
+ canCreate: boolean | undefined,
+ readOnlyEnv: boolean,
+ loading: boolean,
+ ) => {
+ return (
+ !loading &&
+ isFeatureEnabled.value &&
+ userCanClaimOpenAiCredits.value &&
+ !readOnlyEnv &&
+ canCreate
+ );
+ };
+
+ const getButtonVisibility = (
+ hasWorkflows: boolean,
+ canCreate: boolean | undefined,
+ readOnlyEnv: boolean,
+ ) => {
+ return (
+ isFeatureEnabled.value &&
+ userCanClaimOpenAiCredits.value &&
+ !readOnlyEnv &&
+ canCreate &&
+ hasWorkflows
+ );
+ };
+
+ const { shouldShowSimplifiedLayout } = useEmptyStateDetection();
+
+ const getSimplifiedLayoutVisibility = (route: RouteLocationNormalized, loading: boolean) => {
+ return shouldShowSimplifiedLayout(route, isFeatureEnabled.value, loading);
+ };
+
+ return {
+ isFeatureEnabled,
+ claimingCredits,
+ userCanClaimOpenAiCredits,
+ claimFreeAiCredits,
+ openAiWorkflow,
+ claimCreditsAndOpenWorkflow,
+ getCardVisibility,
+ getButtonVisibility,
+ getSimplifiedLayoutVisibility,
+ trackExecuteAiWorkflow,
+ trackExecuteAiWorkflowSuccess,
+ };
+ },
+);
diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/utils/workflowSamples.ts b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/utils/workflowSamples.ts
new file mode 100644
index 0000000000..f2bfd84ae4
--- /dev/null
+++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/utils/workflowSamples.ts
@@ -0,0 +1,48 @@
+import { ApplicationError, deepCopy, OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
+import type { WorkflowDataWithTemplateId } from '@/Interface';
+import { isWorkflowDataWithTemplateId } from '@/utils/templates/typeGuards';
+import { READY_TO_RUN_WORKFLOW_V1 } from '../workflows/ai-workflow';
+import { READY_TO_RUN_WORKFLOW_V2 } from '../workflows/ai-workflow-v2';
+
+const getWorkflowJson = (json: unknown): WorkflowDataWithTemplateId => {
+ if (!isWorkflowDataWithTemplateId(json)) {
+ throw new ApplicationError('Invalid workflow template JSON structure');
+ }
+
+ return json;
+};
+
+/**
+ * Injects OpenAI credentials into workflow template if available in localStorage
+ */
+const injectOpenAiCredentialIntoWorkflow = (
+ workflow: WorkflowDataWithTemplateId,
+): WorkflowDataWithTemplateId => {
+ const credentialId = localStorage.getItem('N8N_READY_TO_RUN_V2_OPENAI_CREDENTIAL_ID');
+
+ if (!credentialId) {
+ return workflow;
+ }
+
+ const clonedWorkflow = deepCopy(workflow);
+
+ if (clonedWorkflow.nodes) {
+ const openAiNode = clonedWorkflow.nodes.find((node) => node.name === 'OpenAI Model');
+ if (openAiNode) {
+ openAiNode.credentials ??= {};
+ openAiNode.credentials[OPEN_AI_API_CREDENTIAL_TYPE] = {
+ id: credentialId,
+ name: '',
+ };
+ }
+ }
+
+ return clonedWorkflow;
+};
+
+export const getReadyToRunAIWorkflows = (): WorkflowDataWithTemplateId[] => {
+ return [
+ injectOpenAiCredentialIntoWorkflow(getWorkflowJson(READY_TO_RUN_WORKFLOW_V1)),
+ injectOpenAiCredentialIntoWorkflow(getWorkflowJson(READY_TO_RUN_WORKFLOW_V2)),
+ ];
+};
diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow-v2.ts b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow-v2.ts
new file mode 100644
index 0000000000..837167ecf3
--- /dev/null
+++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow-v2.ts
@@ -0,0 +1,253 @@
+import type { WorkflowDataCreate } from '@n8n/rest-api-client';
+
+export const READY_TO_RUN_WORKFLOW_V2: WorkflowDataCreate = {
+ name: 'AI Agent workflow',
+ meta: { templateId: 'ready-to-run-ai-workflow-v2' },
+ nodes: [
+ {
+ parameters: {
+ url: 'https://www.theverge.com/rss/index.xml',
+ options: {},
+ },
+ type: 'n8n-nodes-base.rssFeedReadTool',
+ typeVersion: 1.2,
+ position: [-16, 768],
+ id: '303e9b4e-cc4e-4d8a-8ede-7550f070d212',
+ name: 'Get Tech News',
+ },
+ {
+ parameters: {
+ toolDescription: 'Reads the news',
+ url: '=https://feeds.bbci.co.uk/news/world/rss.xml',
+ options: {},
+ },
+ type: 'n8n-nodes-base.rssFeedReadTool',
+ typeVersion: 1.2,
+ position: [112, 768],
+ id: '4090a753-f131-40b1-87c3-cf74d5a7e325',
+ name: 'Get World News',
+ },
+ {
+ parameters: {
+ rule: {
+ interval: [
+ {
+ triggerAtHour: 7,
+ },
+ ],
+ },
+ },
+ type: 'n8n-nodes-base.scheduleTrigger',
+ typeVersion: 1.2,
+ position: [-560, 752],
+ id: '651543b5-0213-433f-8760-57d62b8d6d64',
+ name: 'Run every day at 7AM',
+ notesInFlow: true,
+ notes: 'Double-click to open',
+ },
+ {
+ parameters: {
+ assignments: {
+ assignments: [
+ {
+ id: '85b5c530-2c13-4424-ab83-05979bc879a5',
+ name: 'output',
+ value: '={{ $json.output }}',
+ type: 'string',
+ },
+ ],
+ },
+ options: {},
+ },
+ type: 'n8n-nodes-base.set',
+ typeVersion: 3.4,
+ position: [160, 544],
+ id: '99f7bb9e-f8c0-43ca-a9a8-a76634ac9611',
+ name: 'Output',
+ notesInFlow: true,
+ notes: 'Double-click to open',
+ },
+ {
+ parameters: {},
+ type: 'n8n-nodes-base.manualTrigger',
+ typeVersion: 1,
+ position: [-560, 544],
+ id: 'a0390291-6794-4673-9a6a-5c3d3a5d9e4b',
+ name: 'Click ‘Execute workflow’ to run',
+ },
+ {
+ parameters: {
+ content: '## ⚡ Start here:',
+ height: 224,
+ width: 224,
+ color: 7,
+ },
+ type: 'n8n-nodes-base.stickyNote',
+ typeVersion: 1,
+ position: [-624, 480],
+ id: 'fac5929f-e065-4474-96b1-7bcc06834238',
+ name: 'Sticky Note',
+ },
+ {
+ parameters: {
+ model: {
+ __rl: true,
+ mode: 'list',
+ value: 'gpt-4.1-mini',
+ },
+ options: {},
+ },
+ type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
+ typeVersion: 1.2,
+ position: [-272, 768],
+ id: 'b16482e8-0d48-4426-aa93-c3fee11dd3cd',
+ name: 'OpenAI Model',
+ notesInFlow: true,
+ credentials: {},
+ notes: 'Double-click to open',
+ },
+ {
+ parameters: {
+ content: '@[youtube](cMyOkQ4N-5M)',
+ height: 512,
+ width: 902,
+ color: 7,
+ },
+ type: 'n8n-nodes-base.stickyNote',
+ typeVersion: 1,
+ position: [-352, -96],
+ id: 'ec65e69e-77fa-4912-a4af-49e0a248e2c8',
+ name: 'Sticky Note3',
+ },
+ {
+ parameters: {
+ promptType: 'define',
+ text: '=Summarize world news and tech news from the last 24 hours. \nSkip your comments. \nThe titles should be "World news:" and "Tech news:" \nToday is {{ $today }}',
+ options: {},
+ },
+ type: '@n8n/n8n-nodes-langchain.agent',
+ typeVersion: 2.2,
+ position: [-272, 544],
+ id: '084d56aa-d157-4964-9073-b36d9d9589c5',
+ name: 'AI Summary Agent',
+ notesInFlow: true,
+ notes: 'Double-click to open',
+ },
+ {
+ parameters: {
+ content: '### Double click here to see the results:',
+ height: 240,
+ width: 192,
+ color: 7,
+ },
+ type: 'n8n-nodes-base.stickyNote',
+ typeVersion: 1,
+ position: [112, 464],
+ id: 'a4b7a69a-0db8-4b9b-a81d-fd83378043a3',
+ name: 'Sticky Note1',
+ },
+ {
+ parameters: {
+ content:
+ '### 📰 Daily AI Summary\n\n\nThis workflow gets the latest news and asks AI to summarize it for you.\n\n⭐ Bonus: Send the summary via email by connecting your Gmail account\n\n▶ Watch the video to get started ',
+ height: 272,
+ width: 224,
+ color: 5,
+ },
+ type: 'n8n-nodes-base.stickyNote',
+ typeVersion: 1,
+ position: [-624, 32],
+ id: '74d80857-5e63-47a8-8e86-8ecd10fd5f9e',
+ name: 'Sticky Note2',
+ },
+ {
+ parameters: {
+ subject: 'Your news daily summary',
+ emailType: 'text',
+ message: '={{ $json.output }}',
+ options: {},
+ },
+ type: 'n8n-nodes-base.gmail',
+ typeVersion: 2.1,
+ position: [432, 544],
+ id: '45625d0d-bf26-4379-9eed-7bbc8e5d87a5',
+ name: 'Send summary by email',
+ webhookId: '093b04f1-5e78-4926-9863-1b100d6f2ead',
+ notesInFlow: true,
+ credentials: {},
+ notes: 'Double-click to open',
+ },
+ ],
+ connections: {
+ 'Get Tech News': {
+ ai_tool: [
+ [
+ {
+ node: 'AI Summary Agent',
+ type: 'ai_tool',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ 'Get World News': {
+ ai_tool: [
+ [
+ {
+ node: 'AI Summary Agent',
+ type: 'ai_tool',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ 'Run every day at 7AM': {
+ main: [
+ [
+ {
+ node: 'AI Summary Agent',
+ type: 'main',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ 'Click ‘Execute workflow’ to run': {
+ main: [
+ [
+ {
+ node: 'AI Summary Agent',
+ type: 'main',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ 'OpenAI Model': {
+ ai_languageModel: [
+ [
+ {
+ node: 'AI Summary Agent',
+ type: 'ai_languageModel',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ 'AI Summary Agent': {
+ main: [
+ [
+ {
+ node: 'Output',
+ type: 'main',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ Output: {
+ main: [[]],
+ },
+ },
+ pinData: {},
+};
diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow.ts b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow.ts
new file mode 100644
index 0000000000..244572cdbe
--- /dev/null
+++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow.ts
@@ -0,0 +1,240 @@
+import type { WorkflowDataCreate } from '@n8n/rest-api-client';
+
+export const READY_TO_RUN_WORKFLOW_V1: WorkflowDataCreate = {
+ name: 'AI Agent workflow',
+ meta: { templateId: 'ready-to-run-ai-workflow-v1' },
+ nodes: [
+ {
+ parameters: {
+ url: 'https://www.theverge.com/rss/index.xml',
+ options: {},
+ },
+ type: 'n8n-nodes-base.rssFeedReadTool',
+ typeVersion: 1.2,
+ position: [-16, 768],
+ id: '303e9b4e-cc4e-4d8a-8ede-7550f070d212',
+ name: 'Get Tech News',
+ },
+ {
+ parameters: {
+ toolDescription: 'Reads the news',
+ url: '=https://feeds.bbci.co.uk/news/world/rss.xml',
+ options: {},
+ },
+ type: 'n8n-nodes-base.rssFeedReadTool',
+ typeVersion: 1.2,
+ position: [112, 768],
+ id: '4090a753-f131-40b1-87c3-cf74d5a7e325',
+ name: 'Get World News',
+ },
+ {
+ parameters: {
+ rule: {
+ interval: [
+ {
+ triggerAtHour: 7,
+ },
+ ],
+ },
+ },
+ type: 'n8n-nodes-base.scheduleTrigger',
+ typeVersion: 1.2,
+ position: [-560, 752],
+ id: '651543b5-0213-433f-8760-57d62b8d6d64',
+ name: 'Run every day at 7AM',
+ notesInFlow: true,
+ notes: 'Double-click to open',
+ },
+ {
+ parameters: {
+ assignments: {
+ assignments: [
+ {
+ id: '85b5c530-2c13-4424-ab83-05979bc879a5',
+ name: 'output',
+ value: '={{ $json.output }}',
+ type: 'string',
+ },
+ ],
+ },
+ options: {},
+ },
+ type: 'n8n-nodes-base.set',
+ typeVersion: 3.4,
+ position: [160, 544],
+ id: '99f7bb9e-f8c0-43ca-a9a8-a76634ac9611',
+ name: 'Output',
+ notesInFlow: true,
+ notes: 'Double-click to open',
+ },
+ {
+ parameters: {},
+ type: 'n8n-nodes-base.manualTrigger',
+ typeVersion: 1,
+ position: [-560, 544],
+ id: 'a0390291-6794-4673-9a6a-5c3d3a5d9e4b',
+ name: 'Click ‘Execute workflow’ to run',
+ },
+ {
+ parameters: {
+ content: '## ⚡ Start here:',
+ height: 240,
+ width: 224,
+ color: 7,
+ },
+ type: 'n8n-nodes-base.stickyNote',
+ typeVersion: 1,
+ position: [-624, 464],
+ id: 'fac5929f-e065-4474-96b1-7bcc06834238',
+ name: 'Sticky Note',
+ },
+ {
+ parameters: {
+ model: {
+ __rl: true,
+ mode: 'list',
+ value: 'gpt-4.1-mini',
+ },
+ options: {},
+ },
+ type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
+ typeVersion: 1.2,
+ position: [-272, 768],
+ id: 'b16482e8-0d48-4426-aa93-c3fee11dd3cd',
+ name: 'OpenAI Model',
+ notesInFlow: true,
+ credentials: {},
+ notes: 'Double-click to open',
+ },
+ {
+ parameters: {
+ promptType: 'define',
+ text: '=Summarize world news and tech news from the last 24 hours. \nSkip your comments. \nThe titles should be "World news:" and "Tech news:" \nToday is {{ $today }}',
+ options: {},
+ },
+ type: '@n8n/n8n-nodes-langchain.agent',
+ typeVersion: 2.2,
+ position: [-272, 544],
+ id: '084d56aa-d157-4964-9073-b36d9d9589c5',
+ name: 'AI Summary Agent',
+ notesInFlow: true,
+ notes: 'Double-click to open',
+ },
+ {
+ parameters: {
+ content: '### Double click here to see the results:',
+ height: 240,
+ width: 192,
+ color: 7,
+ },
+ type: 'n8n-nodes-base.stickyNote',
+ typeVersion: 1,
+ position: [112, 464],
+ id: 'a4b7a69a-0db8-4b9b-a81d-fd83378043a3',
+ name: 'Sticky Note1',
+ },
+ {
+ parameters: {
+ content:
+ '### 📰 Daily AI Summary\n\n\nThis workflow gets the latest news and asks AI to summarize it for you.\n\n⭐ Bonus: Send the summary via email by connecting your Gmail account\n\n\n\n@[youtube](cMyOkQ4N-5M)',
+ height: 432,
+ width: 384,
+ color: 5,
+ },
+ type: 'n8n-nodes-base.stickyNote',
+ typeVersion: 1,
+ position: [-1152, 464],
+ id: '74d80857-5e63-47a8-8e86-8ecd10fd5f9e',
+ name: 'Sticky Note2',
+ },
+ {
+ parameters: {
+ subject: 'Your news daily summary',
+ emailType: 'text',
+ message: '={{ $json.output }}',
+ options: {},
+ },
+ type: 'n8n-nodes-base.gmail',
+ typeVersion: 2.1,
+ position: [432, 544],
+ id: '45625d0d-bf26-4379-9eed-7bbc8e5d87a5',
+ name: 'Send summary by email',
+ webhookId: '093b04f1-5e78-4926-9863-1b100d6f2ead',
+ notesInFlow: true,
+ credentials: {},
+ notes: 'Double-click to open',
+ },
+ ],
+ connections: {
+ 'Get Tech News': {
+ ai_tool: [
+ [
+ {
+ node: 'AI Summary Agent',
+ type: 'ai_tool',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ 'Get World News': {
+ ai_tool: [
+ [
+ {
+ node: 'AI Summary Agent',
+ type: 'ai_tool',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ 'Run every day at 7AM': {
+ main: [
+ [
+ {
+ node: 'AI Summary Agent',
+ type: 'main',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ 'Click ‘Execute workflow’ to run': {
+ main: [
+ [
+ {
+ node: 'AI Summary Agent',
+ type: 'main',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ 'OpenAI Model': {
+ ai_languageModel: [
+ [
+ {
+ node: 'AI Summary Agent',
+ type: 'ai_languageModel',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ 'AI Summary Agent': {
+ main: [
+ [
+ {
+ node: 'Output',
+ type: 'main',
+ index: 0,
+ },
+ ],
+ ],
+ },
+ Output: {
+ main: [[]],
+ },
+ },
+ pinData: {},
+};
diff --git a/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.test.ts b/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.test.ts
index f4ff4bc4cc..3391bc386e 100644
--- a/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.test.ts
+++ b/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.test.ts
@@ -62,6 +62,7 @@ vi.mock('@/composables/useDocumentTitle', () => ({
const mockDebounce = {
callDebounced: vi.fn((fn) => fn()),
+ debounce: vi.fn(),
};
vi.mock('@/composables/useDebounce', () => ({
useDebounce: vi.fn(() => mockDebounce),
diff --git a/packages/frontend/editor-ui/src/utils/templates/workflowSamples.ts b/packages/frontend/editor-ui/src/utils/templates/workflowSamples.ts
index ec97cca210..bb0e2329f1 100644
--- a/packages/frontend/editor-ui/src/utils/templates/workflowSamples.ts
+++ b/packages/frontend/editor-ui/src/utils/templates/workflowSamples.ts
@@ -1,6 +1,7 @@
import { ApplicationError, type INodeTypeNameVersion } from 'n8n-workflow';
import type { WorkflowDataWithTemplateId } from '@/Interface';
import { isWorkflowDataWithTemplateId } from '@/utils/templates/typeGuards';
+import { getReadyToRunAIWorkflows } from '@/experiments/readyToRunWorkflowsV2/utils/workflowSamples';
/* eslint-disable import-x/extensions */
import easyAiStarterJson from '@/utils/templates/samples/easy_ai_starter.json';
@@ -192,6 +193,7 @@ export const getSampleWorkflowByTemplateId = (
const workflows = [
getEasyAiWorkflowJson(),
getRagStarterWorkflowJson(),
+ ...getReadyToRunAIWorkflows(),
...getPrebuiltAgents().map((agent) => agent.template),
...getTutorialTemplates().map((tutorial) => tutorial.template),
];
diff --git a/packages/frontend/editor-ui/src/views/WorkflowsView.vue b/packages/frontend/editor-ui/src/views/WorkflowsView.vue
index 35e41dda68..32c098b567 100644
--- a/packages/frontend/editor-ui/src/views/WorkflowsView.vue
+++ b/packages/frontend/editor-ui/src/views/WorkflowsView.vue
@@ -26,8 +26,10 @@ import SuggestedWorkflowCard from '@/experiments/personalizedTemplates/component
import SuggestedWorkflows from '@/experiments/personalizedTemplates/components/SuggestedWorkflows.vue';
import { usePersonalizedTemplatesStore } from '@/experiments/personalizedTemplates/stores/personalizedTemplates.store';
import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store';
+import { useReadyToRunWorkflowsV2Store } from '@/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store';
import TemplateRecommendationV2 from '@/experiments/templateRecoV2/components/TemplateRecommendationV2.vue';
import { usePersonalizedTemplatesV2Store } from '@/experiments/templateRecoV2/stores/templateRecoV2.store';
+import SimplifiedEmptyLayout from '@/experiments/readyToRunWorkflowsV2/components/SimplifiedEmptyLayout.vue';
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
import { useInsightsStore } from '@/features/insights/insights.store';
import type {
@@ -124,6 +126,7 @@ const templatesStore = useTemplatesStore();
const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore();
const personalizedTemplatesStore = usePersonalizedTemplatesStore();
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
+const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store();
const personalizedTemplatesV2Store = usePersonalizedTemplatesV2Store();
const documentTitle = useDocumentTitle();
@@ -437,6 +440,19 @@ const showPersonalizedTemplates = computed(
() => !loading.value && personalizedTemplatesStore.isFeatureEnabled(),
);
+const shouldUseSimplifiedLayout = computed(() => {
+ return readyToRunWorkflowsV2Store.getSimplifiedLayoutVisibility(route, loading.value);
+});
+
+const hasActiveCallouts = computed(() => {
+ return (
+ showPrebuiltAgentsCallout.value ||
+ showAIStarterCollectionCallout.value ||
+ showPersonalizedTemplates.value ||
+ showReadyToRunWorkflowsCallout.value
+ );
+});
+
/**
* WATCHERS, STORE SUBSCRIPTIONS AND EVENT BUS HANDLERS
*/
@@ -1713,7 +1729,10 @@ const onNameSubmit = async (name: string) => {
+
+
{
@mouseleave="folderHelpers.resetDropTarget"
>
-
+