Como criar um community node do n8n do zero: do scaffold ao Creator Portal
Um community node do n8n, integração publicada por terceiros, é um pacote npm com três marcas de identificação. O nome começa com n8n-nodes-, a keyword é n8n-community-node-package, e um atributo n8n no package.json aponta para as classes compiladas de node e de credencial. Junto vão duas classes TypeScript que o n8n carrega em runtime. A doc oficial recomenda hoje este caminho (conferida em 25/08/2026): scaffold com o CLI n8n-node e teste local com n8n-node dev. Se a meta é a verificação, o passo seguinte é publicar no npm via GitHub Actions com provenance, o atestado assinado de quem construiu o pacote. A exigência vale para nodes verificados desde 01/05/2026.
Este guia percorre esse caminho inteiro usando um node real e público como mapa. É o n8n-nodes-tamperlens, que expõe a API do Tamperlens como três operações e está no npm desde 11/08/2026. As regras de verificação que mudam a arquitetura (zero dependências, multipart à mão, o scanner, a tarde que o OIDC custou) já têm post próprio. Aqui o assunto é o como: o que cada arquivo faz, o que cada bloco de código precisa conter, e em que ordem as coisas acontecem até a submissão.
O que tem dentro de um community node?
Tirando testes e CI, o pacote inteiro são sete arquivos de fonte. A árvore do node real:
n8n-nodes-tamperlens/
├── package.json ← o contrato com o n8n
├── credentials/
│ └── TamperlensApi.credentials.ts
└── nodes/Tamperlens/
├── Tamperlens.node.ts ← a classe do node
├── GenericFunctions.ts ← helpers puros, testáveis sem n8n
├── Tamperlens.node.json ← o "codex": categorias e links de doc
├── tamperlens.svg ← ícone (tema claro)
└── tamperlens.dark.svg ← ícone (tema escuro)
Quatro peças, quatro papéis:
1. O package.json é o contrato. É ele que faz de um pacote npm um community node, pelo atributo n8n:
{
"name": "n8n-nodes-tamperlens",
"keywords": ["n8n-community-node-package", "..."],
"files": ["dist"],
"n8n": {
"n8nNodesApiVersion": 1,
"credentials": ["dist/credentials/TamperlensApi.credentials.js"],
"nodes": ["dist/nodes/Tamperlens/Tamperlens.node.js"]
},
"peerDependencies": { "n8n-workflow": "*" }
}
Repare que os caminhos apontam para dist/, o JavaScript compilado, não o TypeScript. Isso cria a primeira pegadinha prática do projeto: o tsc só emite .js, e os ícones SVG e o .node.json precisam estar em dist/ também. O script de build do node real resolve isso copiando os três arquivos estáticos depois do tsc. Se o seu esquecer, o pacote instala e o node aparece sem ícone, ou não aparece.
2. A classe do node (*.node.ts) implementa INodeType: um objeto description que declara a interface inteira (nome, ícone, operações, campos) e um método execute() que processa os itens. As duas seções seguintes abrem cada metade.
3. A classe da credencial (*.credentials.ts) implementa ICredentialType. Ela declara os campos que o usuário preenche, a regra de autenticação que o n8n aplica sozinho e um request de teste para o botão “Test”.
4. O codex (*.node.json) é metadado de catálogo: categorias e links para a documentação. É o que preenche o painel lateral quando o usuário encontra o node.
Declarativo ou programático: qual estilo usar?
O n8n tem dois estilos de node. A doc de escolha de estilo (conferida em 25/08/2026) recomenda o declarativo para a maioria dos casos: JSON descrevendo o roteamento das requisições, menos código, menos bug. Mas ela lista onde o programático é obrigatório: trigger nodes, APIs que não são REST, versionamento completo e “qualquer node que precise transformar os dados de entrada”. Esse último critério é o que decide para muita API de arquivo.
O node do Tamperlens é programático por essa razão. Ele não repassa JSON: monta o corpo da requisição a partir do binário do item, incluindo um multipart/form-data construído à mão para a operação de comparação. A história do porquê está no post da verificação. Se a sua API recebe JSON e devolve JSON, comece declarativo. Se ela recebe arquivos, você vai acabar no execute().
Como começar: o scaffold do CLI n8n-node
O jeito atual de começar do zero, pela doc do CLI (conferida em 25/08/2026):
npm create @n8n/node@latest
# ou, instalado global:
npm install --global @n8n/node-cli
n8n-node new
O scaffold, o gerador de estrutura inicial, pergunta o nome do projeto, o tipo de node (HTTP API, que é o declarativo, ou programático) e um template. Depois gera a estrutura completa. Dali em diante, dois comandos carregam o loop de desenvolvimento:
n8n-node devcompila o projeto e sobe um n8n local emlocalhost:5678com o seu node já carregado. Você adiciona o node num workflow de verdade e testa contra a API real. É o substituto moderno do ritual denpm linkpara dentro do~/.n8n/customque os tutoriais antigos ensinam.n8n-node lint(com--fixpara o que for automático) roda as regras de qualidade que a verificação vai cobrar depois.
Transparência do caso real: o n8n-nodes-tamperlens nasceu antes de o CLI virar o caminho recomendado. O pacote foi montado à mão sobre a estrutura do n8n-nodes-starter. Hoje a doc de building pede que submissões novas comecem do scaffold do CLI. Não há razão para não obedecer: a estrutura gerada já sai no formato que a revisão espera, workflow de publicação incluído.
Como o node declara a interface?
A metade description da classe é declarativa mesmo num node programático. Quatro detalhes dela valem apontar, porque nenhum é óbvio no primeiro node:
export class Tamperlens implements INodeType {
description: INodeTypeDescription = {
displayName: 'Tamperlens',
name: 'tamperlens',
icon: { light: 'file:tamperlens.svg', dark: 'file:tamperlens.dark.svg' },
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"]}}',
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [{ name: 'tamperlensApi', required: true }],
properties: [ /* Operation + campos por operação */ ],
};
- O ícone é um par claro/escuro.
iconaceita{ light, dark }, e a verificação cobra as duas variantes. No caso real, a dark só troca a cor do traço do mesmo SVG. subtitleé uma expressão.={{$parameter["operation"]}}faz o node mostrar no canvas qual operação aquela instância executa, de graça, sem código.usableAsTool: trueé uma linha com consequência grande: marca o node como utilizável como ferramenta pelo AI Agent do n8n. Para uma API empacotada, é a diferença entre “existe num workflow” e “um agente pode decidir chamá-la”.- Campos aparecem por operação via
displayOptions. Cada property pode declarardisplayOptions: { show: { operation: ['inspect'] } }. O campo só é renderizado quando a operação selecionada bate. É assim que três operações dividem um formulário sem virarem três nodes.
E uma decisão de vocabulário que a revisão olha: cada opção de Operation carrega action (“Inspect a document for fraud signals”) além de name e description. O action é o texto que aparece na busca de nodes, escrito como verbo.
Como o execute() processa itens sem derrubar o workflow?
O esqueleto do execute() é um loop por item com um contrato de erro específico. Esse contrato é a parte que os tutoriais resumem e a revisão cobra por extenso:
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
try {
// ... monta a requisição do item i e chama a API ...
returnData.push({ json, pairedItem: { item: i } });
} catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: { error: error instanceof Error ? error.message : String(error) },
pairedItem: { item: i },
});
continue;
}
throw new NodeOperationError(this.getNode(), error as Error, { itemIndex: i });
}
}
return [returnData];
}
Quatro regras embutidas aí:
try/catchpor item, não por execução. Um lote de vinte documentos em que o décimo é corrompido não pode perder os outros dezenove.continueOnFail()decide o destino do erro. Ligado (é o usuário quem liga, na aba de settings do node), o erro vira um item de saída{ json: { error } }e o workflow segue; desligado, a execução para com erro.- Erro sempre embrulhado em
NodeOperationError, comitemIndex. Nunca umthrow errorcru: o scanner da verificação, o validador automático de pacotes, reprova o throw cru sintaticamente, até quando ele parece defensável. OitemIndexé o que permite à UI apontar qual item falhou. pairedItemnos dois caminhos. Sucesso e erro carregampairedItem: { item: i }. É o que deixa o n8n rastrear de qual item de entrada cada saída veio. Também é o que faz expressões como$('NóAnterior').itemfuncionarem rio abaixo.
Como a credencial injeta a chave sem o node vê-la?
A credencial é uma classe separada, e o desenho importa: o código do node nunca toca na API key. A classe declara os campos e uma regra genérica de autenticação:
export class TamperlensApi implements ICredentialType {
name = 'tamperlensApi';
properties = [
{ displayName: 'API Key', name: 'apiKey', type: 'string',
typeOptions: { password: true }, required: true, default: '' },
{ displayName: 'Base URL', name: 'baseUrl', type: 'string',
default: 'https://tamperlens.com/api/v1' },
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: { headers: { Authorization: '=Bearer {{$credentials.apiKey}}' } },
};
test: ICredentialTestRequest = {
request: { baseURL: '={{$credentials.baseUrl}}', url: '/receipt/verify',
method: 'POST', body: { report: {}, receipt: {} } },
};
}
Três decisões nesse bloco:
typeOptions: { password: true }faz o campo renderizar mascarado. A revisão espera isso em qualquer segredo.authenticateé declarativo: a expressão=Bearer {{$credentials.apiKey}}diz ao n8n como montar o header. No node, a chamada éthis.helpers.httpRequestWithAuthentication.call(this, 'tamperlensApi', options). O runtime injeta o header na hora da requisição, e a chave nunca passa pelo código que você escreveu. É também o que faz a credencial funcionar igual em qualquer operação futura.- O bloco
testalimenta o botão “Test” da credencial. Qual endpoint apontar ali é uma decisão de produto com consequência de cobrança. O argumento completo (aponte para algo autenticado e não medido) está no post da verificação.
Como entra o arquivo? Binário do item, nunca caminho
As diretrizes de verificação (conferidas em 25/08/2026) são diretas: o código “não deve interagir com variáveis de ambiente nem tentar ler/escrever arquivos”. Tudo que o node precisa chega por parâmetro. Para um node que processa documentos, a consequência prática é o par:
const binary = this.helpers.assertBinaryData(i, binaryPropertyName);
const buffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
assertBinaryData valida que o campo binário existe no item (com erro legível se não existir) e entrega os metadados: fileName, mimeType. getBinaryDataBuffer entrega os bytes. O mimeType do item vira o Content-Type da requisição, com fallback para application/octet-stream. O campo que o usuário configura no node não é um caminho de arquivo. É o nome do campo binário do item (data por padrão), que um node anterior já carregou: IMAP, webhook, HTTP Request.
O efeito colateral é o argumento de venda. Um node que só lê binário do item roda idêntico no n8n autogerenciado e no n8n Cloud. Lá, “caminho no disco” nem é um conceito que o usuário alcança.
Como publicar no npm do jeito que a verificação exige?
Desde 01/05/2026, a doc de building (conferida em 25/08/2026) exige que node verificado seja publicado via GitHub Actions com provenance statement. Publish da máquina local não atende. O scaffold novo do CLI já vem com o workflow de publicação pronto. Para pacote existente, a doc manda adotar o publish.yml do n8n-nodes-starter.
Provenance, no npm, é o atestado assinado de que o pacote foi construído daquele repositório público, por aquele workflow. É a resposta do ecossistema a pacote de automação virando vetor de ataque.
O workflow real do node, na variante token-less (npm Trusted Publishing via OIDC, sem token guardado no repositório):
on:
push:
tags: ["v*"]
permissions:
contents: read
id-token: write # OIDC para o npm Trusted Publishing + provenance
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4 # sem registry-url — deliberado
with:
node-version: 22
- run: npm install -g npm@11 # pinado: nem 10, nem 12.0.x
- run: npm ci
- run: npm test
- run: npm publish --access public
O fluxo de release vira: versão no package.json, tag v0.1.2, push da tag. O CI builda, testa e publica. Dois cintos de segurança ficam fora do YAML. O primeiro é prepublishOnly: npm test no package.json, para nenhum publish sair sem a suíte verde, nem por engano. O segundo é o script de teste buildando antes de testar, para a suíte rodar sobre o dist/ que vai para o npm.
As três linhas comentadas no YAML acima (o registry-url ausente, o npm pinado em 11) são cicatrizes de uma tarde de debugging que tem seção própria no post da verificação. Copie o estado final e leia a história se algo der ENEEDAUTH.
Como funciona a verificação da n8n — e quanto tempo leva?
Com o pacote no npm, a submissão é feita no Creator Portal. O que as diretrizes e a página de submissão cobram (ambas conferidas em 25/08/2026):
- Licença MIT e repositório público, com a URL do repositório no npm batendo com o GitHub.
- Interface e documentação em inglês.
- Um serviço por node, e não pode ser um serviço que o n8n já integra.
- Zero dependências externas e nada de filesystem ou variáveis de ambiente: as duas regras que moldaram as seções anteriores.
- Passar no scanner:
npx @n8n/scan-community-package n8n-nodes-SEUPACOTE. Rode antes do primeiro publish: cada achado dele depois de publicado é uma versão nova no npm. - Publicação por GitHub Actions com provenance, da seção anterior.
- E uma ressalva que vale ler antes de investir o esforço: a n8n se reserva o direito de recusar nodes que concorram com funcionalidades pagas da plataforma.
O que a verificação compra, nas palavras da doc: usuários “descobrem e instalam nodes verificados direto do painel de nodes do n8n”. Sem ela, o usuário precisa achar o pacote no npm e instalar pelo nome nas configurações.
Os prazos do caso real, para calibrar expectativa: o node foi ao npm em 11/08/2026, em duas versões no mesmo dia, a segunda corrigindo os achados do scanner. A submissão ao Creator Portal foi em 14/08/2026, e a confirmação indicou janela de revisão de até quatro semanas. A doc pública não fixa prazo nenhum (conferida em 25/08/2026). Este post foi escrito com a revisão ainda aberta: sem resultado para relatar, e nenhum a prometer. A regra da casa enquanto isso: repositório congelado até a revisão terminar.
O checklist do zero ao submetido
- Scaffold:
npm create @n8n/node@latest; escolha declarativo se a API é JSON puro, programático se há arquivo ou transformação. - Contrato: nome
n8n-nodes-*, keywordn8n-community-node-package, atributon8napontando paradist/, build copiando SVGs e codex paradist/. - Interface: ícone claro/escuro,
subtitlepor expressão,actioncomo verbo em cada operação, campos por operação viadisplayOptions,usableAsToolse fizer sentido como ferramenta de agente. execute(): loop por item,try/catchpor item,continueOnFail()respeitado,NodeOperationErrorcomitemIndex,pairedItemnos dois caminhos.- Credencial: segredo com
password: true,authenticatedeclarativo,testapontando para endpoint autenticado e barato. O node nunca toca na chave. - Arquivo = binário do item:
assertBinaryData+getBinaryDataBuffer; nenhumfs, nenhumprocess.env. - Teste local:
n8n-node dev, workflow de verdade, API de verdade;n8n-node linte o scanner antes do primeiro publish. - Publicação e submissão: tag → GitHub Actions → npm com provenance; depois creators.n8n.io/nodes. O repo congela até a resposta.
Empacotar uma API para dentro de um ecossistema de automação é um padrão que se repete. A mesma decisão já apareceu aqui na versão MCP. E o n8n que roda o atendimento da casa é o mesmo para o qual este node existe. O custo de fazer do jeito verificável é quase todo pago no primeiro dia. A partir daí, um node de zero dependências é o tipo de software que não acorda ninguém.
Precisa de um projeto técnico sob medida?
Arquitetura, TypeScript, APIs e automação, do protótipo à produção. Quem responde o seu e-mail é quem escreve o código, e o prazo que eu prometo é o prazo que eu consigo cumprir.
Me manda uma mensagem →