n8n published [email protected] on 21 September 2026 at 07:41 UTC. The patch is two bug fixes. The public API change is the one operators will notice first: PATCH /credentials/{credentialId} no longer returns HTTP 405.
The full release notes and downloads are on the GitHub release page. The range from [email protected] is the compare view.
Credential writes leave the legacy handlers ¶
On 2.39.5, PATCH /api/v1/credentials/{credentialId} answered HTTP 405. The same call worked on 2.38.7. The fix is #38982, commit 48dccc2.
The 405 came from a path parameter collision. PATCH and PUT on a credential id never bound. The commit moves credential writes onto CredentialsPublicController under @PublicApiController. Type and body checks move to assertKnownCredentialType and to validateCredentialData in credentials.utils.ts.
The release note names only the PATCH. The same commit moves the other write routes so the collision cannot remain beside it:
POST /api/v1/credentialsPATCH /api/v1/credentials/{credentialId}DELETE /api/v1/credentials/{credentialId}PUT /api/v1/credentials/{credentialId}/transfer
Request shapes are Zod classes in packages/@n8n/api-types/src/dto/credentials/credential-public.dto.ts. CreateCredentialPublicDto takes name, type, and data. The schema marks data write only. Optional projectId picks the project. Omit it and the credential is created in the personal project of the caller. The response omits data. id, createdAt, and updatedAt are read only.
UpdateCredentialPublicDto makes name, type, data, isGlobal, isResolvable, and isPartialData optional. Changing type requires data in the same body. isPartialData defaults to false. True merges the sent keys into the stored secret. False replaces the stored data object, so a partial map drops every omitted key. TransferCredentialPublicDto requires destinationProjectId and drops unknown keys. DeleteCredentialPublicDto adds usageScope, either project or instance.
credentials.handler.ts and credentials.mapper.ts are gone. @ApiResponse parses and strips the body. The header is still X-N8N-API-KEY. A successful create or update returns HTTP 200 and omits data.
An unauthorized caller with an invalid body now gets HTTP 403, not HTTP 400. Scope checks run before body validation. The old handler validated the body first. An empty PATCH body is still HTTP 415 because the route sets @Body({ required: true }).
Unloadable nodes take the published triggers down ¶
#39057, commit badfc76, changes packages/cli/src/workflows/publication/workflow-publication-applier.ts. Trigger registration sits in packages/cli/src/workflows/triggers/workflow-trigger-activator.ts.
Publication can hit a node type this process cannot load. NODES_EXCLUDE is the common cause. The workflow cannot run. Constructing Workflow throws UnrecognizedNodeTypeError from n8n-core. Publish time checks already reject the graph, and every execution entry point fails the same way. Reconciliation did not stop. Before this patch, workflow_publication_outbox gained a new failed row every 10 seconds.
WorkflowPublicationApplier.apply now fails the publication, tears down triggers from the version already published, advances the published version, and marks every trigger failed. That matches the older activation path and ends the retry loop.
Teardown cannot pass the activator a version that still contains the unknown node. The previous version usually holds that node, and building it would throw. The applier strips those nodes, then calls workflowTriggerActivator.deactivate with the filtered list and the trigger ids to drop. It also calls deregisterUnresolvableNodes. activate is not called. setPublishedVersion still runs, so the pointer moves even though activation failed. That filter is also what makes unpublish succeed.
The regression test throws UnrecognizedNodeTypeError for n8n-nodes-base.executeCommand. A known trigger is recorded as failed with triggerKind in-memory. The unknown node is failed with triggerKind persisted and the message Unrecognized node type: n8n-nodes-base.executeCommand. If that node is the only trigger, the result is still one persisted failed row, and activate is not called.
The pull request gives a reproduction:
- Start the instance with
NODES_EXCLUDE=[]. - Create a workflow with a Schedule Trigger and an Execute Command node. Publish it.
- Set
NODES_EXCLUDE=["n8n-nodes-base.executeCommand"]and restart. - Read
workflow_publication_outbox. Expect onefailedrecord and then silence. Before the fix, a newfailedrecord appeared every 10 seconds. - Read
workflow_publication_trigger_status. Expect afailedrow for the Schedule Trigger and one for Execute Command. - Clear the exclusion, restart, and publish again. The workflow should activate.
An abort before teardown does not call deactivate or setPublishedVersion. The test aborts with deadline and expects that error to surface.
One gap stays open. A workflow whose only triggers are webhooks, plus an unknown node, comes back live after a restart and errors on every request. Startup does not apply persisted triggers again. A reconciler tick guard and query indexes are named as later work. They are not in [email protected].
Upgrade notes ¶
Treat HTTP 403 as the auth failure on credential create and update, including when the JSON is also wrong. HTTP 400 now means the caller passed the scope check and the body failed validation. HTTP 415 still means the PATCH body was empty.
Send a complete data object on update unless isPartialData is true. The default replaces stored credential data.
A published workflow that contains a node type this process cannot load will drop its previous triggers and sit in failed. Those triggers do not stay up while reconciliation retries. The release notes list no database migration.
Where to get it ¶
- Release page: [email protected] on GitHub
- Repository:
n8n-io/n8n - Tag:
[email protected]