├── CHANGELOG.md ├── .gitignore ├── eslint.config.mjs ├── .vscode ├── extensions.json └── launch.json ├── nodes ├── GithubIssues │ ├── resources │ │ ├── issue │ │ │ ├── get.ts │ │ │ ├── create.ts │ │ │ ├── index.ts │ │ │ └── getAll.ts │ │ └── issueComment │ │ │ ├── index.ts │ │ │ └── getAll.ts │ ├── shared │ │ ├── utils.ts │ │ ├── transport.ts │ │ └── descriptions.ts │ ├── GithubIssues.node.json │ ├── listSearch │ │ ├── getUsers.ts │ │ ├── getRepositories.ts │ │ └── getIssues.ts │ └── GithubIssues.node.ts └── Example │ ├── Example.node.json │ ├── example.svg │ ├── example.dark.svg │ └── Example.node.ts ├── .github └── workflows │ └── ci.yml ├── tsconfig.json ├── LICENSE.md ├── .prettierrc.js ├── credentials ├── GithubIssuesApi.credentials.ts └── GithubIssuesOAuth2Api.credentials.ts ├── package.json ├── icons ├── github.svg └── github.dark.svg ├── README_TEMPLATE.md ├── CODE_OF_CONDUCT.md └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { config } from '@n8n/node-cli/eslint'; 2 | 3 | export default config; 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "EditorConfig.EditorConfig", 5 | "esbenp.prettier-vscode", 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to running n8n", 6 | "processId": "${command:PickProcess}", 7 | "request": "attach", 8 | "skipFiles": ["/**"], 9 | "type": "node" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /nodes/GithubIssues/resources/issue/get.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | import { issueSelect } from '../../shared/descriptions'; 3 | 4 | const showOnlyForIssueGet = { 5 | operation: ['get'], 6 | resource: ['issue'], 7 | }; 8 | 9 | export const issueGetDescription: INodeProperties[] = [ 10 | { 11 | ...issueSelect, 12 | displayOptions: { show: showOnlyForIssueGet }, 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /nodes/GithubIssues/shared/utils.ts: -------------------------------------------------------------------------------- 1 | export function parseLinkHeader(header?: string): { [rel: string]: string } { 2 | const links: { [rel: string]: string } = {}; 3 | 4 | for (const part of header?.split(',') ?? []) { 5 | const section = part.trim(); 6 | const match = section.match(/^<([^>]+)>\s*;\s*rel="?([^"]+)"?/); 7 | if (match) { 8 | const [, url, rel] = match; 9 | links[rel] = url; 10 | } 11 | } 12 | 13 | return links; 14 | } 15 | -------------------------------------------------------------------------------- /nodes/Example/Example.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "n8n-nodes-example", 3 | "nodeVersion": "1.0", 4 | "codexVersion": "1.0", 5 | "categories": ["Development", "Developer Tools"], 6 | "resources": { 7 | "credentialDocumentation": [ 8 | { 9 | "url": "https://github.com/org/repo?tab=readme-ov-file#credentials" 10 | } 11 | ], 12 | "primaryDocumentation": [ 13 | { 14 | "url": "https://github.com/org/repo?tab=readme-ov-file" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nodes/GithubIssues/GithubIssues.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "n8n-nodes-github-issues", 3 | "nodeVersion": "1.0", 4 | "codexVersion": "1.0", 5 | "categories": ["Development", "Developer Tools"], 6 | "resources": { 7 | "credentialDocumentation": [ 8 | { 9 | "url": "https://github.com/org/repo?tab=readme-ov-file#credentials" 10 | } 11 | ], 12 | "primaryDocumentation": [ 13 | { 14 | "url": "https://github.com/org/repo?tab=readme-ov-file" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '22' 20 | 21 | - name: Install dependencies 22 | run: 'npm ci' 23 | 24 | - name: Run lint 25 | run: 'npm run lint' 26 | 27 | - name: Run build 28 | run: 'npm run build' 29 | -------------------------------------------------------------------------------- /nodes/Example/example.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /nodes/Example/example.dark.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "es2019", 7 | "lib": ["es2019", "es2020", "es2022.error"], 8 | "removeComments": true, 9 | "useUnknownInCatchVariables": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "preserveConstEnums": true, 16 | "esModuleInterop": true, 17 | "resolveJsonModule": true, 18 | "incremental": true, 19 | "declaration": true, 20 | "sourceMap": true, 21 | "skipLibCheck": true, 22 | "outDir": "./dist/" 23 | }, 24 | "include": ["credentials/**/*", "nodes/**/*", "nodes/**/*.json", "package.json"] 25 | } 26 | -------------------------------------------------------------------------------- /nodes/GithubIssues/shared/transport.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IHookFunctions, 3 | IExecuteFunctions, 4 | IExecuteSingleFunctions, 5 | ILoadOptionsFunctions, 6 | IHttpRequestMethods, 7 | IDataObject, 8 | IHttpRequestOptions, 9 | } from 'n8n-workflow'; 10 | 11 | export async function githubApiRequest( 12 | this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, 13 | method: IHttpRequestMethods, 14 | resource: string, 15 | qs: IDataObject = {}, 16 | body: IDataObject | undefined = undefined, 17 | ) { 18 | const authenticationMethod = this.getNodeParameter('authentication', 0); 19 | 20 | const options: IHttpRequestOptions = { 21 | method: method, 22 | qs, 23 | body, 24 | url: `https://api.github.com${resource}`, 25 | json: true, 26 | }; 27 | 28 | const credentialType = 29 | authenticationMethod === 'accessToken' ? 'githubIssuesApi' : 'githubIssuesOAuth2Api'; 30 | 31 | return this.helpers.httpRequestWithAuthentication.call(this, credentialType, options); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 n8n 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * https://prettier.io/docs/en/options.html#semicolons 4 | */ 5 | semi: true, 6 | 7 | /** 8 | * https://prettier.io/docs/en/options.html#trailing-commas 9 | */ 10 | trailingComma: 'all', 11 | 12 | /** 13 | * https://prettier.io/docs/en/options.html#bracket-spacing 14 | */ 15 | bracketSpacing: true, 16 | 17 | /** 18 | * https://prettier.io/docs/en/options.html#tabs 19 | */ 20 | useTabs: true, 21 | 22 | /** 23 | * https://prettier.io/docs/en/options.html#tab-width 24 | */ 25 | tabWidth: 2, 26 | 27 | /** 28 | * https://prettier.io/docs/en/options.html#arrow-function-parentheses 29 | */ 30 | arrowParens: 'always', 31 | 32 | /** 33 | * https://prettier.io/docs/en/options.html#quotes 34 | */ 35 | singleQuote: true, 36 | 37 | /** 38 | * https://prettier.io/docs/en/options.html#quote-props 39 | */ 40 | quoteProps: 'as-needed', 41 | 42 | /** 43 | * https://prettier.io/docs/en/options.html#end-of-line 44 | */ 45 | endOfLine: 'lf', 46 | 47 | /** 48 | * https://prettier.io/docs/en/options.html#print-width 49 | */ 50 | printWidth: 100, 51 | }; 52 | -------------------------------------------------------------------------------- /credentials/GithubIssuesApi.credentials.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IAuthenticateGeneric, 3 | Icon, 4 | ICredentialTestRequest, 5 | ICredentialType, 6 | INodeProperties, 7 | } from 'n8n-workflow'; 8 | 9 | export class GithubIssuesApi implements ICredentialType { 10 | name = 'githubIssuesApi'; 11 | 12 | displayName = 'GitHub Issues API'; 13 | 14 | icon: Icon = { light: 'file:../icons/github.svg', dark: 'file:../icons/github.dark.svg' }; 15 | 16 | documentationUrl = 17 | 'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#deleting-a-personal-access-token'; 18 | 19 | properties: INodeProperties[] = [ 20 | { 21 | displayName: 'Access Token', 22 | name: 'accessToken', 23 | type: 'string', 24 | typeOptions: { password: true }, 25 | default: '', 26 | }, 27 | ]; 28 | 29 | authenticate: IAuthenticateGeneric = { 30 | type: 'generic', 31 | properties: { 32 | headers: { 33 | Authorization: '=token {{$credentials?.accessToken}}', 34 | }, 35 | }, 36 | }; 37 | 38 | test: ICredentialTestRequest = { 39 | request: { 40 | baseURL: 'https://api.github.com', 41 | url: '/user', 42 | method: 'GET', 43 | }, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /nodes/GithubIssues/resources/issueComment/index.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | import { repoNameSelect, repoOwnerSelect } from '../../shared/descriptions'; 3 | import { issueCommentGetManyDescription } from './getAll'; 4 | 5 | const showOnlyForIssueComments = { 6 | resource: ['issueComment'], 7 | }; 8 | 9 | export const issueCommentDescription: INodeProperties[] = [ 10 | { 11 | displayName: 'Operation', 12 | name: 'operation', 13 | type: 'options', 14 | noDataExpression: true, 15 | displayOptions: { 16 | show: showOnlyForIssueComments, 17 | }, 18 | options: [ 19 | { 20 | name: 'Get Many', 21 | value: 'getAll', 22 | action: 'Get issue comments', 23 | description: 'Get issue comments', 24 | routing: { 25 | request: { 26 | method: 'GET', 27 | url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues/comments', 28 | }, 29 | }, 30 | }, 31 | ], 32 | default: 'getAll', 33 | }, 34 | { 35 | ...repoOwnerSelect, 36 | displayOptions: { 37 | show: showOnlyForIssueComments, 38 | }, 39 | }, 40 | { 41 | ...repoNameSelect, 42 | displayOptions: { 43 | show: showOnlyForIssueComments, 44 | }, 45 | }, 46 | ...issueCommentGetManyDescription, 47 | ]; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "n8n-nodes-<...>", 3 | "version": "0.1.0", 4 | "description": "", 5 | "license": "MIT", 6 | "homepage": "", 7 | "keywords": [ 8 | "n8n-community-node-package" 9 | ], 10 | "author": { 11 | "name": "", 12 | "email": "" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/<...>/n8n-nodes-<...>.git" 17 | }, 18 | "scripts": { 19 | "build": "n8n-node build", 20 | "build:watch": "tsc --watch", 21 | "dev": "n8n-node dev", 22 | "lint": "n8n-node lint", 23 | "lint:fix": "n8n-node lint --fix", 24 | "release": "n8n-node release", 25 | "prepublishOnly": "n8n-node prerelease" 26 | }, 27 | "files": [ 28 | "dist" 29 | ], 30 | "n8n": { 31 | "n8nNodesApiVersion": 1, 32 | "strict": true, 33 | "credentials": [ 34 | "dist/credentials/GithubIssuesApi.credentials.js", 35 | "dist/credentials/GithubIssuesOAuth2Api.credentials.js" 36 | ], 37 | "nodes": [ 38 | "dist/nodes/GithubIssues/GithubIssues.node.js", 39 | "dist/nodes/Example/Example.node.js" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "@n8n/node-cli": "*", 44 | "eslint": "9.32.0", 45 | "prettier": "3.6.2", 46 | "release-it": "^19.0.4", 47 | "typescript": "5.9.2" 48 | }, 49 | "peerDependencies": { 50 | "n8n-workflow": "*" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/github.dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nodes/GithubIssues/listSearch/getUsers.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ILoadOptionsFunctions, 3 | INodeListSearchResult, 4 | INodeListSearchItems, 5 | } from 'n8n-workflow'; 6 | import { githubApiRequest } from '../shared/transport'; 7 | 8 | type UserSearchItem = { 9 | login: string; 10 | html_url: string; 11 | }; 12 | 13 | type UserSearchResponse = { 14 | items: UserSearchItem[]; 15 | total_count: number; 16 | }; 17 | 18 | export async function getUsers( 19 | this: ILoadOptionsFunctions, 20 | filter?: string, 21 | paginationToken?: string, 22 | ): Promise { 23 | const page = paginationToken ? +paginationToken : 1; 24 | const per_page = 100; 25 | 26 | let responseData: UserSearchResponse = { 27 | items: [], 28 | total_count: 0, 29 | }; 30 | 31 | try { 32 | responseData = await githubApiRequest.call(this, 'GET', '/search/users', { 33 | q: filter, 34 | page, 35 | per_page, 36 | }); 37 | } catch { 38 | // will fail if the owner does not have any users 39 | } 40 | 41 | const results: INodeListSearchItems[] = responseData.items.map((item: UserSearchItem) => ({ 42 | name: item.login, 43 | value: item.login, 44 | url: item.html_url, 45 | })); 46 | 47 | const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined; 48 | return { results, paginationToken: nextPaginationToken }; 49 | } 50 | -------------------------------------------------------------------------------- /credentials/GithubIssuesOAuth2Api.credentials.ts: -------------------------------------------------------------------------------- 1 | import type { Icon, ICredentialType, INodeProperties } from 'n8n-workflow'; 2 | 3 | export class GithubIssuesOAuth2Api implements ICredentialType { 4 | name = 'githubIssuesOAuth2Api'; 5 | 6 | extends = ['oAuth2Api']; 7 | 8 | displayName = 'GitHub Issues OAuth2 API'; 9 | 10 | icon: Icon = { light: 'file:../icons/github.svg', dark: 'file:../icons/github.dark.svg' }; 11 | 12 | documentationUrl = 'https://docs.github.com/en/apps/oauth-apps'; 13 | 14 | properties: INodeProperties[] = [ 15 | { 16 | displayName: 'Grant Type', 17 | name: 'grantType', 18 | type: 'hidden', 19 | default: 'authorizationCode', 20 | }, 21 | { 22 | displayName: 'Authorization URL', 23 | name: 'authUrl', 24 | type: 'hidden', 25 | default: 'https://github.com/login/oauth/authorize', 26 | required: true, 27 | }, 28 | { 29 | displayName: 'Access Token URL', 30 | name: 'accessTokenUrl', 31 | type: 'hidden', 32 | default: 'https://github.com/login/oauth/access_token', 33 | required: true, 34 | }, 35 | { 36 | displayName: 'Scope', 37 | name: 'scope', 38 | type: 'hidden', 39 | default: 'repo', 40 | }, 41 | { 42 | displayName: 'Auth URI Query Parameters', 43 | name: 'authQueryParameters', 44 | type: 'hidden', 45 | default: '', 46 | }, 47 | { 48 | displayName: 'Authentication', 49 | name: 'authentication', 50 | type: 'hidden', 51 | default: 'header', 52 | }, 53 | ]; 54 | } 55 | -------------------------------------------------------------------------------- /nodes/GithubIssues/listSearch/getRepositories.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ILoadOptionsFunctions, 3 | INodeListSearchItems, 4 | INodeListSearchResult, 5 | } from 'n8n-workflow'; 6 | import { githubApiRequest } from '../shared/transport'; 7 | 8 | type RepositorySearchItem = { 9 | name: string; 10 | html_url: string; 11 | }; 12 | 13 | type RepositorySearchResponse = { 14 | items: RepositorySearchItem[]; 15 | total_count: number; 16 | }; 17 | 18 | export async function getRepositories( 19 | this: ILoadOptionsFunctions, 20 | filter?: string, 21 | paginationToken?: string, 22 | ): Promise { 23 | const owner = this.getCurrentNodeParameter('owner', { extractValue: true }); 24 | const page = paginationToken ? +paginationToken : 1; 25 | const per_page = 100; 26 | const q = `${filter ?? ''} user:${owner} fork:true`; 27 | let responseData: RepositorySearchResponse = { 28 | items: [], 29 | total_count: 0, 30 | }; 31 | 32 | try { 33 | responseData = await githubApiRequest.call(this, 'GET', '/search/repositories', { 34 | q, 35 | page, 36 | per_page, 37 | }); 38 | } catch { 39 | // will fail if the owner does not have any repositories 40 | } 41 | 42 | const results: INodeListSearchItems[] = responseData.items.map((item: RepositorySearchItem) => ({ 43 | name: item.name, 44 | value: item.name, 45 | url: item.html_url, 46 | })); 47 | 48 | const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined; 49 | return { results, paginationToken: nextPaginationToken }; 50 | } 51 | -------------------------------------------------------------------------------- /nodes/GithubIssues/listSearch/getIssues.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ILoadOptionsFunctions, 3 | INodeListSearchResult, 4 | INodeListSearchItems, 5 | } from 'n8n-workflow'; 6 | import { githubApiRequest } from '../shared/transport'; 7 | 8 | type IssueSearchItem = { 9 | number: number; 10 | title: string; 11 | html_url: string; 12 | }; 13 | 14 | type IssueSearchResponse = { 15 | items: IssueSearchItem[]; 16 | total_count: number; 17 | }; 18 | 19 | export async function getIssues( 20 | this: ILoadOptionsFunctions, 21 | filter?: string, 22 | paginationToken?: string, 23 | ): Promise { 24 | const page = paginationToken ? +paginationToken : 1; 25 | const per_page = 100; 26 | 27 | let responseData: IssueSearchResponse = { 28 | items: [], 29 | total_count: 0, 30 | }; 31 | const owner = this.getNodeParameter('owner', '', { extractValue: true }); 32 | const repository = this.getNodeParameter('repository', '', { extractValue: true }); 33 | const filters = [filter, `repo:${owner}/${repository}`]; 34 | 35 | responseData = await githubApiRequest.call(this, 'GET', '/search/issues', { 36 | q: filters.filter(Boolean).join(' '), 37 | page, 38 | per_page, 39 | }); 40 | 41 | const results: INodeListSearchItems[] = responseData.items.map((item: IssueSearchItem) => ({ 42 | name: item.title, 43 | value: item.number, 44 | url: item.html_url, 45 | })); 46 | 47 | const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined; 48 | return { results, paginationToken: nextPaginationToken }; 49 | } 50 | -------------------------------------------------------------------------------- /nodes/GithubIssues/resources/issue/create.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | 3 | const showOnlyForIssueCreate = { 4 | operation: ['create'], 5 | resource: ['issue'], 6 | }; 7 | 8 | export const issueCreateDescription: INodeProperties[] = [ 9 | { 10 | displayName: 'Title', 11 | name: 'title', 12 | type: 'string', 13 | default: '', 14 | required: true, 15 | displayOptions: { 16 | show: showOnlyForIssueCreate, 17 | }, 18 | description: 'The title of the issue', 19 | routing: { 20 | send: { 21 | type: 'body', 22 | property: 'title', 23 | }, 24 | }, 25 | }, 26 | { 27 | displayName: 'Body', 28 | name: 'body', 29 | type: 'string', 30 | typeOptions: { 31 | rows: 5, 32 | }, 33 | default: '', 34 | displayOptions: { 35 | show: showOnlyForIssueCreate, 36 | }, 37 | description: 'The body of the issue', 38 | routing: { 39 | send: { 40 | type: 'body', 41 | property: 'body', 42 | }, 43 | }, 44 | }, 45 | { 46 | displayName: 'Labels', 47 | name: 'labels', 48 | type: 'collection', 49 | typeOptions: { 50 | multipleValues: true, 51 | multipleValueButtonText: 'Add Label', 52 | }, 53 | displayOptions: { 54 | show: showOnlyForIssueCreate, 55 | }, 56 | default: { label: '' }, 57 | options: [ 58 | { 59 | displayName: 'Label', 60 | name: 'label', 61 | type: 'string', 62 | default: '', 63 | description: 'Label to add to issue', 64 | }, 65 | ], 66 | routing: { 67 | send: { 68 | type: 'body', 69 | property: 'labels', 70 | value: '={{$value.map((data) => data.label)}}', 71 | }, 72 | }, 73 | }, 74 | ]; 75 | -------------------------------------------------------------------------------- /nodes/GithubIssues/resources/issueComment/getAll.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | import { parseLinkHeader } from '../../shared/utils'; 3 | 4 | const showOnlyForIssueCommentGetMany = { 5 | operation: ['getAll'], 6 | resource: ['issueComment'], 7 | }; 8 | 9 | export const issueCommentGetManyDescription: INodeProperties[] = [ 10 | { 11 | displayName: 'Limit', 12 | name: 'limit', 13 | type: 'number', 14 | displayOptions: { 15 | show: { 16 | ...showOnlyForIssueCommentGetMany, 17 | returnAll: [false], 18 | }, 19 | }, 20 | typeOptions: { 21 | minValue: 1, 22 | maxValue: 100, 23 | }, 24 | default: 50, 25 | routing: { 26 | send: { 27 | type: 'query', 28 | property: 'per_page', 29 | }, 30 | output: { 31 | maxResults: '={{$value}}', 32 | }, 33 | }, 34 | description: 'Max number of results to return', 35 | }, 36 | { 37 | displayName: 'Return All', 38 | name: 'returnAll', 39 | type: 'boolean', 40 | displayOptions: { 41 | show: showOnlyForIssueCommentGetMany, 42 | }, 43 | default: false, 44 | description: 'Whether to return all results or only up to a given limit', 45 | routing: { 46 | send: { 47 | paginate: '={{ $value }}', 48 | type: 'query', 49 | property: 'per_page', 50 | value: '100', 51 | }, 52 | operations: { 53 | pagination: { 54 | type: 'generic', 55 | properties: { 56 | continue: `={{ !!(${parseLinkHeader.toString()})($response.headers?.link).next }}`, 57 | request: { 58 | url: `={{ (${parseLinkHeader.toString()})($response.headers?.link)?.next ?? $request.url }}`, 59 | }, 60 | }, 61 | }, 62 | }, 63 | }, 64 | }, 65 | ]; 66 | -------------------------------------------------------------------------------- /nodes/GithubIssues/resources/issue/index.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | import { repoNameSelect, repoOwnerSelect } from '../../shared/descriptions'; 3 | import { issueGetManyDescription } from './getAll'; 4 | import { issueGetDescription } from './get'; 5 | import { issueCreateDescription } from './create'; 6 | 7 | const showOnlyForIssues = { 8 | resource: ['issue'], 9 | }; 10 | 11 | export const issueDescription: INodeProperties[] = [ 12 | { 13 | displayName: 'Operation', 14 | name: 'operation', 15 | type: 'options', 16 | noDataExpression: true, 17 | displayOptions: { 18 | show: showOnlyForIssues, 19 | }, 20 | options: [ 21 | { 22 | name: 'Get Many', 23 | value: 'getAll', 24 | action: 'Get issues in a repository', 25 | description: 'Get many issues in a repository', 26 | routing: { 27 | request: { 28 | method: 'GET', 29 | url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues', 30 | }, 31 | }, 32 | }, 33 | { 34 | name: 'Get', 35 | value: 'get', 36 | action: 'Get an issue', 37 | description: 'Get the data of a single issue', 38 | routing: { 39 | request: { 40 | method: 'GET', 41 | url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues/{{$parameter.issue}}', 42 | }, 43 | }, 44 | }, 45 | { 46 | name: 'Create', 47 | value: 'create', 48 | action: 'Create a new issue', 49 | description: 'Create a new issue', 50 | routing: { 51 | request: { 52 | method: 'POST', 53 | url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues', 54 | }, 55 | }, 56 | }, 57 | ], 58 | default: 'getAll', 59 | }, 60 | { 61 | ...repoOwnerSelect, 62 | displayOptions: { 63 | show: showOnlyForIssues, 64 | }, 65 | }, 66 | { 67 | ...repoNameSelect, 68 | displayOptions: { 69 | show: showOnlyForIssues, 70 | }, 71 | }, 72 | ...issueGetManyDescription, 73 | ...issueGetDescription, 74 | ...issueCreateDescription, 75 | ]; 76 | -------------------------------------------------------------------------------- /README_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # n8n-nodes-_node-name_ 2 | 3 | This is an n8n community node. It lets you use _app/service name_ in your n8n workflows. 4 | 5 | _App/service name_ is _one or two sentences describing the service this node integrates with_. 6 | 7 | [n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/reference/license/) workflow automation platform. 8 | 9 | [Installation](#installation) 10 | [Operations](#operations) 11 | [Credentials](#credentials) 12 | [Compatibility](#compatibility) 13 | [Usage](#usage) 14 | [Resources](#resources) 15 | [Version history](#version-history) 16 | 17 | ## Installation 18 | 19 | Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community nodes documentation. 20 | 21 | ## Operations 22 | 23 | _List the operations supported by your node._ 24 | 25 | ## Credentials 26 | 27 | _If users need to authenticate with the app/service, provide details here. You should include prerequisites (such as signing up with the service), available authentication methods, and how to set them up._ 28 | 29 | ## Compatibility 30 | 31 | _State the minimum n8n version, as well as which versions you test against. You can also include any known version incompatibility issues._ 32 | 33 | ## Usage 34 | 35 | _This is an optional section. Use it to help users with any difficult or confusing aspects of the node._ 36 | 37 | _By the time users are looking for community nodes, they probably already know n8n basics. But if you expect new users, you can link to the [Try it out](https://docs.n8n.io/try-it-out/) documentation to help them get started._ 38 | 39 | ## Resources 40 | 41 | * [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes) 42 | * _Link to app/service documentation._ 43 | 44 | ## Version history 45 | 46 | _This is another optional section. If your node has multiple versions, include a short description of available versions and what changed, as well as any compatibility impact._ 47 | 48 | 49 | -------------------------------------------------------------------------------- /nodes/GithubIssues/GithubIssues.node.ts: -------------------------------------------------------------------------------- 1 | import { NodeConnectionTypes, type INodeType, type INodeTypeDescription } from 'n8n-workflow'; 2 | import { issueDescription } from './resources/issue'; 3 | import { issueCommentDescription } from './resources/issueComment'; 4 | import { getRepositories } from './listSearch/getRepositories'; 5 | import { getUsers } from './listSearch/getUsers'; 6 | import { getIssues } from './listSearch/getIssues'; 7 | 8 | export class GithubIssues implements INodeType { 9 | description: INodeTypeDescription = { 10 | displayName: 'GitHub Issues', 11 | name: 'githubIssues', 12 | icon: { light: 'file:../../icons/github.svg', dark: 'file:../../icons/github.dark.svg' }, 13 | group: ['input'], 14 | version: 1, 15 | subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', 16 | description: 'Consume issues from the GitHub API', 17 | defaults: { 18 | name: 'GitHub Issues', 19 | }, 20 | usableAsTool: true, 21 | inputs: [NodeConnectionTypes.Main], 22 | outputs: [NodeConnectionTypes.Main], 23 | credentials: [ 24 | { 25 | name: 'githubIssuesApi', 26 | required: true, 27 | displayOptions: { 28 | show: { 29 | authentication: ['accessToken'], 30 | }, 31 | }, 32 | }, 33 | { 34 | name: 'githubIssuesOAuth2Api', 35 | required: true, 36 | displayOptions: { 37 | show: { 38 | authentication: ['oAuth2'], 39 | }, 40 | }, 41 | }, 42 | ], 43 | requestDefaults: { 44 | baseURL: 'https://api.github.com', 45 | headers: { 46 | Accept: 'application/json', 47 | 'Content-Type': 'application/json', 48 | }, 49 | }, 50 | properties: [ 51 | { 52 | displayName: 'Authentication', 53 | name: 'authentication', 54 | type: 'options', 55 | options: [ 56 | { 57 | name: 'Access Token', 58 | value: 'accessToken', 59 | }, 60 | { 61 | name: 'OAuth2', 62 | value: 'oAuth2', 63 | }, 64 | ], 65 | default: 'accessToken', 66 | }, 67 | { 68 | displayName: 'Resource', 69 | name: 'resource', 70 | type: 'options', 71 | noDataExpression: true, 72 | options: [ 73 | { 74 | name: 'Issue', 75 | value: 'issue', 76 | }, 77 | { 78 | name: 'Issue Comment', 79 | value: 'issueComment', 80 | }, 81 | ], 82 | default: 'issue', 83 | }, 84 | ...issueDescription, 85 | ...issueCommentDescription, 86 | ], 87 | }; 88 | 89 | methods = { 90 | listSearch: { 91 | getRepositories, 92 | getUsers, 93 | getIssues, 94 | }, 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /nodes/Example/Example.node.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IExecuteFunctions, 3 | INodeExecutionData, 4 | INodeType, 5 | INodeTypeDescription, 6 | } from 'n8n-workflow'; 7 | import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow'; 8 | 9 | export class Example implements INodeType { 10 | description: INodeTypeDescription = { 11 | displayName: 'Example', 12 | name: 'example', 13 | icon: { light: 'file:example.svg', dark: 'file:example.dark.svg' }, 14 | group: ['input'], 15 | version: 1, 16 | description: 'Basic Example Node', 17 | defaults: { 18 | name: 'Example', 19 | }, 20 | inputs: [NodeConnectionTypes.Main], 21 | outputs: [NodeConnectionTypes.Main], 22 | usableAsTool: true, 23 | properties: [ 24 | // Node properties which the user gets displayed and 25 | // can change on the node. 26 | { 27 | displayName: 'My String', 28 | name: 'myString', 29 | type: 'string', 30 | default: '', 31 | placeholder: 'Placeholder value', 32 | description: 'The description text', 33 | }, 34 | ], 35 | }; 36 | 37 | // The function below is responsible for actually doing whatever this node 38 | // is supposed to do. In this case, we're just appending the `myString` property 39 | // with whatever the user has entered. 40 | // You can make async calls and use `await`. 41 | async execute(this: IExecuteFunctions): Promise { 42 | const items = this.getInputData(); 43 | 44 | let item: INodeExecutionData; 45 | let myString: string; 46 | 47 | // Iterates over all input items and add the key "myString" with the 48 | // value the parameter "myString" resolves to. 49 | // (This could be a different value for each item in case it contains an expression) 50 | for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { 51 | try { 52 | myString = this.getNodeParameter('myString', itemIndex, '') as string; 53 | item = items[itemIndex]; 54 | 55 | item.json.myString = myString; 56 | } catch (error) { 57 | // This node should never fail but we want to showcase how 58 | // to handle errors. 59 | if (this.continueOnFail()) { 60 | items.push({ json: this.getInputData(itemIndex)[0].json, error, pairedItem: itemIndex }); 61 | } else { 62 | // Adding `itemIndex` allows other workflows to handle this error 63 | if (error.context) { 64 | // If the error thrown already contains the context property, 65 | // only append the itemIndex 66 | error.context.itemIndex = itemIndex; 67 | throw error; 68 | } 69 | throw new NodeOperationError(this.getNode(), error, { 70 | itemIndex, 71 | }); 72 | } 73 | } 74 | } 75 | 76 | return [items]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /nodes/GithubIssues/resources/issue/getAll.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | import { parseLinkHeader } from '../../shared/utils'; 3 | 4 | const showOnlyForIssueGetMany = { 5 | operation: ['getAll'], 6 | resource: ['issue'], 7 | }; 8 | 9 | export const issueGetManyDescription: INodeProperties[] = [ 10 | { 11 | displayName: 'Limit', 12 | name: 'limit', 13 | type: 'number', 14 | displayOptions: { 15 | show: { 16 | ...showOnlyForIssueGetMany, 17 | returnAll: [false], 18 | }, 19 | }, 20 | typeOptions: { 21 | minValue: 1, 22 | maxValue: 100, 23 | }, 24 | default: 50, 25 | routing: { 26 | send: { 27 | type: 'query', 28 | property: 'per_page', 29 | }, 30 | output: { 31 | maxResults: '={{$value}}', 32 | }, 33 | }, 34 | description: 'Max number of results to return', 35 | }, 36 | { 37 | displayName: 'Return All', 38 | name: 'returnAll', 39 | type: 'boolean', 40 | displayOptions: { 41 | show: showOnlyForIssueGetMany, 42 | }, 43 | default: false, 44 | description: 'Whether to return all results or only up to a given limit', 45 | routing: { 46 | send: { 47 | paginate: '={{ $value }}', 48 | type: 'query', 49 | property: 'per_page', 50 | value: '100', 51 | }, 52 | operations: { 53 | pagination: { 54 | type: 'generic', 55 | properties: { 56 | continue: `={{ !!(${parseLinkHeader.toString()})($response.headers?.link).next }}`, 57 | request: { 58 | url: `={{ (${parseLinkHeader.toString()})($response.headers?.link)?.next ?? $request.url }}`, 59 | }, 60 | }, 61 | }, 62 | }, 63 | }, 64 | }, 65 | { 66 | displayName: 'Filters', 67 | name: 'filters', 68 | type: 'collection', 69 | typeOptions: { 70 | multipleValueButtonText: 'Add Filter', 71 | }, 72 | displayOptions: { 73 | show: showOnlyForIssueGetMany, 74 | }, 75 | default: {}, 76 | options: [ 77 | { 78 | displayName: 'Updated Since', 79 | name: 'since', 80 | type: 'dateTime', 81 | default: '', 82 | description: 'Return only issues updated at or after this time', 83 | routing: { 84 | request: { 85 | qs: { 86 | since: '={{$value}}', 87 | }, 88 | }, 89 | }, 90 | }, 91 | { 92 | displayName: 'State', 93 | name: 'state', 94 | type: 'options', 95 | options: [ 96 | { 97 | name: 'All', 98 | value: 'all', 99 | description: 'Returns issues with any state', 100 | }, 101 | { 102 | name: 'Closed', 103 | value: 'closed', 104 | description: 'Return issues with "closed" state', 105 | }, 106 | { 107 | name: 'Open', 108 | value: 'open', 109 | description: 'Return issues with "open" state', 110 | }, 111 | ], 112 | default: 'open', 113 | description: 'The issue state to filter on', 114 | routing: { 115 | request: { 116 | qs: { 117 | state: '={{$value}}', 118 | }, 119 | }, 120 | }, 121 | }, 122 | ], 123 | }, 124 | ]; 125 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jan@n8n.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /nodes/GithubIssues/shared/descriptions.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const repoOwnerSelect: INodeProperties = { 4 | displayName: 'Repository Owner', 5 | name: 'owner', 6 | type: 'resourceLocator', 7 | default: { mode: 'list', value: '' }, 8 | required: true, 9 | modes: [ 10 | { 11 | displayName: 'Repository Owner', 12 | name: 'list', 13 | type: 'list', 14 | placeholder: 'Select an owner...', 15 | typeOptions: { 16 | searchListMethod: 'getUsers', 17 | searchable: true, 18 | searchFilterRequired: false, 19 | }, 20 | }, 21 | { 22 | displayName: 'Link', 23 | name: 'url', 24 | type: 'string', 25 | placeholder: 'e.g. https://github.com/n8n-io', 26 | extractValue: { 27 | type: 'regex', 28 | regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)', 29 | }, 30 | validation: [ 31 | { 32 | type: 'regex', 33 | properties: { 34 | regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)(?:.*)', 35 | errorMessage: 'Not a valid GitHub URL', 36 | }, 37 | }, 38 | ], 39 | }, 40 | { 41 | displayName: 'By Name', 42 | name: 'name', 43 | type: 'string', 44 | placeholder: 'e.g. n8n-io', 45 | validation: [ 46 | { 47 | type: 'regex', 48 | properties: { 49 | regex: '[-_a-zA-Z0-9]+', 50 | errorMessage: 'Not a valid GitHub Owner Name', 51 | }, 52 | }, 53 | ], 54 | url: '=https://github.com/{{$value}}', 55 | }, 56 | ], 57 | }; 58 | 59 | export const repoNameSelect: INodeProperties = { 60 | displayName: 'Repository Name', 61 | name: 'repository', 62 | type: 'resourceLocator', 63 | default: { 64 | mode: 'list', 65 | value: '', 66 | }, 67 | required: true, 68 | modes: [ 69 | { 70 | displayName: 'Repository Name', 71 | name: 'list', 72 | type: 'list', 73 | placeholder: 'Select an Repository...', 74 | typeOptions: { 75 | searchListMethod: 'getRepositories', 76 | searchable: true, 77 | }, 78 | }, 79 | { 80 | displayName: 'Link', 81 | name: 'url', 82 | type: 'string', 83 | placeholder: 'e.g. https://github.com/n8n-io/n8n', 84 | extractValue: { 85 | type: 'regex', 86 | regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)', 87 | }, 88 | validation: [ 89 | { 90 | type: 'regex', 91 | properties: { 92 | regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)(?:.*)', 93 | errorMessage: 'Not a valid GitHub Repository URL', 94 | }, 95 | }, 96 | ], 97 | }, 98 | { 99 | displayName: 'By Name', 100 | name: 'name', 101 | type: 'string', 102 | placeholder: 'e.g. n8n', 103 | validation: [ 104 | { 105 | type: 'regex', 106 | properties: { 107 | regex: '[-_.0-9a-zA-Z]+', 108 | errorMessage: 'Not a valid GitHub Repository Name', 109 | }, 110 | }, 111 | ], 112 | url: '=https://github.com/{{$parameter["owner"]}}/{{$value}}', 113 | }, 114 | ], 115 | displayOptions: { 116 | hide: { 117 | resource: ['user', 'organization'], 118 | operation: ['getRepositories'], 119 | }, 120 | }, 121 | }; 122 | 123 | export const issueSelect: INodeProperties = { 124 | displayName: 'Issue', 125 | name: 'issue', 126 | type: 'resourceLocator', 127 | default: { 128 | mode: 'list', 129 | value: '', 130 | }, 131 | required: true, 132 | modes: [ 133 | { 134 | displayName: 'Issue', 135 | name: 'list', 136 | type: 'list', 137 | placeholder: 'Select an Issue...', 138 | typeOptions: { 139 | searchListMethod: 'getIssues', 140 | searchable: true, 141 | }, 142 | }, 143 | { 144 | displayName: 'By ID', 145 | name: 'name', 146 | type: 'string', 147 | placeholder: 'e.g. 123', 148 | url: '=https://github.com/{{$parameter.owner}}/{{$parameter.repository}}/issues/{{$value}}', 149 | }, 150 | ], 151 | }; 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Banner image](https://user-images.githubusercontent.com/10284570/173569848-c624317f-42b1-45a6-ab09-f0ea3c247648.png) 2 | 3 | # n8n-nodes-starter 4 | 5 | This starter repository helps you build custom integrations for [n8n](https://n8n.io). It includes example nodes, credentials, the node linter, and all the tooling you need to get started. 6 | 7 | ## Quick Start 8 | 9 | > [!TIP] 10 | > **New to building n8n nodes?** The fastest way to get started is with `npm create @n8n/node`. This command scaffolds a complete node package for you using the [@n8n/node-cli](https://www.npmjs.com/package/@n8n/node-cli). 11 | 12 | **To create a new node package from scratch:** 13 | 14 | ```bash 15 | npm create @n8n/node 16 | ``` 17 | 18 | **Already using this starter? Start developing with:** 19 | 20 | ```bash 21 | npm run dev 22 | ``` 23 | 24 | This starts n8n with your nodes loaded and hot reload enabled. 25 | 26 | ## What's Included 27 | 28 | This starter repository includes two example nodes to learn from: 29 | 30 | - **[Example Node](nodes/Example/)** - A simple starter node that shows the basic structure with a custom `execute` method 31 | - **[GitHub Issues Node](nodes/GithubIssues/)** - A complete, production-ready example built using the **declarative style**: 32 | - **Low-code approach** - Define operations declaratively without writing request logic 33 | - Multiple resources (Issues, Comments) 34 | - Multiple operations (Get, Get All, Create) 35 | - Two authentication methods (OAuth2 and Personal Access Token) 36 | - List search functionality for dynamic dropdowns 37 | - Proper error handling and typing 38 | - Ideal for HTTP API-based integrations 39 | 40 | > [!TIP] 41 | > The declarative/low-code style (used in GitHub Issues) is the recommended approach for building nodes that interact with HTTP APIs. It significantly reduces boilerplate code and handles requests automatically. 42 | 43 | Browse these examples to understand both approaches, then modify them or create your own. 44 | 45 | ## Finding Inspiration 46 | 47 | Looking for more examples? Check out these resources: 48 | 49 | - **[npm Community Nodes](https://www.npmjs.com/search?q=keywords:n8n-community-node-package)** - Browse thousands of community-built nodes on npm using the `n8n-community-node-package` tag 50 | - **[n8n Built-in Nodes](https://github.com/n8n-io/n8n/tree/master/packages/nodes-base/nodes)** - Study the source code of n8n's official nodes for production-ready patterns and best practices 51 | - **[n8n Credentials](https://github.com/n8n-io/n8n/tree/master/packages/nodes-base/credentials)** - See how authentication is implemented for various services 52 | 53 | These are excellent resources to understand how to structure your nodes, handle different API patterns, and implement advanced features. 54 | 55 | ## Prerequisites 56 | 57 | Before you begin, install the following on your development machine: 58 | 59 | ### Required 60 | 61 | - **[Node.js](https://nodejs.org/)** (v22 or higher) and npm 62 | - Linux/Mac/WSL: Install via [nvm](https://github.com/nvm-sh/nvm) 63 | - Windows: Follow [Microsoft's NodeJS guide](https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows) 64 | - **[git](https://git-scm.com/downloads)** 65 | 66 | ### Recommended 67 | 68 | - Follow n8n's [development environment setup guide](https://docs.n8n.io/integrations/creating-nodes/build/node-development-environment/) 69 | 70 | > [!NOTE] 71 | > The `@n8n/node-cli` is included as a dev dependency and will be installed automatically when you run `npm install`. The CLI includes n8n for local development, so you don't need to install n8n globally. 72 | 73 | ## Getting Started with this Starter 74 | 75 | Follow these steps to create your own n8n community node package: 76 | 77 | ### 1. Create Your Repository 78 | 79 | [Generate a new repository](https://github.com/n8n-io/n8n-nodes-starter/generate) from this template, then clone it: 80 | 81 | ```bash 82 | git clone https://github.com//.git 83 | cd 84 | ``` 85 | 86 | ### 2. Install Dependencies 87 | 88 | ```bash 89 | npm install 90 | ``` 91 | 92 | This installs all required dependencies including the `@n8n/node-cli`. 93 | 94 | ### 3. Explore the Examples 95 | 96 | Browse the example nodes in [nodes/](nodes/) and [credentials/](credentials/) to understand the structure: 97 | 98 | - Start with [nodes/Example/](nodes/Example/) for a basic node 99 | - Study [nodes/GithubIssues/](nodes/GithubIssues/) for a real-world implementation 100 | 101 | ### 4. Build Your Node 102 | 103 | Edit the example nodes to fit your use case, or create new node files by copying the structure from [nodes/Example/](nodes/Example/). 104 | 105 | > [!TIP] 106 | > If you want to scaffold a completely new node package, use `npm create @n8n/node` to start fresh with the CLI's interactive generator. 107 | 108 | ### 5. Configure Your Package 109 | 110 | Update `package.json` with your details: 111 | 112 | - `name` - Your package name (must start with `n8n-nodes-`) 113 | - `author` - Your name and email 114 | - `repository` - Your repository URL 115 | - `description` - What your node does 116 | 117 | Make sure your node is registered in the `n8n.nodes` array. 118 | 119 | ### 6. Develop and Test Locally 120 | 121 | Start n8n with your node loaded: 122 | 123 | ```bash 124 | npm run dev 125 | ``` 126 | 127 | This command runs `n8n-node dev` which: 128 | 129 | - Builds your node with watch mode 130 | - Starts n8n with your node available 131 | - Automatically rebuilds when you make changes 132 | - Opens n8n in your browser (usually http://localhost:5678) 133 | 134 | You can now test your node in n8n workflows! 135 | 136 | > [!NOTE] 137 | > Learn more about CLI commands in the [@n8n/node-cli documentation](https://www.npmjs.com/package/@n8n/node-cli). 138 | 139 | ### 7. Lint Your Code 140 | 141 | Check for errors: 142 | 143 | ```bash 144 | npm run lint 145 | ``` 146 | 147 | Auto-fix issues when possible: 148 | 149 | ```bash 150 | npm run lint:fix 151 | ``` 152 | 153 | ### 8. Build for Production 154 | 155 | When ready to publish: 156 | 157 | ```bash 158 | npm run build 159 | ``` 160 | 161 | This compiles your TypeScript code to the `dist/` folder. 162 | 163 | ### 9. Prepare for Publishing 164 | 165 | Before publishing: 166 | 167 | 1. **Update documentation**: Replace this README with your node's documentation. Use [README_TEMPLATE.md](README_TEMPLATE.md) as a starting point. 168 | 2. **Update the LICENSE**: Add your details to the [LICENSE](LICENSE.md) file. 169 | 3. **Test thoroughly**: Ensure your node works in different scenarios. 170 | 171 | ### 10. Publish to npm 172 | 173 | Publish your package to make it available to the n8n community: 174 | 175 | ```bash 176 | npm publish 177 | ``` 178 | 179 | Learn more about [publishing to npm](https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry). 180 | 181 | ### 11. Submit for Verification (Optional) 182 | 183 | Get your node verified for n8n Cloud: 184 | 185 | 1. Ensure your node meets the [requirements](https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/): 186 | - Uses MIT license ✅ (included in this starter) 187 | - No external package dependencies 188 | - Follows n8n's design guidelines 189 | - Passes quality and security review 190 | 191 | 2. Submit through the [n8n Creator Portal](https://creators.n8n.io/nodes) 192 | 193 | **Benefits of verification:** 194 | 195 | - Available directly in n8n Cloud 196 | - Discoverable in the n8n nodes panel 197 | - Verified badge for quality assurance 198 | - Increased visibility in the n8n community 199 | 200 | ## Available Scripts 201 | 202 | This starter includes several npm scripts to streamline development: 203 | 204 | | Script | Description | 205 | | --------------------- | ---------------------------------------------------------------- | 206 | | `npm run dev` | Start n8n with your node and watch for changes (runs `n8n-node dev`) | 207 | | `npm run build` | Compile TypeScript to JavaScript for production (runs `n8n-node build`) | 208 | | `npm run build:watch` | Build in watch mode (auto-rebuild on changes) | 209 | | `npm run lint` | Check your code for errors and style issues (runs `n8n-node lint`) | 210 | | `npm run lint:fix` | Automatically fix linting issues when possible (runs `n8n-node lint --fix`) | 211 | | `npm run release` | Create a new release (runs `n8n-node release`) | 212 | 213 | > [!TIP] 214 | > These scripts use the [@n8n/node-cli](https://www.npmjs.com/package/@n8n/node-cli) under the hood. You can also run CLI commands directly, e.g., `npx n8n-node dev`. 215 | 216 | ## Troubleshooting 217 | 218 | ### My node doesn't appear in n8n 219 | 220 | 1. Make sure you ran `npm install` to install dependencies 221 | 2. Check that your node is listed in `package.json` under `n8n.nodes` 222 | 3. Restart the dev server with `npm run dev` 223 | 4. Check the console for any error messages 224 | 225 | ### Linting errors 226 | 227 | Run `npm run lint:fix` to automatically fix most common issues. For remaining errors, check the [n8n node development guidelines](https://docs.n8n.io/integrations/creating-nodes/). 228 | 229 | ### TypeScript errors 230 | 231 | Make sure you're using Node.js v22 or higher and have run `npm install` to get all type definitions. 232 | 233 | ## Resources 234 | 235 | - **[n8n Node Documentation](https://docs.n8n.io/integrations/creating-nodes/)** - Complete guide to building nodes 236 | - **[n8n Community Forum](https://community.n8n.io/)** - Get help and share your nodes 237 | - **[@n8n/node-cli Documentation](https://www.npmjs.com/package/@n8n/node-cli)** - CLI tool reference 238 | - **[n8n Creator Portal](https://creators.n8n.io/nodes)** - Submit your node for verification 239 | - **[Submit Community Nodes Guide](https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/)** - Verification requirements and process 240 | 241 | ## Contributing 242 | 243 | Have suggestions for improving this starter? [Open an issue](https://github.com/n8n-io/n8n-nodes-starter/issues) or submit a pull request! 244 | 245 | ## License 246 | 247 | [MIT](https://github.com/n8n-io/n8n-nodes-starter/blob/master/LICENSE.md) 248 | --------------------------------------------------------------------------------