mcpbeat

N8n:public API

n8n-io/n8n:public-api

>- Adds, migrates, or updates n8n Public API v1 endpoints with @PublicApiController — public DTOs, API-key and RBAC scopes, cursor pagination, OpenAPI + coverage wiring, and tests. Use when working under packages/cli/src/public-api/v1/ or when exposing an existing service through /api/v1.

4k tokens
context cost
the whole folder, loaded on every use
2
files
instructions only
0
copies elsewhere
how many repositories repackaged it
199283
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/n8n-io/n8n --skill n8n:public-api

What comes with it

7 225 bytes besides the instruction
reference.md

The instruction itself

12 sections, as written by the author

Public API v1

Public API v1 lives in packages/cli/src/public-api/v1/, mounted at /api/v1

with API-key auth and public error formatting via PublicApiControllerRegistry

(packages/cli/src/public-api/public-api-controller.registry.ts).

Two rule tiers: invariants (never break) and team defaults (follow unless

an existing public contract forces otherwise). When this skill and the code

disagree on a detail, the code wins — so open the files below. That is a reason to

check the code, not license to drop a team default.

Non-negotiable rules

  • New endpoints are @PublicApiController classes under v1/controllers/, one

*.public.controller.ts per feature. A controller is a class — never

export = (the legacy tuple style; require-public-api-controller flags it).

  • Public API and internal REST are separate HTTP surfaces. A public controller

never calls an internal controller/endpoint; both reuse the same service.

  • Controllers and handlers delegate to a service — never import a repository or

Container.get(…Repository) (no-repository-in-public-api-handler).

  • Input/output go through DTOs from @n8n/api-types; every JSON route declares

@ApiResponse(Dto).

  • Register each controller via a side-effect import in v1/controllers/index.ts

(public-api-controllers.test.ts fails otherwise).

  • Don't add business logic to legacy express-openapi-validator (EOV) handlers.
  • Migrating a legacy endpoint must not change its public contract.

These are n8n-local-rules ESLint rules (see packages/cli/eslint.config.mjs)

and can't be silenced inline (no-public-api-guardrail-disable). The off

allowlist there covers pre-existing legacy files only — it's shrink-only, don't

add to it.

Team defaults

  • List endpoints: cursor-based pagination (internal API uses both cursor- and

page-based — don't copy an internal endpoint's model).

  • Updates: full-object PUT, not PATCH. A successful GET body should be

acceptable as a PUT body for the same resource (round-trip), aside from

server-managed/immutable fields.

  • Strict input DTOs; output DTOs are an allowlist of public fields.
  • Never return real secrets/tokens in responses or error details — mask with the

resource's sentinel/placeholder (or omit). Echoing that sentinel on PUT

means keep; any other value replaces. Detail:

Updates and write-only secrets.

  • "Test connection/config" endpoints validate the request body (test-before-save).

Architecture

Public and internal are sibling routes over one shared, HTTP-agnostic service;

neither calls the other.

GET /rest/tags    → TagsController         ┐  JWT auth, internal shape
                                            ├─→ TagService
GET /api/v1/tags  → TagsPublicController   ┘  API-key auth, public DTO

Reuse the service behavior. Reuse a DTO only when public and internal contracts

are intentionally identical; otherwise make a public-specific DTO that doesn't

depend on a UI-oriented internal shape.

Before editing

Open these — they are the source of truth, not this skill:

  • v1/controllers/ — copy structure from tags.public.controller.ts (list +

cursor) or workflows.public.controller.ts (@Param + @ProjectScope), and

index.ts for the barrel.

  • Decorators in packages/@n8n/decorators/src/controller/:

public-api-controller.ts, api-key-scope.ts, api-response.ts,

api-error-response.ts, api-summary.ts, api-description.ts, api-tags.ts,

route.ts, scoped.ts, args.ts, licensed.ts.

  • The OpenAPI generator (reads the decorators above, no hand-written YAML

needed for a controller route): v1/openapi-gen/generate.ts,

v1/openapi-gen/decorator-routes.ts.

  • Pagination helpers: v1/shared/services/pagination.service.ts

(decodeCursor, encodeNextCursor).

  • DTOs: packages/@n8n/api-types/src/dto/.
  • Gating tests: v1/__tests__/public-api-controllers.test.ts,

v1/__tests__/scope-parity.test.ts,

v1/openapi-gen/__tests__/generated-spec-drift.test.ts.

  • The internal controller for this resource and its neighboring functional tests.

Declaring a controller

A controller is a class marked @PublicApiController('/base') that injects the

shared service via its constructor and delegates to it. Copy the shape from an

existing controller in v1/controllers/ with the same operation type and auth

model; reuse only what applies. Decorators, all from @n8n/decorators:

| Decorator | Use |

|---|---|

| @PublicApiController('/base') | Class marker; mounts routes at /api/v1/base. |

| @Get/@Post/@Put/@Patch/@Delete('/path') | Route method. |

| @ApiKeyScope('res:action') | API-key grant check. |

| @ProjectScope/@GlobalScope('res:action') | User RBAC check. |

| @ApiResponse(status) / @ApiResponse(status, Dto) | Success status + (optional) output DTO; registry .parse()s + strips the return value. Exactly one per route — a second @ApiResponse throws. 204 can't carry a DTO — throws. |

| @ApiErrorResponse(status) | Declares an additional documented non-2xx status (e.g. 404, 409). Stack multiple for more than one. 400/401/403 are added automatically (body/query present, always, and @ApiKeyScope present, respectively) — don't declare those yourself. |

| @ApiSummary(text) / @ApiDescription(text) / @ApiTags([...]) | OpenAPI summary/description/tags. @ApiTags sorts alphabetically regardless of the order you pass. All optional but expected on every real route. |

| @Query / @Body / @Param('name') | Bind + validate via a Z.class DTO / path param. |

| @Licensed('feat') | Not yet enforced for @PublicApiController routesPublicApiControllerRegistry doesn't read licenseFeature (only the internal @RestController registry does). If the endpoint gates an EE feature, check the license manually in the handler (Container.get(License).isLicensed(LICENSE_FEATURES.X), throwing ForbiddenError on failure) instead of relying on this decorator alone. |

Authorization (easy to get wrong)

  • @ApiKeyScope (what the API key is granted) and @ProjectScope/@GlobalScope

(what the user may do) are independent. Use both when the model needs both.

  • @ProjectScope reads req.params as-is and does not remap id — name the path

param what the resolver expects (workflowId, credentialId, projectId,

dataTableId, …). A generic id often fails.

  • @ApiKeyScope takes a string, { anyOf: [...] }, or { allOf: [...] } — never

a bare array. The scope must exist in the permissions registry

(API_KEY_RESOURCES in @n8n/permissions); scope-parity.test.ts fails on an

orphan scope.

DTOs

  • Build the public response shape explicitly; don't return an ORM entity and lean

on @ApiResponse stripping to hide fields.

  • Treat the output DTO as an allowlist. Re-check nested relations, ownership

fields, tokens, and encrypted values.

  • Make input DTOs strict so unknown/partial fields aren't silently accepted.
  • Secrets: never return a real secret; use the resource's sentinel/placeholder

(or omit). See Updates and write-only secrets.

List endpoints (cursor pagination)

Copy the cursor flow from tags.public.controller.ts. Use publicApiPaginationSchema

plus decodeCursor / encodeNextCursor from the shared pagination service; the

cursor is opaque; return { data, nextCursor } (never a bare array) with

nextCursor: null on the last page; an invalid cursor is a 400. Preserve an

existing endpoint's pagination as-is. Detail:

List endpoints and cursor pagination.

Wiring checklist

  • v1/controllers/<feature>.public.controller.ts + side-effect import in

v1/controllers/index.ts.

  • Public DTO in @n8n/api-types + export from the barrel (src/dto/).
  • @ApiKeyScope value exists in the permissions registry.
  • Don't hand-write the OpenAPI path or x-required-scope for a controller

route — the generator (v1/openapi-gen/generate.ts) builds it from your

decorators (@ApiSummary/@ApiDescription/@ApiTags/@ApiKeyScope/

@ApiResponse/@ApiErrorResponse). Run the full pnpm build and commit

the regenerated handlers/<feature>/spec/paths/*.generated.yml fragment(s)

and openapi.decorator-routes.generated.yml

generated-spec-drift.test.ts fails CI if they're stale. `pnpm run

build:data` alone is not enough after touching a controller: it runs

the generator against the already-compiled dist/, so a new/changed

controller silently doesn't show up unless tsc ran first.

  • Add the route to packages/nodes-base/nodes/N8n/n8n-api-coverage.json.
  • Tests.

Testing

Always cover: happy path, input-validation failure, missing API-key scope, RBAC

denial. Prefer covering the business path in

packages/cli/test/integration/public-api/ (real HTTP + DB); mocked-service unit

tests don't replace that. Add the cases that apply (cursor pages,

not-found/conflict, no sensitive fields, credential keep/replace, migration

contract) — see Testing matrix. Match the nearest

existing tests.

More detail (reference.md)

  • List endpoints and cursor pagination
  • Updates and write-only secrets
  • Test-before-save endpoints
  • Errors
  • Testing matrix
  • Migrating legacy EOV endpoints

How to use it

Copy the folder

Take n8n-io/n8n:public-api from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.