The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"nodes": [
{
"id": "798ddb4b-94e6-4967-a1ab-9bb82a0050ec",
"name": "Fetch PR + Code Context (Correction)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-580,
520
],
"alwaysOutputData": true,
"parameters": {
"jsCode": "const toStr = (v) => String(v ?? '').trim();\nconst toNum = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; };\nconst encodePath = (p) => toStr(p).split('/').map(encodeURIComponent).join('/');\n\nconst getEnv = (key) => {\n try {\n if ($env && $env[key]) return String($env[key]).trim();\n } catch (_e) {}\n try {\n const p = (typeof globalThis !== 'undefined' && globalThis.process && globalThis.process.env)\n ? globalThis.process.env[key]\n : '';\n if (p) return String(p).trim();\n } catch (_e) {}\n return '';\n};\n\nconst reqFromNormalize = (() => {\n try {\n return $items('Normalize Chat Input', 0, 0)[0]?.json || {};\n } catch (_e) {\n return {};\n }\n})();\n\nconst req = {\n ...($json || {}),\n ...reqFromNormalize,\n};\n\nconst question = toStr(req.question || '');\nconst owner = toStr(req.owner || getEnv('GITHUB_OWNER') || 'oliversoft-tech');\nconst token = toStr(req.github_token || getEnv('GITHUB_TOKEN') || '');\nif (!token) {\n throw new Error('[Fetch PR + Code Context] GITHUB_TOKEN ausente. Defina em env (GITHUB_TOKEN) ou no Normalize Chat Input.');\n}\n\nconst rows = $input.all().map((i) => i.json || {});\nlet repos = rows\n .map((r) => toStr(r.repo_name || r.name || r.repository || r.slug || ''))\n .filter(Boolean)\n .filter((n) => n.includes('fastroute-'));\nif (repos.length === 0) repos = ['fastroute-domain', 'fastroute-api', 'fastroute-mobile-hybrid', 'fastroute-changemanagement'];\nrepos = [...new Set(repos)];\n\nconst gh = async (url) => this.helpers.httpRequest({\n method: 'GET',\n url,\n headers: {\n Authorization: 'Bearer ' + token,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28'\n },\n json: true\n});\n\nconst queryTerms = question\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ')\n .split(/\\s+/)\n .filter((t) => t.length >= 4)\n .slice(0, 10);\n\nconst allPrs = [];\nfor (const repo of repos) {\n const list = await gh('https://api.github.com/repos/' + owner + '/' + repo + '/pulls?state=all&sort=updated&direction=desc&per_page=100');\n for (const pr of (Array.isArray(list) ? list : [])) {\n const title = toStr(pr.title);\n const headRef = toStr(pr.head?.ref || '');\n const governanceLike = title.toLowerCase().includes('governance: impact analysis') || headRef.startsWith('codex/governance-');\n if (!governanceLike) continue;\n\n allPrs.push({\n repo,\n number: pr.number,\n title,\n state: pr.state,\n draft: !!pr.draft,\n created_at: pr.created_at,\n updated_at: pr.updated_at,\n merged_at: pr.merged_at,\n user: toStr(pr.user?.login || ''),\n url: toStr(pr.html_url || ''),\n body: toStr(pr.body || '').slice(0, 7000),\n head: toStr(pr.head?.ref || ''),\n base: toStr(pr.base?.ref || ''),\n });\n }\n}\n\nallPrs.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());\nconst selected = allPrs.slice(0, 25);\n\nconst fileIndex = [];\nconst commitHits = [];\nfor (const pr of selected) {\n try {\n const files = await gh('https://api.github.com/repos/' + owner + '/' + pr.repo + '/pulls/' + pr.number + '/files?per_page=100');\n pr.files = (Array.isArray(files) ? files : []).map((f) => ({\n filename: toStr(f.filename),\n status: toStr(f.status),\n additions: toNum(f.additions) || 0,\n deletions: toNum(f.deletions) || 0,\n raw_url: toStr(f.raw_url || ''),\n blob_url: toStr(f.blob_url || '')\n }));\n\n for (const f of pr.files) {\n fileIndex.push({\n repo: pr.repo,\n pr_number: pr.number,\n filename: f.filename,\n raw_url: f.raw_url,\n blob_url: f.blob_url,\n pr_url: pr.url,\n });\n }\n } catch (_e) {\n pr.files = [];\n }\n\n try {\n const commits = await gh('https://api.github.com/repos/' + owner + '/' + pr.repo + '/pulls/' + pr.number + '/commits?per_page=100');\n pr.commits = (Array.isArray(commits) ? commits : []).map((c) => {\n const msg = toStr(c?.commit?.message || '');\n return {\n sha: toStr(c?.sha || '').slice(0, 10),\n message: msg.slice(0, 900),\n author: toStr(c?.commit?.author?.name || c?.author?.login || ''),\n };\n });\n\n if (queryTerms.length > 0) {\n const text = pr.commits.map((c) => c.message).join(' \\n ').toLowerCase();\n const matched = queryTerms.filter((t) => text.includes(t));\n if (matched.length > 0) {\n commitHits.push({\n repo: pr.repo,\n pr_number: pr.number,\n url: pr.url,\n matched_terms: [...new Set(matched)],\n commit_messages: pr.commits.map((c) => c.message).slice(0, 10),\n });\n }\n }\n } catch (_e) {\n pr.commits = [];\n }\n}\n\nconst matches = fileIndex.filter((f) => {\n if (queryTerms.length === 0) return false;\n const hay = (f.filename + ' ' + f.blob_url).toLowerCase();\n return queryTerms.some((t) => hay.includes(t));\n}).slice(0, 20);\n\nconst codeSnippets = [];\nfor (const m of matches) {\n try {\n const content = await gh('https://api.github.com/repos/' + owner + '/' + m.repo + '/contents/' + encodePath(m.filename) + '?ref=main');\n let decoded = '';\n if (content && content.content) {\n decoded = Buffer.from(String(content.content).replace(/\\n/g, ''), 'base64').toString('utf8');\n }\n codeSnippets.push({\n repo: m.repo,\n pr_number: m.pr_number,\n filename: m.filename,\n pr_url: m.pr_url,\n code: toStr(decoded).slice(0, 2500)\n });\n } catch (_e) {\n // ignore fetch errors\n }\n}\n\nconst fastrouteModeloDados = {\n fonte: 'skill:fasterxml/references/modelo-dados.md',\n tabelas_chave: [\n 'orders', 'addresses', 'routes', 'route_waypoints', 'waypoint_delivery_photo',\n 'change_log', 'mutations_applied', 'users', 'clients', 'orders_import'\n ],\n relacionamentos_relevantes: [\n 'addresses.order_id -> orders.id',\n 'routes.import_id -> orders_import.id',\n 'routes.driver_id -> users.id',\n 'orders.import_id -> orders_import.id'\n ],\n pontos_atencao: [\n 'route_waypoints e routes usam status/version para ciclo de entrega',\n 'change_log e mutations_applied suportam trilha e idempotencia offline-first',\n 'dados de latitude/longitude aparecem em ordens e enderecos e impactam clustering/roteirizacao',\n 'fotos de comprovacao em waypoint_delivery_photo vinculam rota, waypoint e usuario'\n ]\n};\n\nreturn [{\n json: {\n route: req.route,\n question,\n owner,\n impact_workflow_id: req.impact_workflow_id,\n pr_context: {\n owner,\n repos,\n total_found: allPrs.length,\n selected_count: selected.length,\n query_terms: queryTerms,\n prs: selected,\n commit_text_hits: commitHits,\n code_snippets: codeSnippets,\n fastroute_modelo_dados: fastrouteModeloDados\n }\n }\n}];"
}
},
{
"id": "94a2ed38-2711-451c-bc65-69d8d48c57a7",
"name": "Basic LLM Chain (Correction Proposal)",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"typeVersion": 1.7,
"position": [
-320,
520
],
"parameters": {
"promptType": "define",
"text": "={{[\n 'Pedido de correcao em linguagem natural:',\n $json.question || '',\n '',\n 'Contexto de PRs e codigo (JSON):',\n JSON.stringify($json.pr_context || {}, null, 2)\n].join('\\n')}}",
"hasOutputParser": false,
"messages": {
"messageValues": [
{
"message": "Voce e um arquiteto de mudancas e deve propor uma correcao a partir de PRs/codigo/commits existentes.\nConsidere tambem o modelo de dados do FastRoute presente no contexto.\n\nRetorne SOMENTE JSON valido sem markdown, no formato:\n{\n \"status\": \"SIM|PARCIAL|NAO|SEM_EVIDENCIA\",\n \"diagnostico\": \"string\",\n \"impact_payload\": {\n \"problema\": \"string\",\n \"mudanca\": \"string\",\n \"evidencias\": \"string\",\n \"restricoes\": \"string\"\n },\n \"evidencias\": [\n { \"repo\": \"string\", \"pr_number\": number, \"url\": \"string\", \"arquivos\": [\"string\"], \"commits\": [\"string\"] }\n ]\n}\n\nRegras:\n1) impact_payload deve ser preenchido para acionar Impact Analysis.\n2) Se faltar evidencia, use status=SEM_EVIDENCIA e descreva lacunas em diagnostico.\n3) Nao invente PRs/arquivos/commits."
}
]
},
"batching": {
"batchSize": 1
}
}
},
{
"id": "26d24e09-7dc5-4521-a849-480ee702fc9c",
"name": "Fetch PR + Code Context (PR QA)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-580,
760
],
"alwaysOutputData": true,
"parameters": {
"jsCode": "const toStr = (v) => String(v ?? '').trim();\nconst toNum = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null; };\nconst encodePath = (p) => toStr(p).split('/').map(encodeURIComponent).join('/');\n\nconst getEnv = (key) => {\n try {\n if ($env && $env[key]) return String($env[key]).trim();\n } catch (_e) {}\n try {\n const p = (typeof globalThis !== 'undefined' && globalThis.process && globalThis.process.env)\n ? globalThis.process.env[key]\n : '';\n if (p) return String(p).trim();\n } catch (_e) {}\n return '';\n};\n\nconst reqFromNormalize = (() => {\n try {\n return $items('Normalize Chat Input', 0, 0)[0]?.json || {};\n } catch (_e) {\n return {};\n }\n})();\n\nconst req = {\n ...($json || {}),\n ...reqFromNormalize,\n};\n\nconst question = toStr(req.question || '');\nconst owner = toStr(req.owner || getEnv('GITHUB_OWNER') || 'oliversoft-tech');\nconst token = toStr(req.github_token || getEnv('GITHUB_TOKEN') || '');\nif (!token) {\n throw new Error('[Fetch PR + Code Context] GITHUB_TOKEN ausente. Defina em env (GITHUB_TOKEN) ou no Normalize Chat Input.');\n}\n\nconst rows = $input.all().map((i) => i.json || {});\nlet repos = rows\n .map((r) => toStr(r.repo_name || r.name || r.repository || r.slug || ''))\n .filter(Boolean)\n .filter((n) => n.includes('fastroute-'));\nif (repos.length === 0) repos = ['fastroute-domain', 'fastroute-api', 'fastroute-mobile-hybrid', 'fastroute-changemanagement'];\nrepos = [...new Set(repos)];\n\nconst gh = async (url) => this.helpers.httpRequest({\n method: 'GET',\n url,\n headers: {\n Authorization: 'Bearer ' + token,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28'\n },\n json: true\n});\n\nconst queryTerms = question\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ')\n .split(/\\s+/)\n .filter((t) => t.length >= 4)\n .slice(0, 10);\n\nconst allPrs = [];\nfor (const repo of repos) {\n const list = await gh('https://api.github.com/repos/' + owner + '/' + repo + '/pulls?state=all&sort=updated&direction=desc&per_page=100');\n for (const pr of (Array.isArray(list) ? list : [])) {\n const title = toStr(pr.title);\n const headRef = toStr(pr.head?.ref || '');\n const governanceLike = title.toLowerCase().includes('governance: impact analysis') || headRef.startsWith('codex/governance-');\n if (!governanceLike) continue;\n\n allPrs.push({\n repo,\n number: pr.number,\n title,\n state: pr.state,\n draft: !!pr.draft,\n created_at: pr.created_at,\n updated_at: pr.updated_at,\n merged_at: pr.merged_at,\n user: toStr(pr.user?.login || ''),\n url: toStr(pr.html_url || ''),\n body: toStr(pr.body || '').slice(0, 7000),\n head: toStr(pr.head?.ref || ''),\n base: toStr(pr.base?.ref || ''),\n });\n }\n}\n\nallPrs.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());\nconst selected = allPrs.slice(0, 25);\n\nconst fileIndex = [];\nconst commitHits = [];\nfor (const pr of selected) {\n try {\n const files = await gh('https://api.github.com/repos/' + owner + '/' + pr.repo + '/pulls/' + pr.number + '/files?per_page=100');\n pr.files = (Array.isArray(files) ? files : []).map((f) => ({\n filename: toStr(f.filename),\n status: toStr(f.status),\n additions: toNum(f.additions) || 0,\n deletions: toNum(f.deletions) || 0,\n raw_url: toStr(f.raw_url || ''),\n blob_url: toStr(f.blob_url || '')\n }));\n\n for (const f of pr.files) {\n fileIndex.push({\n repo: pr.repo,\n pr_number: pr.number,\n filename: f.filename,\n raw_url: f.raw_url,\n blob_url: f.blob_url,\n pr_url: pr.url,\n });\n }\n } catch (_e) {\n pr.files = [];\n }\n\n try {\n const commits = await gh('https://api.github.com/repos/' + owner + '/' + pr.repo + '/pulls/' + pr.number + '/commits?per_page=100');\n pr.commits = (Array.isArray(commits) ? commits : []).map((c) => {\n const msg = toStr(c?.commit?.message || '');\n return {\n sha: toStr(c?.sha || '').slice(0, 10),\n message: msg.slice(0, 900),\n author: toStr(c?.commit?.author?.name || c?.author?.login || ''),\n };\n });\n\n if (queryTerms.length > 0) {\n const text = pr.commits.map((c) => c.message).join(' \\n ').toLowerCase();\n const matched = queryTerms.filter((t) => text.includes(t));\n if (matched.length > 0) {\n commitHits.push({\n repo: pr.repo,\n pr_number: pr.number,\n url: pr.url,\n matched_terms: [...new Set(matched)],\n commit_messages: pr.commits.map((c) => c.message).slice(0, 10),\n });\n }\n }\n } catch (_e) {\n pr.commits = [];\n }\n}\n\nconst matches = fileIndex.filter((f) => {\n if (queryTerms.length === 0) return false;\n const hay = (f.filename + ' ' + f.blob_url).toLowerCase();\n return queryTerms.some((t) => hay.includes(t));\n}).slice(0, 20);\n\nconst codeSnippets = [];\nfor (const m of matches) {\n try {\n const content = await gh('https://api.github.com/repos/' + owner + '/' + m.repo + '/contents/' + encodePath(m.filename) + '?ref=main');\n let decoded = '';\n if (content && content.content) {\n decoded = Buffer.from(String(content.content).replace(/\\n/g, ''), 'base64').toString('utf8');\n }\n codeSnippets.push({\n repo: m.repo,\n pr_number: m.pr_number,\n filename: m.filename,\n pr_url: m.pr_url,\n code: toStr(decoded).slice(0, 2500)\n });\n } catch (_e) {\n // ignore fetch errors\n }\n}\n\nconst fastrouteModeloDados = {\n fonte: 'skill:fasterxml/references/modelo-dados.md',\n tabelas_chave: [\n 'orders', 'addresses', 'routes', 'route_waypoints', 'waypoint_delivery_photo',\n 'change_log', 'mutations_applied', 'users', 'clients', 'orders_import'\n ],\n relacionamentos_relevantes: [\n 'addresses.order_id -> orders.id',\n 'routes.import_id -> orders_import.id',\n 'routes.driver_id -> users.id',\n 'orders.import_id -> orders_import.id'\n ],\n pontos_atencao: [\n 'route_waypoints e routes usam status/version para ciclo de entrega',\n 'change_log e mutations_applied suportam trilha e idempotencia offline-first',\n 'dados de latitude/longitude aparecem em ordens e enderecos e impactam clustering/roteirizacao',\n 'fotos de comprovacao em waypoint_delivery_photo vinculam rota, waypoint e usuario'\n ]\n};\n\nreturn [{\n json: {\n route: req.route,\n question,\n owner,\n impact_workflow_id: req.impact_workflow_id,\n pr_context: {\n owner,\n repos,\n total_found: allPrs.length,\n selected_count: selected.length,\n query_terms: queryTerms,\n prs: selected,\n commit_text_hits: commitHits,\n code_snippets: codeSnippets,\n fastroute_modelo_dados: fastrouteModeloDados\n }\n }\n}];"
}
},
{
"id": "869c3447-aadc-4ca0-846e-d9c4495b8cce",
"name": "Basic LLM Chain (PR Questions)",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"typeVersion": 1.7,
"position": [
-320,
760
],
"parameters": {
"promptType": "define",
"text": "={{[\n 'Pergunta do usuario:',\n $json.question || '',\n '',\n 'Contexto de PRs e codigo (JSON):',\n JSON.stringify($json.pr_context || {}, null, 2)\n].join('\\n')}}",
"hasOutputParser": false,
"messages": {
"messageValues": [
{
"message": "Voce responde perguntas sobre implementacao de necessidades com base em PRs de governance, textos de commits e trechos de codigo.\n\nQuando a pergunta for algo como \"essa necessidade ja foi implementada?\", responda com:\n- Status: SIM | PARCIAL | NAO | SEM_EVIDENCIA\n- Justificativa tecnica\n- Evidencias com links de PR, arquivos e/ou commits\n- Gaps encontrados\n\nRegras:\n1) Use somente o contexto recebido (prs, commit_text_hits, code_snippets, fastroute_modelo_dados).\n2) Nao invente evidencias.\n3) Seja objetivo e tecnico em portugues."
}
]
},
"batching": {
"batchSize": 1
}
}
}
],
"connections": {}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
4-Nodes-Pr-Search-Commits-Modelo-Dados. Uses chainLlm. Manual trigger; 4 nodes.
Source: https://github.com/oliverbill/fastroute-changemanagement/blob/01c24d7d3e3621e97a82a810e066b272ee8a0025/docs/n8n/4-nodes-pr-search-commits-modelo-dados.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
RoboNuggets - Faceless POV AI Machine (R24). Uses scheduleTrigger, googleSheets, chainLlm, lmChatOpenAi. Scheduled trigger; 31 nodes.
Video Automation (images only). Uses chainLlm, lmChatOpenAi, outputParserStructured, splitOut. Scheduled trigger; 28 nodes.
This n8n template demonstrates how to automate personalized cold email follow-ups using AI personalization and database tracking. Perfect for sales teams, recruiters, and agencies managing high-volume
Narrating Over A Video Using Multimodal Ai. Uses lmChatOpenAi, splitOut, httpRequest, convertToFile. Event-driven trigger; 21 nodes.
Executecommand Localfile. Uses localFileTrigger, executeCommand, lmChatMistralCloud, outputParserStructured. Event-driven trigger; 16 nodes.