feat(API): Deprecate workflow activate/deactivate public API endpoints (#34771)

This commit is contained in:
Dmitrii
2026-07-23 13:41:42 +00:00
committed by GitHub
parent 4ead8600f9
commit 05c691b0b7
5 changed files with 76 additions and 5 deletions
@@ -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:
@@ -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:
@@ -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'),
@@ -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, `@<unix-seconds>`) 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;
};
@@ -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));