diff --git a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.activate.yml b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.activate.yml index e26d54dcd64..36b4ef8954f 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.activate.yml +++ b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.activate.yml @@ -5,7 +5,10 @@ post: tags: - Workflow summary: Publish a workflow - description: Publish a workflow. In n8n v1, this action was termed activating a workflow. + description: >- + Deprecated: use POST /workflows/{id}/publish instead. Publish a workflow. In n8n v1, this + action was termed activating a workflow. + deprecated: true parameters: - $ref: '../schemas/parameters/workflowId.yml' requestBody: diff --git a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.deactivate.yml b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.deactivate.yml index 3a65ee377c5..f9cae3e8838 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.deactivate.yml +++ b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.deactivate.yml @@ -5,7 +5,8 @@ post: tags: - Workflow summary: Deactivate a workflow - description: Deactivate a workflow. + description: 'Deprecated: use POST /workflows/{id}/unpublish instead. Deactivate a workflow.' + deprecated: true parameters: - $ref: '../schemas/parameters/workflowId.yml' responses: diff --git a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts index 4ef5dd590b2..7654920af94 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts @@ -24,6 +24,7 @@ import { publicApiScope, projectScope, validCursor, + deprecated, } from '../../shared/middlewares/global.middleware'; import { encodeNextCursor } from '../../shared/services/pagination.service'; @@ -386,9 +387,11 @@ const workflowHandlers: WorkflowHandlers = { ], publishWorkflow, unpublishWorkflow, - // `activate`/`deactivate` are legacy aliases; they share the exact same code path. - activateWorkflow: publishWorkflow, - deactivateWorkflow: unpublishWorkflow, + activateWorkflow: [deprecated({ since: new Date('2026-07-23T00:00:00Z') }), ...publishWorkflow], + deactivateWorkflow: [ + deprecated({ since: new Date('2026-07-23T00:00:00Z') }), + ...unpublishWorkflow, + ], getWorkflowTags: [ publicApiScope('workflowTags:list'), projectScope('workflow:read', 'workflow'), diff --git a/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts b/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts index 542a9eae335..9fab0abdc19 100644 --- a/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts +++ b/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts @@ -88,6 +88,28 @@ export const validCursor = ( return next(); }; +export type DeprecationInfo = { + /** When the endpoint became deprecated. Emitted as an RFC 9745 `Deprecation` header. */ + since: Date; +}; + +/** + * Signals that an endpoint is deprecated via the RFC 9745 `Deprecation` response header. Callers + * pass a semantic `Date`; this middleware owns the on-the-wire formatting so the wire syntax + * (an RFC 9651 structured-field Date, `@`) never leaks to call sites. + * + * `since` is a fixed value owned by the caller — never derive it from `Date.now()`, so the header + * stays deterministic across requests. + */ +export const deprecated = ({ since }: DeprecationInfo) => { + const deprecation = `@${Math.floor(since.getTime() / 1000)}`; + + return (_req: Request, res: express.Response, next: express.NextFunction): void => { + res.setHeader('Deprecation', deprecation); + next(); + }; +}; + export type ScopeTaggedMiddleware = Middleware & { __apiKeyScope: ApiKeyScope; }; diff --git a/packages/cli/test/integration/public-api/workflows.test.ts b/packages/cli/test/integration/public-api/workflows.test.ts index d88909821f8..38291a4b554 100644 --- a/packages/cli/test/integration/public-api/workflows.test.ts +++ b/packages/cli/test/integration/public-api/workflows.test.ts @@ -1373,6 +1373,48 @@ describe.each(['deactivate', 'unpublish'])('POST /workflows/:id/%s', (action) => }); }); +describe('Deprecation header on legacy activate/deactivate aliases', () => { + // RFC 9745 structured-field Date: "@" followed by unix seconds. Assert the contract, not a + // specific date, so the test doesn't couple to the (release-owned) deprecation date. + const rfc9745Date = /^@\d+$/; + + test('should return a Deprecation header on activate', async () => { + const workflow = await createWorkflowWithTriggerAndHistory({}, member); + + const response = await authMemberAgent.post(`/workflows/${workflow.id}/activate`); + + expect(response.statusCode).toBe(200); + expect(response.headers.deprecation).toMatch(rfc9745Date); + }); + + test('should return a Deprecation header on deactivate', async () => { + const workflow = await createActiveWorkflow({}, member); + + const response = await authMemberAgent.post(`/workflows/${workflow.id}/deactivate`); + + expect(response.statusCode).toBe(200); + expect(response.headers.deprecation).toMatch(rfc9745Date); + }); + + test('should not return a Deprecation header on publish', async () => { + const workflow = await createWorkflowWithTriggerAndHistory({}, member); + + const response = await authMemberAgent.post(`/workflows/${workflow.id}/publish`); + + expect(response.statusCode).toBe(200); + expect(response.headers.deprecation).toBeUndefined(); + }); + + test('should not return a Deprecation header on unpublish', async () => { + const workflow = await createActiveWorkflow({}, member); + + const response = await authMemberAgent.post(`/workflows/${workflow.id}/unpublish`); + + expect(response.statusCode).toBe(200); + expect(response.headers.deprecation).toBeUndefined(); + }); +}); + describe('POST /workflows/:id/archive', () => { test('should fail due to missing API Key', testWithAPIKey('post', '/workflows/2/archive', null));