feat(editor): Implement Ready to Run Workflows V2 experiment (no-changelog) (#19468)

This commit is contained in:
Romeo Balta
2025-09-15 11:21:25 +01:00
committed by GitHub
parent 796e44eace
commit 267a62d9c1
16 changed files with 1160 additions and 13 deletions

View File

@@ -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,

View File

@@ -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": "<a href=\"#\">Back to Personal</a>",
"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}'",

View File

@@ -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;

View File

@@ -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')"
>
<ProjectCreateResource
data-test-id="add-resource-buttons"
:actions="menu"
:disabled="sourceControlStore.preferences.branchReadOnly"
@action="onSelect"
>
<N8nButton
data-test-id="add-resource-workflow"
v-bind="createWorkflowButton"
@click="onSelect(ACTION_TYPES.WORKFLOW)"
/>
</ProjectCreateResource>
<div style="display: flex; gap: var(--spacing-xs); align-items: center">
<ReadyToRunV2Button :has-active-callouts="props.hasActiveCallouts" />
<ProjectCreateResource
data-test-id="add-resource-buttons"
:actions="menu"
:disabled="sourceControlStore.preferences.branchReadOnly"
@action="onSelect"
>
<N8nButton
data-test-id="add-resource-workflow"
v-bind="createWorkflowButton"
@click="onSelect(ACTION_TYPES.WORKFLOW)"
/>
</ProjectCreateResource>
</div>
</N8nTooltip>
</div>
</div>

View File

@@ -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,

View File

@@ -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 = {

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { N8nButton } from '@n8n/design-system';
import { useI18n } from '@n8n/i18n';
import { getResourcePermissions } from '@n8n/permissions';
import { useProjectPages } from '@/composables/useProjectPages';
import { useProjectsStore } from '@/stores/projects.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useFoldersStore } from '@/stores/folders.store';
import { useToast } from '@/composables/useToast';
import { useReadyToRunWorkflowsV2Store } from '../stores/readyToRunWorkflowsV2.store';
const props = defineProps<{
hasActiveCallouts?: boolean;
}>();
const route = useRoute();
const i18n = useI18n();
const toast = useToast();
const projectPages = useProjectPages();
const projectsStore = useProjectsStore();
const sourceControlStore = useSourceControlStore();
const foldersStore = useFoldersStore();
const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store();
const projectPermissions = computed(() => {
return getResourcePermissions(
projectsStore.currentProject?.scopes ?? projectsStore.personalProject?.scopes,
);
});
const showButton = computed(() => {
return (
readyToRunWorkflowsV2Store.getButtonVisibility(
foldersStore.totalWorkflowCount > 0, // Has workflows
projectPermissions.value.workflow.create,
sourceControlStore.preferences.branchReadOnly,
) && !props.hasActiveCallouts // Hide when callouts are shown
);
});
const handleClick = async () => {
const projectId = projectPages.isOverviewSubPage
? projectsStore.personalProject?.id
: (route.params.projectId as string);
try {
await readyToRunWorkflowsV2Store.claimCreditsAndOpenWorkflow(
'button',
route.params.folderId as string,
projectId,
);
} catch (error) {
toast.showError(error, i18n.baseText('generic.error'));
}
};
</script>
<template>
<N8nButton
v-if="showButton"
data-test-id="ready-to-run-v2-button"
type="secondary"
icon="sparkles"
:loading="readyToRunWorkflowsV2Store.claimingCredits"
:disabled="
sourceControlStore.preferences.branchReadOnly || readyToRunWorkflowsV2Store.claimingCredits
"
@click="handleClick"
>
{{ i18n.baseText('workflows.empty.readyToRunV2') }}
</N8nButton>
</template>

View File

@@ -0,0 +1,222 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { N8nCard, N8nHeading, N8nText, N8nIcon } from '@n8n/design-system';
import { useI18n } from '@n8n/i18n';
import { useUsersStore } from '@/stores/users.store';
import { useProjectsStore } from '@/stores/projects.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { getResourcePermissions } from '@n8n/permissions';
import { useProjectPages } from '@/composables/useProjectPages';
import { useToast } from '@/composables/useToast';
import { useReadyToRunWorkflowsV2Store } from '../stores/readyToRunWorkflowsV2.store';
import type { IUser } from 'n8n-workflow';
const route = useRoute();
const i18n = useI18n();
const toast = useToast();
const usersStore = useUsersStore();
const projectsStore = useProjectsStore();
const sourceControlStore = useSourceControlStore();
const projectPages = useProjectPages();
const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store();
const currentUser = computed(() => usersStore.currentUser ?? ({} as IUser));
const personalProject = computed(() => projectsStore.personalProject);
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
const projectPermissions = computed(() => {
return getResourcePermissions(
projectsStore.currentProject?.scopes ?? personalProject.value?.scopes,
);
});
const emptyListDescription = computed(() => {
if (readOnlyEnv.value) {
return i18n.baseText('workflows.empty.description.readOnlyEnv');
} else if (!projectPermissions.value.workflow.create) {
return i18n.baseText('workflows.empty.description.noPermission');
} else {
return i18n.baseText('workflows.empty.description');
}
});
const showReadyToRunV2Card = computed(() => {
return readyToRunWorkflowsV2Store.getCardVisibility(
projectPermissions.value.workflow.create,
readOnlyEnv.value,
false, // loading is false in simplified layout
);
});
const handleReadyToRunV2Click = async () => {
const projectId = projectPages.isOverviewSubPage
? personalProject.value?.id
: (route.params.projectId as string);
try {
await readyToRunWorkflowsV2Store.claimCreditsAndOpenWorkflow(
'card',
route.params.folderId as string,
projectId,
);
} catch (error) {
toast.showError(error, i18n.baseText('generic.error'));
}
};
const addWorkflow = () => {
emit('click:add');
};
const emit = defineEmits<{
'click:add': [];
}>();
</script>
<template>
<div :class="$style.simplifiedLayout">
<div :class="$style.content">
<div :class="$style.welcome">
<N8nHeading tag="h1" size="2xlarge" :class="$style.welcomeTitle">
{{
currentUser.firstName
? i18n.baseText('workflows.empty.heading', {
interpolate: { name: currentUser.firstName },
})
: i18n.baseText('workflows.empty.heading.userNotSetup')
}}
</N8nHeading>
<N8nText size="large" color="text-base" :class="$style.welcomeDescription">
{{ emptyListDescription }}
</N8nText>
</div>
<div
v-if="!readOnlyEnv && projectPermissions.workflow.create"
:class="$style.actionsContainer"
>
<N8nCard
v-if="showReadyToRunV2Card"
:class="$style.actionCard"
hoverable
data-test-id="ready-to-run-v2-card"
@click="handleReadyToRunV2Click"
>
<div :class="$style.cardContent">
<N8nIcon
:class="$style.cardIcon"
icon="sparkles"
color="foreground-dark"
:stroke-width="1.5"
/>
<N8nText size="large" class="mt-xs">
{{ i18n.baseText('workflows.empty.readyToRunV2') }}
</N8nText>
</div>
</N8nCard>
<N8nCard
:class="$style.actionCard"
hoverable
data-test-id="new-workflow-card"
@click="addWorkflow"
>
<div :class="$style.cardContent">
<N8nIcon
:class="$style.cardIcon"
icon="file"
color="foreground-dark"
:stroke-width="1.5"
/>
<N8nText size="large" class="mt-xs">
{{ i18n.baseText('workflows.empty.startFromScratch') }}
</N8nText>
</div>
</N8nCard>
</div>
</div>
</div>
</template>
<style lang="scss" module>
.simplifiedLayout {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.header {
position: fixed;
top: var(--spacing-l);
left: var(--spacing-l);
opacity: 0.6;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
max-width: 600px;
text-align: center;
}
.welcome {
margin-bottom: var(--spacing-2xl);
}
.welcomeTitle {
margin-bottom: var(--spacing-m);
}
.welcomeDescription {
max-width: 480px;
}
.actionsContainer {
display: flex;
gap: var(--spacing-s);
justify-content: center;
flex-wrap: wrap;
}
.actionCard {
width: 192px;
height: 230px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
.cardIcon svg {
color: var(--color-primary);
}
}
}
.cardContent {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-m);
}
.cardIcon {
font-size: 48px;
margin-bottom: var(--spacing-xs);
svg {
transition: color 0.3s ease;
}
}
</style>

View File

@@ -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,
};
}

View File

@@ -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,
};
},
);

View File

@@ -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)),
];
};

View File

@@ -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: {},
};

View File

@@ -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: {},
};

View File

@@ -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),

View File

@@ -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),
];

View File

@@ -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) => {
</script>
<template>
<SimplifiedEmptyLayout v-if="shouldUseSimplifiedLayout" @click:add="addWorkflow" />
<ResourcesListLayout
v-else
v-model:filters="filters"
resource-key="workflows"
type="list-paginated"
@@ -1735,7 +1754,10 @@ const onNameSubmit = async (name: string) => {
@mouseleave="folderHelpers.resetDropTarget"
>
<template #header>
<ProjectHeader @create-folder="createFolderInCurrent">
<ProjectHeader
:has-active-callouts="hasActiveCallouts"
@create-folder="createFolderInCurrent"
>
<InsightsSummary
v-if="showInsights"
:loading="insightsStore.weeklySummary.isLoading"