diff --git a/packages/@n8n/api-types/src/community-node-types.ts b/packages/@n8n/api-types/src/community-node-types.ts
index 4c01bb3fd41..9aed59b7a84 100644
--- a/packages/@n8n/api-types/src/community-node-types.ts
+++ b/packages/@n8n/api-types/src/community-node-types.ts
@@ -1,5 +1,12 @@
import type { INodeTypeDescription } from 'n8n-workflow';
+/** Temporary Gateway credits offer for a community node. Dates are ISO datetimes. */
+export type GatewayCreditsPromotion = {
+ text: string;
+ startsAt?: string | null;
+ endsAt?: string | null;
+};
+
export type CommunityNodeType = {
id: number;
authorGithubUrl: string;
@@ -19,4 +26,5 @@ export type CommunityNodeType = {
nodeDescription: INodeTypeDescription;
isInstalled: boolean;
nodeVersions?: Array<{ npmVersion: string; checksum: string }>;
+ gatewayCreditsPromotion?: GatewayCreditsPromotion | null;
};
diff --git a/packages/cli/src/modules/community-packages/community-node-types-utils.ts b/packages/cli/src/modules/community-packages/community-node-types-utils.ts
index 8aca512b496..2fd43093a73 100644
--- a/packages/cli/src/modules/community-packages/community-node-types-utils.ts
+++ b/packages/cli/src/modules/community-packages/community-node-types-utils.ts
@@ -1,3 +1,4 @@
+import type { GatewayCreditsPromotion } from '@n8n/api-types';
import type { INodeTypeDescription } from 'n8n-workflow';
import { paginatedRequest, type StrapiFilters } from '@/utils/strapi-utils';
@@ -21,6 +22,7 @@ export type StrapiCommunityNodeType = {
nodeDescription: INodeTypeDescription;
nodeVersions?: Array<{ npmVersion: string; checksum: string }>;
aiNodeSdkVersion?: number;
+ gatewayCreditsPromotion?: GatewayCreditsPromotion | null;
};
/**
diff --git a/packages/frontend/editor-ui/src/app/stores/nodeTypes.store.ts b/packages/frontend/editor-ui/src/app/stores/nodeTypes.store.ts
index 90f0fb9ccc7..b6560a529de 100644
--- a/packages/frontend/editor-ui/src/app/stores/nodeTypes.store.ts
+++ b/packages/frontend/editor-ui/src/app/stores/nodeTypes.store.ts
@@ -574,6 +574,7 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
visibleNodeTypesByInputConnectionTypeNames,
isConfigurableNode,
communityNodesAndActions,
+ vettedCommunityNodeTypes,
communityNodeType,
officialCommunityNodeTypes,
unofficialCommunityNodeTypes,
diff --git a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.vue b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.vue
index 811f8e59a18..a4faedfa9bb 100644
--- a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.vue
+++ b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialConfig.vue
@@ -55,6 +55,8 @@ import CredentialTypeSelector from './CredentialTypeSelector.vue';
import { useQuickConnect } from '../../quickConnect/composables/useQuickConnect';
import QuickConnectButton from '../../quickConnect/components/QuickConnectButton.vue';
import QuickConnectBanner from '../../quickConnect/components/QuickConnectBanner.vue';
+import { useGatewayCreditsPromotion } from '../../gatewayCreditsPromotion/useGatewayCreditsPromotion';
+import GatewayCreditsPromotion from '../../gatewayCreditsPromotion/GatewayCreditsPromotion.vue';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { ProjectTypes } from '@/features/collaboration/projects/projects.types';
import type { ProjectType } from '@/features/collaboration/projects/projects.types';
@@ -129,6 +131,9 @@ const chatPanelStore = useChatPanelStore();
const i18n = useI18n();
const telemetry = useTelemetry();
const { getQuickConnectOption } = useQuickConnect();
+const { promotionText } = useGatewayCreditsPromotion({
+ credentialType: () => props.credentialType.name,
+});
onBeforeMount(async () => {
uiStore.activeCredentialType = props.credentialType.name;
@@ -461,6 +466,7 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
:text="quickConnectBannerText"
:disclaimer="quickConnectOption?.disclaimer"
/>
+
{
+
+import { N8nCallout } from '@n8n/design-system';
+
+defineProps<{ text: string }>();
+
+
+
+
+ {{ text }}
+
+
diff --git a/packages/frontend/editor-ui/src/features/credentials/gatewayCreditsPromotion/useGatewayCreditsPromotion.test.ts b/packages/frontend/editor-ui/src/features/credentials/gatewayCreditsPromotion/useGatewayCreditsPromotion.test.ts
new file mode 100644
index 00000000000..f61c7682b05
--- /dev/null
+++ b/packages/frontend/editor-ui/src/features/credentials/gatewayCreditsPromotion/useGatewayCreditsPromotion.test.ts
@@ -0,0 +1,139 @@
+import { createTestingPinia } from '@pinia/testing';
+import { setActivePinia } from 'pinia';
+import type { CommunityNodeType, FrontendSettings, GatewayCreditsPromotion } from '@n8n/api-types';
+import type { INodeTypeDescription } from 'n8n-workflow';
+import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
+import { useSettingsStore } from '@n8n/stores/settings.store';
+import { useGatewayCreditsPromotion } from './useGatewayCreditsPromotion';
+
+const gateway = {
+ supportedNodes: [] as string[],
+ supportedCredentialTypes: ['typeSafeAiApi'],
+};
+
+vi.mock('@/app/stores/aiGateway.store', () => ({
+ useAiGatewayStore: () => ({
+ fetchConfig: vi.fn().mockResolvedValue(undefined),
+ isCredentialTypeSupported: (type: string) => gateway.supportedCredentialTypes.includes(type),
+ isNodeSupported: (name: string) => gateway.supportedNodes.includes(name.replace(/Tool$/, '')),
+ }),
+}));
+
+const setGatewayEnabled = (enabled: boolean) => {
+ useSettingsStore().settings = { aiGateway: { enabled } } as FrontendSettings;
+};
+
+const NODE_TYPE = '@typesafe-ai/n8n-nodes-typesafe-ai.typeSafeAi';
+const TEXT = 'Free until October 10, 2026.';
+
+// Strapi serves node descriptions without `credentials`; only installed node types have them.
+const communityNode = (name: string, promotion: GatewayCreditsPromotion) =>
+ ({
+ name,
+ nodeDescription: { name },
+ gatewayCreditsPromotion: promotion,
+ }) as CommunityNodeType;
+
+const setup = (promotion: GatewayCreditsPromotion) => {
+ const store = useNodeTypesStore();
+ store.vettedCommunityNodeTypes = new Map(
+ [NODE_TYPE, `${NODE_TYPE}Tool`].map((name) => [name, communityNode(name, promotion)]),
+ );
+ Object.assign(store, {
+ getNodeType: (name: string) =>
+ name === NODE_TYPE
+ ? ({ name, credentials: [{ name: 'typeSafeAiApi' }] } as INodeTypeDescription)
+ : null,
+ });
+};
+
+describe('useGatewayCreditsPromotion', () => {
+ beforeEach(() => {
+ setActivePinia(createTestingPinia());
+ setGatewayEnabled(true);
+ gateway.supportedNodes = [NODE_TYPE];
+ gateway.supportedCredentialTypes = ['typeSafeAiApi'];
+ vi.useFakeTimers({ now: new Date('2026-06-01T00:00:00Z') });
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('returns the text inside the window, by node type and by credential type', () => {
+ setup({ text: TEXT, startsAt: '2026-01-01T00:00:00Z', endsAt: '2026-10-10T00:00:00Z' });
+
+ expect(useGatewayCreditsPromotion({ nodeType: NODE_TYPE }).promotionText.value).toBe(TEXT);
+ expect(
+ useGatewayCreditsPromotion({ credentialType: 'typeSafeAiApi' }).promotionText.value,
+ ).toBe(TEXT);
+ });
+
+ it('returns the text for the Tool variant', () => {
+ setup({ text: TEXT });
+
+ expect(useGatewayCreditsPromotion({ nodeType: `${NODE_TYPE}Tool` }).promotionText.value).toBe(
+ TEXT,
+ );
+ });
+
+ it('returns nothing when Gateway credits are disabled', () => {
+ setup({ text: TEXT });
+ setGatewayEnabled(false);
+
+ expect(useGatewayCreditsPromotion({ nodeType: NODE_TYPE }).promotionText.value).toBeUndefined();
+ });
+
+ it('returns nothing when the gateway does not support the node', () => {
+ setup({ text: TEXT });
+ gateway.supportedNodes = [];
+
+ expect(useGatewayCreditsPromotion({ nodeType: NODE_TYPE }).promotionText.value).toBeUndefined();
+ });
+
+ it('returns nothing before startsAt', () => {
+ setup({ text: TEXT, startsAt: '2026-07-01T00:00:00Z' });
+
+ expect(useGatewayCreditsPromotion({ nodeType: NODE_TYPE }).promotionText.value).toBeUndefined();
+ });
+
+ it('returns nothing after endsAt', () => {
+ setup({ text: TEXT, endsAt: '2026-05-01T00:00:00Z' });
+
+ expect(useGatewayCreditsPromotion({ nodeType: NODE_TYPE }).promotionText.value).toBeUndefined();
+ });
+
+ it('returns nothing when the text is null', () => {
+ setup({ text: null } as unknown as GatewayCreditsPromotion);
+
+ expect(useGatewayCreditsPromotion({ nodeType: NODE_TYPE }).promotionText.value).toBeUndefined();
+ });
+
+ it('returns nothing by credential type when the gateway does not support the credential', () => {
+ setup({ text: TEXT });
+ gateway.supportedCredentialTypes = [];
+
+ expect(
+ useGatewayCreditsPromotion({ credentialType: 'typeSafeAiApi' }).promotionText.value,
+ ).toBeUndefined();
+ });
+
+ it('skips an expired promotion on another node with the same credential', () => {
+ const store = useNodeTypesStore();
+ store.vettedCommunityNodeTypes = new Map([
+ [
+ 'expired.node',
+ communityNode('expired.node', { text: 'old', endsAt: '2026-05-01T00:00:00Z' }),
+ ],
+ [NODE_TYPE, communityNode(NODE_TYPE, { text: TEXT })],
+ ]);
+ Object.assign(store, {
+ getNodeType: () => ({ credentials: [{ name: 'typeSafeAiApi' }] }) as INodeTypeDescription,
+ });
+ gateway.supportedNodes = [NODE_TYPE, 'expired.node'];
+
+ expect(
+ useGatewayCreditsPromotion({ credentialType: 'typeSafeAiApi' }).promotionText.value,
+ ).toBe(TEXT);
+ });
+});
diff --git a/packages/frontend/editor-ui/src/features/credentials/gatewayCreditsPromotion/useGatewayCreditsPromotion.ts b/packages/frontend/editor-ui/src/features/credentials/gatewayCreditsPromotion/useGatewayCreditsPromotion.ts
new file mode 100644
index 00000000000..72ac1e28f62
--- /dev/null
+++ b/packages/frontend/editor-ui/src/features/credentials/gatewayCreditsPromotion/useGatewayCreditsPromotion.ts
@@ -0,0 +1,71 @@
+import { computed, toValue, type MaybeRefOrGetter } from 'vue';
+import type { GatewayCreditsPromotion } from '@n8n/api-types';
+import { useSettingsStore } from '@n8n/stores/settings.store';
+import { useAiGatewayStore } from '@/app/stores/aiGateway.store';
+import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
+
+const isActive = (promotion: GatewayCreditsPromotion | null | undefined, now: number) =>
+ !!promotion?.text?.trim() &&
+ (!promotion.startsAt || now >= Date.parse(promotion.startsAt)) &&
+ (!promotion.endsAt || now < Date.parse(promotion.endsAt));
+
+/**
+ * Gateway credits promotion of a community node, looked up by node type or,
+ * when there is no node type, by credential type.
+ */
+export function useGatewayCreditsPromotion({
+ nodeType,
+ credentialType,
+}: {
+ nodeType?: MaybeRefOrGetter;
+ credentialType?: MaybeRefOrGetter;
+}) {
+ const nodeTypesStore = useNodeTypesStore();
+ const settingsStore = useSettingsStore();
+ const aiGatewayStore = useAiGatewayStore();
+ const isEnabled = computed(() => settingsStore.isAiGatewayEnabled);
+
+ if (isEnabled.value) void aiGatewayStore.fetchConfig();
+
+ // The credential modal can open before a workflow loads the community node catalog.
+ if (
+ isEnabled.value &&
+ credentialType !== undefined &&
+ nodeTypesStore.vettedCommunityNodeTypes.size === 0
+ ) {
+ void nodeTypesStore.fetchCommunityNodePreviews();
+ }
+
+ const communityNode = computed(() => {
+ const nodeTypeName = toValue(nodeType);
+ if (nodeTypeName) return nodeTypesStore.communityNodeType(nodeTypeName);
+
+ const credentialTypeName = toValue(credentialType);
+ if (!credentialTypeName || !aiGatewayStore.isCredentialTypeSupported(credentialTypeName)) {
+ return undefined;
+ }
+ // Strapi serves node descriptions without `credentials`, so read them from the installed node type.
+ return [...nodeTypesStore.vettedCommunityNodeTypes.values()].find(
+ ({ name, gatewayCreditsPromotion }) =>
+ isActive(gatewayCreditsPromotion, Date.now()) &&
+ nodeTypesStore
+ .getNodeType(name)
+ ?.credentials?.some(({ name: credential }) => credential === credentialTypeName),
+ );
+ });
+
+ const promotionText = computed(() => {
+ const node = communityNode.value;
+ if (
+ !isEnabled.value ||
+ !node ||
+ !isActive(node.gatewayCreditsPromotion, Date.now()) ||
+ !aiGatewayStore.isNodeSupported(node.name)
+ ) {
+ return undefined;
+ }
+ return node.gatewayCreditsPromotion?.text;
+ });
+
+ return { promotionText };
+}
diff --git a/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.vue b/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.vue
index cc6a0dc22ec..6e1a24b6c33 100644
--- a/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.vue
+++ b/packages/frontend/editor-ui/src/features/ndv/settings/components/NodeSettings.vue
@@ -65,6 +65,8 @@ import { useResizeObserver } from '@vueuse/core';
import CommunityNodeFooter from '@/features/settings/communityNodes/components/nodeCreator/CommunityNodeFooter.vue';
import CommunityNodeUpdateInfo from '@/features/settings/communityNodes/components/nodeCreator/CommunityNodeUpdateInfo.vue';
import QuickConnectBanner from '@/features/credentials/quickConnect/components/QuickConnectBanner.vue';
+import { useGatewayCreditsPromotion } from '@/features/credentials/gatewayCreditsPromotion/useGatewayCreditsPromotion';
+import GatewayCreditsPromotion from '@/features/credentials/gatewayCreditsPromotion/GatewayCreditsPromotion.vue';
import { useQuickConnect } from '@/features/credentials/quickConnect/composables/useQuickConnect';
import { N8nBlockUi, N8nIcon, N8nNotice, N8nText } from '@n8n/design-system';
@@ -281,6 +283,12 @@ const showQuickConnectBanner = computed(
!props.isEmbeddedInCanvas,
);
+const { promotionText } = useGatewayCreditsPromotion({ nodeType: () => node.value?.type });
+const showGatewayCreditsPromotion = computed(
+ () =>
+ !!promotionText.value && !isReadOnly.value && !isDemoPreview.value && !props.isEmbeddedInCanvas,
+);
+
const showNoParametersNotice = computed(
() =>
!isDisplayingCredentials.value &&
@@ -785,6 +793,11 @@ function handleSelectAction(params: INodeParameters) {
:disclaimer="quickConnect?.disclaimer"
:class="$style.quickConnectBanner"
/>
+
({
useNodeTypesStore: vi.fn(() => ({
getCommunityNodeAttributes,
+ communityNodeType: vi.fn(),
getNodeType: vi.fn(),
getAllNodeTypes: vi.fn().mockReturnValue({
nodeTypes: {},
diff --git a/packages/frontend/editor-ui/src/features/settings/communityNodes/components/nodeCreator/CommunityNodeInfo.vue b/packages/frontend/editor-ui/src/features/settings/communityNodes/components/nodeCreator/CommunityNodeInfo.vue
index 384fff2e03a..b2fcec08efd 100644
--- a/packages/frontend/editor-ui/src/features/settings/communityNodes/components/nodeCreator/CommunityNodeInfo.vue
+++ b/packages/frontend/editor-ui/src/features/settings/communityNodes/components/nodeCreator/CommunityNodeInfo.vue
@@ -12,6 +12,8 @@ import { N8nIcon, N8nText, N8nTooltip } from '@n8n/design-system';
import CommunityNodeUpdateInfo from './CommunityNodeUpdateInfo.vue';
import { useQuickConnect } from '@/features/credentials/quickConnect/composables/useQuickConnect';
import QuickConnectBanner from '@/features/credentials/quickConnect/components/QuickConnectBanner.vue';
+import { useGatewayCreditsPromotion } from '@/features/credentials/gatewayCreditsPromotion/useGatewayCreditsPromotion';
+import GatewayCreditsPromotion from '@/features/credentials/gatewayCreditsPromotion/GatewayCreditsPromotion.vue';
const { activeViewStack } = useViewStacks();
@@ -34,6 +36,7 @@ const quickConnect = computed(() => {
const pkg = packageName.value;
return pkg ? getQuickConnectOptionByPackageName(pkg) : undefined;
});
+const { promotionText } = useGatewayCreditsPromotion({ nodeType: nodeTypeName });
const nodeTypesStore = useNodeTypesStore();
@@ -180,6 +183,11 @@ onMounted(async () => {
:text="quickConnect?.text"
:disclaimer="quickConnect?.disclaimer"
/>
+
@@ -202,6 +210,9 @@ onMounted(async () => {
.description {
margin: var(--spacing--md) 0;
}
+.gatewayCreditsPromotion {
+ margin-top: var(--spacing--2xs);
+}
.separator {
height: var(--border-width);
background: var(--color--foreground);
diff --git a/packages/frontend/editor-ui/src/features/shared/nodeCreator/components/Modes/ActionsMode.vue b/packages/frontend/editor-ui/src/features/shared/nodeCreator/components/Modes/ActionsMode.vue
index 7e4623b71e3..f1244319a74 100644
--- a/packages/frontend/editor-ui/src/features/shared/nodeCreator/components/Modes/ActionsMode.vue
+++ b/packages/frontend/editor-ui/src/features/shared/nodeCreator/components/Modes/ActionsMode.vue
@@ -37,6 +37,8 @@ import CommunityNodeFooter from '@/features/settings/communityNodes/components/n
import { useCalloutHelpers } from '@/app/composables/useCalloutHelpers';
import { useQuickConnect } from '@/features/credentials/quickConnect/composables/useQuickConnect';
import QuickConnectBanner from '@/features/credentials/quickConnect/components/QuickConnectBanner.vue';
+import { useGatewayCreditsPromotion } from '@/features/credentials/gatewayCreditsPromotion/useGatewayCreditsPromotion';
+import GatewayCreditsPromotion from '@/features/credentials/gatewayCreditsPromotion/GatewayCreditsPromotion.vue';
import { N8nCallout, N8nInfoTip } from '@n8n/design-system';
const emit = defineEmits<{
@@ -117,6 +119,14 @@ const quickConnect = computed(() => {
return null;
});
+// CommunityNodeInfo shows the promotion when community node details are open.
+const { promotionText } = useGatewayCreditsPromotion({
+ nodeType: () => {
+ const items = useViewStacks().activeViewStack.items;
+ return !communityNodeDetails.value && items?.length ? items[0].key : undefined;
+ },
+});
+
const hasNoTriggerActions = computed(
() =>
parseCategoryActions(
@@ -273,8 +283,13 @@ const callouts = computed(() => []);
-
-
+
+
+
@@ -423,6 +438,9 @@ const callouts = computed(() => []);
z-index: 1;
}
.banner {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing--2xs);
margin: var(--spacing--sm);
margin-top: 0;
}