diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json
index 965b8bc2721..3cd9fc81f06 100644
--- a/packages/frontend/@n8n/i18n/src/locales/en.json
+++ b/packages/frontend/@n8n/i18n/src/locales/en.json
@@ -8147,6 +8147,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 b230bfc30f5..1d6e9042d94 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
@@ -104,6 +104,10 @@ const STUBS = {
template: '',
props: ['theme', 'bold'],
},
+ PublicationIndicator: {
+ template: '{{ label }}',
+ props: ['label', 'variant'],
+ },
N8nActionToggle: {
name: 'N8nActionToggle',
template:
@@ -345,4 +349,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 a5507d93731..cf7693aa2be 100644
--- a/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue
+++ b/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue
@@ -56,6 +56,10 @@ 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 isUntitled = computed(() => props.agent.name === locale.baseText('agents.new.defaultName'));
const isMcpEnabled = computed(
@@ -225,7 +229,13 @@ async function toggleMCPAccess(enabled: boolean) {