From 6c1edac204abb4d241850fbeef95ea1ea720fc5c Mon Sep 17 00:00:00 2001 From: "n8n-cat-bot[bot]" <283985454+n8n-cat-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:48:16 +0000 Subject: [PATCH] fix(API): Accept workflow descriptions on creation (#39705) Co-authored-by: n8n-cat-bot[bot] --- .../__tests__/create-workflow-public.dto.test.ts | 16 ++++++++++++++++ .../dto/workflows/base-workflow-public.dto.ts | 8 +------- .../dto/workflows/create-workflow-public.dto.ts | 13 ++++++++++--- .../src/dto/workflows/workflow-public.openapi.ts | 4 ++++ .../spec/paths/createWorkflow.generated.yml | 4 ++++ .../integration/public-api/workflows.test.ts | 13 +++++++++++++ 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/packages/@n8n/api-types/src/dto/workflows/__tests__/create-workflow-public.dto.test.ts b/packages/@n8n/api-types/src/dto/workflows/__tests__/create-workflow-public.dto.test.ts index 524e54c4377..949b1989095 100644 --- a/packages/@n8n/api-types/src/dto/workflows/__tests__/create-workflow-public.dto.test.ts +++ b/packages/@n8n/api-types/src/dto/workflows/__tests__/create-workflow-public.dto.test.ts @@ -17,6 +17,22 @@ const validPayload = { }; describe('CreateWorkflowPublicDto', () => { + test('accepts an optional workflow description', () => { + const payload = { ...validPayload, description: 'What this workflow does' }; + + expect(CreateWorkflowPublicDto.safeParse(payload)).toMatchObject({ + success: true, + data: { description: payload.description }, + }); + expect(CreateWorkflowPublicDto.safeParse(validPayload).success).toBe(true); + }); + + test('rejects a non-string workflow description', () => { + expect(CreateWorkflowPublicDto.safeParse({ ...validPayload, description: 42 }).success).toBe( + false, + ); + }); + test('rejects an unknown key through both the DTO and its schema', () => { const payload = { ...validPayload, notAWorkflowField: 'x' }; diff --git a/packages/@n8n/api-types/src/dto/workflows/base-workflow-public.dto.ts b/packages/@n8n/api-types/src/dto/workflows/base-workflow-public.dto.ts index 9072423c46b..46e400e9d7b 100644 --- a/packages/@n8n/api-types/src/dto/workflows/base-workflow-public.dto.ts +++ b/packages/@n8n/api-types/src/dto/workflows/base-workflow-public.dto.ts @@ -151,13 +151,7 @@ const staticDataWritePublicSchema = z .nullable() .openapi(workflowCreateFieldDocs.staticData); -/** - * The public workflow write surface, shared by the create and update request bodies. - * - * `CreateWorkflowPublicDto` uses it as-is. `UpdateWorkflowPublicDto` omits `projectId` from its - * shape, since a project is only chosen when a workflow is created, and adds `description`, which - * only the update body has ever accepted. - */ +// Each route adds `description` to its own DTO so their request schemas stay independent. export const workflowWritePublicShape = { id: readOnlyPublicSchema(workflowCreateReadOnlyFieldDocs.id), name: z.string().openapi(workflowCreateFieldDocs.name), diff --git a/packages/@n8n/api-types/src/dto/workflows/create-workflow-public.dto.ts b/packages/@n8n/api-types/src/dto/workflows/create-workflow-public.dto.ts index d7875c32a16..6a5b1029620 100644 --- a/packages/@n8n/api-types/src/dto/workflows/create-workflow-public.dto.ts +++ b/packages/@n8n/api-types/src/dto/workflows/create-workflow-public.dto.ts @@ -1,6 +1,13 @@ +import { z } from 'zod'; + import { workflowWritePublicShape } from './base-workflow-public.dto'; +import { workflowCreateFieldDocs } from './workflow-public.openapi'; import { Z } from '../../zod-class'; -export class CreateWorkflowPublicDto extends Z.class(workflowWritePublicShape, { - strict: true, -}) {} +export class CreateWorkflowPublicDto extends Z.class( + { + ...workflowWritePublicShape, + description: z.string().optional().openapi(workflowCreateFieldDocs.description), + }, + { strict: true }, +) {} diff --git a/packages/@n8n/api-types/src/dto/workflows/workflow-public.openapi.ts b/packages/@n8n/api-types/src/dto/workflows/workflow-public.openapi.ts index 530f1bbcfe0..ffc842de768 100644 --- a/packages/@n8n/api-types/src/dto/workflows/workflow-public.openapi.ts +++ b/packages/@n8n/api-types/src/dto/workflows/workflow-public.openapi.ts @@ -103,6 +103,10 @@ export const workflowSettingsFieldDocs = { export const workflowCreateFieldDocs = { name: { example: 'Workflow 1' }, + description: { + description: 'Description of the workflow', + example: 'My workflow description', + }, nodes: { description: 'Nodes that make up the workflow' }, nodeGroups: { description: 'Visual groupings of nodes shown as frames on the canvas' }, staticData: { description: 'Data the workflow keeps between executions', example: { lastId: 1 } }, diff --git a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/createWorkflow.generated.yml b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/createWorkflow.generated.yml index 5b3667674ac..7ef2722d37e 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/createWorkflow.generated.yml +++ b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/createWorkflow.generated.yml @@ -553,6 +553,10 @@ requestBody: type: string format: date-time readOnly: true + description: + type: string + description: Description of the workflow + example: My workflow description required: - name - nodes diff --git a/packages/cli/test/integration/public-api/workflows.test.ts b/packages/cli/test/integration/public-api/workflows.test.ts index 16ba4da2021..45d85c7742c 100644 --- a/packages/cli/test/integration/public-api/workflows.test.ts +++ b/packages/cli/test/integration/public-api/workflows.test.ts @@ -2202,6 +2202,19 @@ describe('POST /workflows', () => { expect(sharedWorkflow?.role).toEqual('workflow:owner'); }); + test('should create a workflow with a description', async () => { + const description = 'What this workflow does'; + const response = await authMemberAgent.post('/workflows').send({ + ...mockPostWorkflowPayload(), + description, + }); + + expect(response.statusCode).toBe(200); + expect(response.body.description).toBe(description); + const stored = await workflowRepository.findOneBy({ id: response.body.id }); + expect(stored?.description).toBe(description); + }); + test.each([ { key: 'binaryMode', value: 'combined' }, { key: 'credentialResolverId', value: 'some-resolver-id' },