diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json
index ecd19021806..7161fd37234 100644
--- a/packages/frontend/@n8n/i18n/src/locales/en.json
+++ b/packages/frontend/@n8n/i18n/src/locales/en.json
@@ -8038,6 +8038,7 @@
"agents.backToWorkflow": "Back to workflow",
"agents.heading": "Agents",
"agents.list.published": "Published",
+ "agents.list.changesToPublish": "Changes to publish",
"agents.list.noDescription": "No description",
"agents.list.updatedAt": "Updated {date}",
"agents.list.updated": "Last updated",
diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCard.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCard.test.ts
index 09746c419a3..55a2e77df53 100644
--- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCard.test.ts
+++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCard.test.ts
@@ -102,6 +102,10 @@ const STUBS = {
template: '',
props: ['theme', 'bold'],
},
+ PublicationIndicator: {
+ template: '{{ label }}',
+ props: ['label', 'variant'],
+ },
N8nActionToggle: {
name: 'N8nActionToggle',
template:
@@ -328,4 +332,51 @@ describe('AgentCard', () => {
'agents.list.actions.disableMCPAccess',
);
});
+
+ describe('publication indicator', () => {
+ const findIndicator = (wrapper: Awaited>) =>
+ wrapper.find('[data-test-id="agent-card-publish-indicator"]');
+
+ it('is hidden on an unpublished agent', async () => {
+ const wrapper = await renderComponent(createAgent({ activeVersionId: null }));
+
+ expect(findIndicator(wrapper).exists()).toBe(false);
+ });
+
+ it('shows Published with a success dot when the draft matches the published version', async () => {
+ const wrapper = await renderComponent(
+ createAgent({ versionId: 'v1', activeVersionId: 'v1', activeVersion }),
+ );
+
+ const indicator = findIndicator(wrapper);
+ expect(indicator.text()).toBe('agents.list.published');
+ expect(indicator.attributes('data-state')).toBe('published');
+ expect(indicator.attributes('data-variant')).toBe('success');
+ });
+
+ it('shows Changes to publish with a warning dot when the draft differs from the published version', async () => {
+ const wrapper = await renderComponent(
+ createAgent({ versionId: 'v2', activeVersionId: 'v1', activeVersion }),
+ );
+
+ const indicator = findIndicator(wrapper);
+ expect(indicator.text()).toBe('agents.list.changesToPublish');
+ expect(indicator.attributes('data-state')).toBe('changes-to-publish');
+ expect(indicator.attributes('data-variant')).toBe('warning');
+ });
+
+ it('returns to Published after the agent is republished', async () => {
+ const wrapper = await renderComponent(
+ createAgent({ versionId: 'v2', activeVersionId: 'v1', activeVersion }),
+ );
+
+ await wrapper.setProps({
+ agent: createAgent({ versionId: 'v2', activeVersionId: 'v2', activeVersion }),
+ });
+
+ const indicator = findIndicator(wrapper);
+ expect(indicator.text()).toBe('agents.list.published');
+ expect(indicator.attributes('data-variant')).toBe('success');
+ });
+ });
});
diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue
index 8f1ffbba127..ddb627c753a 100644
--- a/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue
+++ b/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue
@@ -55,6 +55,11 @@ const { canCreate, canUpdate, canDelete, canPublish, canUnpublish } = useAgentPe
const isPublished = computed(() => props.agent.activeVersionId !== null);
+// Saving a draft bumps `versionId`, so a mismatch means unpublished edits.
+const hasUnpublishedChanges = computed(
+ () => isPublished.value && props.agent.versionId !== props.agent.activeVersionId,
+);
+
const isMcpEnabled = computed(
() => settingsStore.isModuleActive('mcp') && !!settingsStore.moduleSettings.mcp?.mcpAccessEnabled,
);
@@ -216,7 +221,13 @@ async function toggleMCPAccess(enabled: boolean) {