{ "name": "Jira and Github Automated Standup", "nodes": [ { "parameters": { "assignments": { "assignments": [ { "id": "config-lookback", "name": "lookback_days", "value": 7, "type": "number" }, { "id": "config-jira-project", "name": "jira_project", "value": "KEY", "type": "string" }, { "id": "config-jira-domain", "name": "jira_domain", "value": "SUBDOMAIN.atlassian.net", "type": "string" }, { "id": "config-github-owner", "name": "github_owner", "value": "REPO_OWNER", "type": "string" }, { "id": "config-github-repo", "name": "github_repo", "value": "REPO_NAME", "type": "string" }, { "id": "config-jira-statuses", "name": "jira_in_progress_statuses", "value": "\"In Development\", \"In Code Review\", \"Ready for QA\", \"In QA\", \"Failed QA\"", "type": "string" }, { "id": "config-github-username", "name": "github_username", "value": "YOUR_GITHUB_USERNAME", "type": "string" }, { "id": "config-email", "name": "email", "value": "you@example.com", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [-240, 512], "id": "4838124b-0632-498b-996a-6bee5b53d833", "name": "Config" }, { "parameters": { "rule": { "interval": [ { "field": "weeks", "triggerAtDay": [1], "triggerAtHour": 8 } ] } }, "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.2, "position": [-464, 512], "id": "a122d1ca-aab7-4c3e-8b0d-9da5d6361e10", "name": "Schedule Trigger" }, { "parameters": { "url": "=https://api.github.com/repos/{{ $('Config').item.json.github_owner }}/{{ $('Config').item.json.github_repo }}/commits", "authentication": "predefinedCredentialType", "nodeCredentialType": "githubApi", "sendQuery": true, "queryParameters": { "parameters": [ { "name": "author", "value": "={{ $('Config').item.json.github_username }}" }, { "name": "since", "value": "={{ new Date(Date.now() - $('Config').item.json.lookback_days * 24 * 60 * 60 * 1000).toISOString() }}" } ] }, "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [-16, 16], "id": "b5e6281b-b5a0-487c-bef4-8586d2469349", "name": "Get Commits", "credentials": { "githubApi": { "id": "ozdwIqwwCpEbZDMC", "name": "GitHub account 2" } } }, { "parameters": { "url": "=https://api.github.com/repos/{{ $('Config').item.json.github_owner }}/{{ $('Config').item.json.github_repo }}/commits/{{ $json.sha }}", "authentication": "predefinedCredentialType", "nodeCredentialType": "githubApi", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [208, 16], "id": "f44bbdce-9151-4f45-b2ee-ac8b7716d879", "name": "Get Commit Details", "credentials": { "githubApi": { "id": "ozdwIqwwCpEbZDMC", "name": "GitHub account 2" } } }, { "parameters": { "aggregate": "aggregateAllItemData", "destinationFieldName": "commits", "include": "specifiedFields", "fieldsToInclude": "files, commit", "options": {} }, "type": "n8n-nodes-base.aggregate", "typeVersion": 1, "position": [432, 16], "id": "2f2b6e7d-ee0c-49e0-bd47-7b20c8d3e9ac", "name": "Aggregate Commits", "alwaysOutputData": true }, { "parameters": { "jsCode": "return $input.all().map(item => ({\n commits: (item.json.commits || []).map(commit => ({\n message: commit.commit?.message,\n files: (commit.files || []).map(file => file.filename)\n }))\n}));" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [656, 16], "id": "927a9aff-1ece-4a87-95c2-8942994f7a8a", "name": "Clean Commit Data" }, { "parameters": { "resource": "repository", "operation": "getPullRequests", "owner": { "__rl": true, "value": "={{ $('Config').item.json.github_owner }}", "mode": "id" }, "repository": { "__rl": true, "value": "={{ $('Config').item.json.github_repo }}", "mode": "id" }, "getRepositoryPullRequestsFilters": { "state": "open" } }, "type": "n8n-nodes-base.github", "typeVersion": 1.1, "position": [208, 208], "id": "757d7201-b1b7-4a9d-b889-9569d498748f", "name": "Get Open PRs", "alwaysOutputData": true, "webhookId": "3bd0fdd2-4c9f-4ff7-ae62-92bd6e11fc0e", "credentials": { "githubApi": { "id": "ozdwIqwwCpEbZDMC", "name": "GitHub account 2" } } }, { "parameters": { "aggregate": "aggregateAllItemData", "destinationFieldName": "open_prs", "options": {} }, "type": "n8n-nodes-base.aggregate", "typeVersion": 1, "position": [432, 208], "id": "a3a78590-bc13-4fc9-b69a-0f7cca4bffdf", "name": "Aggregate PRs", "alwaysOutputData": true }, { "parameters": { "jsCode": "return $input.all().map(item => ({\n open_prs: (item.json.open_prs || []).filter(pr => pr.number).map(pr => ({\n number: pr.number,\n title: pr.title,\n state: pr.state,\n draft: pr.draft,\n html_url: pr.html_url,\n body: pr.body,\n created_at: pr.created_at,\n updated_at: pr.updated_at,\n author: pr.user?.login,\n head_branch: pr.head?.ref,\n base_branch: pr.base?.ref,\n labels: (pr.labels || []).map(l => l.name)\n }))\n}));" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [656, 208], "id": "b6d58dae-8312-4fff-864c-e4f10e01654d", "name": "Clean PR Data" }, { "parameters": { "url": "=https://{{ $('Config').item.json.jira_domain }}/rest/api/3/search/jql", "authentication": "predefinedCredentialType", "nodeCredentialType": "jiraSoftwareCloudApi", "sendQuery": true, "queryParameters": { "parameters": [ { "name": "jql", "value": "=project = {{ $('Config').item.json.jira_project }} AND assignee = currentUser() AND status IN ({{ $('Config').item.json.jira_in_progress_statuses }})" }, { "name": "maxResults", "value": "50" }, { "name": "expand", "value": "changelog" } ] }, "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [-16, 400], "id": "f01dffa9-b311-4a3a-bf1b-6a5d267911f5", "name": "Get In Progress Tickets", "credentials": { "jiraSoftwareCloudApi": { "id": "it5KydQKiF1IfhcD", "name": "Jira SW Cloud account" } } }, { "parameters": { "url": "=https://{{ $('Config').item.json.jira_domain }}/rest/api/3/search/jql", "authentication": "predefinedCredentialType", "nodeCredentialType": "jiraSoftwareCloudApi", "sendQuery": true, "queryParameters": { "parameters": [ { "name": "jql", "value": "=project = {{ $('Config').item.json.jira_project }} AND assignee = currentUser() AND updated >= -{{ $('Config').item.json.lookback_days }}d" }, { "name": "maxResults", "value": "50" }, { "name": "expand", "value": "changelog" } ] }, "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [-16, 592], "id": "d8117682-ecd8-4f0b-8e54-791e092a7d17", "name": "Get Recently Updated", "credentials": { "jiraSoftwareCloudApi": { "id": "it5KydQKiF1IfhcD", "name": "Jira SW Cloud account" } } }, { "parameters": { "url": "=https://{{ $('Config').item.json.jira_domain }}/rest/api/3/search/jql", "authentication": "predefinedCredentialType", "nodeCredentialType": "jiraSoftwareCloudApi", "sendQuery": true, "queryParameters": { "parameters": [ { "name": "jql", "value": "=project = {{ $('Config').item.json.jira_project }} AND status CHANGED BY currentUser() DURING (-{{ $('Config').item.json.lookback_days }}d, now())" }, { "name": "maxResults", "value": "50" }, { "name": "expand", "value": "changelog" } ] }, "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [-16, 784], "id": "5919e184-643c-4d24-8fb0-02ccf1ad3f34", "name": "Get My Transitions", "credentials": { "jiraSoftwareCloudApi": { "id": "it5KydQKiF1IfhcD", "name": "Jira SW Cloud account" } } }, { "parameters": { "url": "=https://{{ $('Config').item.json.jira_domain }}/rest/api/3/search/jql", "authentication": "predefinedCredentialType", "nodeCredentialType": "jiraSoftwareCloudApi", "sendQuery": true, "queryParameters": { "parameters": [ { "name": "jql", "value": "=project = {{ $('Config').item.json.jira_project }} AND updatedDate >= -{{ $('Config').item.json.lookback_days }}d AND (comment ~ currentUser() OR worklog ~ currentUser())" }, { "name": "maxResults", "value": "50" }, { "name": "expand", "value": "changelog" } ] }, "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [-16, 976], "id": "d2d0b427-9955-4404-999a-af4ca5f011e1", "name": "Get My Activity", "credentials": { "jiraSoftwareCloudApi": { "id": "it5KydQKiF1IfhcD", "name": "Jira SW Cloud account" } } }, { "parameters": { "aggregate": "aggregateAllItemData", "destinationFieldName": "in_progress_tickets", "options": {} }, "type": "n8n-nodes-base.aggregate", "typeVersion": 1, "position": [208, 400], "id": "5c9f6a55-7855-429f-9c3a-11057891f57f", "name": "Aggregate In Progress", "alwaysOutputData": true }, { "parameters": { "aggregate": "aggregateAllItemData", "destinationFieldName": "recently_updated", "options": {} }, "type": "n8n-nodes-base.aggregate", "typeVersion": 1, "position": [208, 592], "id": "aca015c7-5e2a-434a-8fe0-eb8f0941e46a", "name": "Aggregate Updated", "alwaysOutputData": true }, { "parameters": { "aggregate": "aggregateAllItemData", "destinationFieldName": "my_transitions", "options": {} }, "type": "n8n-nodes-base.aggregate", "typeVersion": 1, "position": [208, 784], "id": "1596dce3-9a2f-4933-a495-05aaf0e8afcd", "name": "Aggregate Transitions", "alwaysOutputData": true }, { "parameters": { "aggregate": "aggregateAllItemData", "destinationFieldName": "my_activity", "options": {} }, "type": "n8n-nodes-base.aggregate", "typeVersion": 1, "position": [208, 976], "id": "3ab43bed-3305-4069-8147-4caf3522a684", "name": "Aggregate Activity", "alwaysOutputData": true }, { "parameters": { "mode": "combine", "combineBy": "combineByPosition", "numberInputs": 4, "options": {} }, "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [432, 656], "id": "4874b1ee-3ed6-4d69-915a-3f1c9be6e53a", "name": "Merge Jira Data", "alwaysOutputData": true }, { "parameters": { "jsCode": "// Consolidate and deduplicate Jira tickets, tracking activity types\nconst input = $input.first().json;\nconst config = $('Config').first().json;\n\nconst inProgress = (input.in_progress_tickets || []).flatMap(r => r.issues || []);\nconst recentlyUpdated = (input.recently_updated || []).flatMap(r => r.issues || []);\nconst transitions = (input.my_transitions || []).flatMap(r => r.issues || []);\nconst activity = (input.my_activity || []).flatMap(r => r.issues || []);\n\nconst issueMap = new Map();\nconst lookbackMs = config.lookback_days * 24 * 60 * 60 * 1000;\nconst cutoffDate = new Date(Date.now() - lookbackMs);\n\nfunction processIssue(issue, activityType) {\n if (!issue || !issue.key) return;\n \n const key = issue.key;\n if (!issueMap.has(key)) {\n issueMap.set(key, {\n key: issue.key,\n summary: issue.fields?.summary || 'No summary',\n status: issue.fields?.status?.name || 'Unknown',\n priority: issue.fields?.priority?.name || 'None',\n updated: issue.fields?.updated,\n url: `https://${config.jira_domain}/browse/${issue.key}`,\n activity_types: [],\n recent_transitions: [],\n assignee: issue.fields?.assignee?.displayName || 'Unassigned'\n });\n }\n \n const entry = issueMap.get(key);\n if (!entry.activity_types.includes(activityType)) {\n entry.activity_types.push(activityType);\n }\n \n // Extract recent transitions from changelog\n if (issue.changelog?.histories) {\n for (const history of issue.changelog.histories) {\n const historyDate = new Date(history.created);\n if (historyDate >= cutoffDate) {\n for (const item of history.items || []) {\n if (item.field === 'status') {\n const transition = {\n from: item.fromString,\n to: item.toString,\n when: history.created,\n by: history.author?.displayName\n };\n // Avoid duplicates\n const exists = entry.recent_transitions.some(\n t => t.from === transition.from && t.to === transition.to && t.when === transition.when\n );\n if (!exists) {\n entry.recent_transitions.push(transition);\n }\n }\n }\n }\n }\n }\n}\n\n// Process each category\ninProgress.forEach(issue => processIssue(issue, 'in_progress'));\nrecentlyUpdated.forEach(issue => processIssue(issue, 'recently_updated'));\ntransitions.forEach(issue => processIssue(issue, 'transitioned'));\nactivity.forEach(issue => processIssue(issue, 'commented_or_logged'));\n\nconst tickets = Array.from(issueMap.values());\n\n// Sort by most recent update\ntickets.sort((a, b) => new Date(b.updated) - new Date(a.updated));\n\nreturn [{\n json: {\n jira_tickets: tickets,\n jira_summary: {\n total_active: tickets.length,\n in_progress: tickets.filter(t => t.activity_types.includes('in_progress')).length,\n updated_today: tickets.filter(t => t.activity_types.includes('recently_updated')).length,\n transitioned: tickets.filter(t => t.activity_types.includes('transitioned')).length,\n with_comments: tickets.filter(t => t.activity_types.includes('commented_or_logged')).length\n }\n }\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [656, 688], "id": "07b865a2-08a7-4490-ad2b-11ecd2a88f07", "name": "Clean Jira Data" }, { "parameters": { "mode": "combine", "combineBy": "combineByPosition", "numberInputs": 3, "options": {} }, "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [880, 192], "id": "b27ba933-afd5-40ad-9e20-869726df7fec", "name": "Merge All", "alwaysOutputData": true }, { "parameters": { "modelId": "claude-sonnet-4-20250514", "messages": { "values": [ { "content": "=You are a helpful assistant that generates concise standup reports for software developers. Based on the GitHub and Jira data provided, create a brief standup update that covers:\n\n1. **What I worked on** - Recent commits, PRs, and Jira tickets that were updated or transitioned\n2. **What I'm currently working on** - Open PRs and in-progress Jira tickets\n3. **Any blockers or notes** - Failed QA items, draft PRs, stale tickets, etc.\n\nKeep it conversational and brief - this is for a quick standup, not a detailed report. Use ticket keys (e.g., {{ $('Config').item.json.jira_project }}-123) and PR numbers as references. If there are status transitions, mention what moved where.\n\nThe lookback period for this report is {{ $('Config').item.json.lookback_days }} day(s).\n\nIf the data shows no commits, no Jira activity, and no open PRs, write a brief note indicating there was no tracked activity on this project since the last update - perhaps it was a planning day, meetings, PTO, or work on a different project.\n\nFormat the output as plain text suitable for Slack, not markdown.\n\nOutput only the standup report itself - no preamble, meta-commentary, or explanations about the data.", "role": "assistant" }, { "content": "=Here is the activity data for the {{ $('Config').item.json.jira_project }} project standup report:\n\n{{ JSON.stringify($json) }}\n\nGenerate the standup report based on this data. If the arrays are empty, that means there was no tracked activity." } ] }, "options": {} }, "type": "@n8n/n8n-nodes-langchain.anthropic", "typeVersion": 1, "position": [1104, 208], "id": "2b79afc3-003f-4c88-8b01-6bfdc652458f", "name": "Summarize Work", "credentials": { "anthropicApi": { "id": "DOM26ikj1QCmBmWU", "name": "Anthropic account" } } }, { "parameters": { "fromEmail": "={{ $('Config').item.json.email }}", "toEmail": "={{ $('Config').item.json.email }}", "subject": "={{ $('Config').item.json.jira_project }} Standup {{ new Date().toLocaleDateString() }}", "emailFormat": "text", "text": "={{ $json.content[0].text }}", "options": {} }, "type": "n8n-nodes-base.emailSend", "typeVersion": 2.1, "position": [1456, 112], "id": "501ac3f1-5c38-4732-91c0-06ae24c63de5", "name": "Send Email", "webhookId": "00ad55ad-7b81-47dc-8dee-be1a4b6f5fb1", "credentials": { "smtp": { "id": "ajtnbj7Y7Db7A7Cx", "name": "SMTP account" } } }, { "parameters": { "operation": "getAll", "calendar": { "__rl": true, "mode": "list", "value": "primary" }, "timeMin": "={{ new Date().toISOString() }}", "timeMax": "={{ new Date(Date.now() + 60 * 60 * 1000).toISOString() }}", "options": { "query": "standup" } }, "type": "n8n-nodes-base.googleCalendar", "typeVersion": 1.3, "position": [1456, 304], "id": "39ff5d90-42d8-4c18-ae3a-cc8571cf22e1", "name": "Get Standup Meeting", "alwaysOutputData": true, "credentials": { "googleCalendarOAuth2Api": { "id": "QdlomyXzKjLxNmRy", "name": "Google Calendar account" } } }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "a8ef4af5-c127-450d-b115-18d1c90ee46d", "leftValue": "={{ $json.id }}", "rightValue": "", "operator": { "type": "string", "operation": "notExists", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [1680, 304], "id": "0e2fb25c-1105-4c9a-8ead-67a34df2c0b4", "name": "If Async" }, { "parameters": { "authentication": "oAuth2", "select": "user", "user": { "__rl": true, "value": "SELECT_YOUR_USER", "mode": "id" }, "text": "={{ $('Summarize Work').item.json.content[0].text }}", "otherOptions": {} }, "type": "n8n-nodes-base.slack", "typeVersion": 2.3, "position": [1680, 496], "id": "6bc80c38-b5a2-460f-82fb-90fd470b416a", "name": "Send to Myself", "webhookId": "32cb8f0b-f380-458b-b2f3-182be8284a29", "credentials": { "slackOAuth2Api": { "id": "KrPfCLkbs3IyUmpr", "name": "Slack account" } } }, { "parameters": { "authentication": "oAuth2", "select": "channel", "channelId": { "__rl": true, "value": "SELECT_YOUR_CHANNEL", "mode": "id" }, "text": "={{ $('Summarize Work').item.json.content[0].text }}", "otherOptions": {} }, "type": "n8n-nodes-base.slack", "typeVersion": 2.3, "position": [1904, 304], "id": "a527f91a-b809-48b5-8077-606eacda3985", "name": "Send to Channel", "webhookId": "d2234e23-db76-498c-883e-eeefd6cb4159", "credentials": { "slackOAuth2Api": { "id": "KrPfCLkbs3IyUmpr", "name": "Slack account" } } }, { "parameters": { "content": "SETUP AFTER IMPORTING\n\n1. Config node - update all values:\n jira_project, jira_domain, jira_in_progress_statuses\n github_owner, github_repo, github_username\n email, lookback_days\n\n2. Schedule Trigger - set your schedule\n\n3. Manual selection required:\n Get Standup Meeting - select your calendar\n Send to Myself - select your Slack user\n Send to Channel - select your channel\n\n4. Reconnect all credentials", "height": 312, "width": 384 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [-640, 176], "id": "444331e5-e85e-41a7-8aad-650e5fa81e2f", "name": "Sticky Note" } ], "pinData": {}, "connections": { "Schedule Trigger": { "main": [ [ { "node": "Config", "type": "main", "index": 0 } ] ] }, "Config": { "main": [ [ { "node": "Get Commits", "type": "main", "index": 0 }, { "node": "Get Open PRs", "type": "main", "index": 0 }, { "node": "Get In Progress Tickets", "type": "main", "index": 0 }, { "node": "Get Recently Updated", "type": "main", "index": 0 }, { "node": "Get My Transitions", "type": "main", "index": 0 }, { "node": "Get My Activity", "type": "main", "index": 0 } ] ] }, "Get Commits": { "main": [ [ { "node": "Get Commit Details", "type": "main", "index": 0 } ] ] }, "Get Commit Details": { "main": [ [ { "node": "Aggregate Commits", "type": "main", "index": 0 } ] ] }, "Aggregate Commits": { "main": [ [ { "node": "Clean Commit Data", "type": "main", "index": 0 } ] ] }, "Clean Commit Data": { "main": [ [ { "node": "Merge All", "type": "main", "index": 0 } ] ] }, "Get Open PRs": { "main": [ [ { "node": "Aggregate PRs", "type": "main", "index": 0 } ] ] }, "Aggregate PRs": { "main": [ [ { "node": "Clean PR Data", "type": "main", "index": 0 } ] ] }, "Clean PR Data": { "main": [ [ { "node": "Merge All", "type": "main", "index": 1 } ] ] }, "Get In Progress Tickets": { "main": [ [ { "node": "Aggregate In Progress", "type": "main", "index": 0 } ] ] }, "Get Recently Updated": { "main": [ [ { "node": "Aggregate Updated", "type": "main", "index": 0 } ] ] }, "Get My Transitions": { "main": [ [ { "node": "Aggregate Transitions", "type": "main", "index": 0 } ] ] }, "Get My Activity": { "main": [ [ { "node": "Aggregate Activity", "type": "main", "index": 0 } ] ] }, "Aggregate In Progress": { "main": [ [ { "node": "Merge Jira Data", "type": "main", "index": 0 } ] ] }, "Aggregate Updated": { "main": [ [ { "node": "Merge Jira Data", "type": "main", "index": 1 } ] ] }, "Aggregate Transitions": { "main": [ [ { "node": "Merge Jira Data", "type": "main", "index": 2 } ] ] }, "Aggregate Activity": { "main": [ [ { "node": "Merge Jira Data", "type": "main", "index": 3 } ] ] }, "Merge Jira Data": { "main": [ [ { "node": "Clean Jira Data", "type": "main", "index": 0 } ] ] }, "Clean Jira Data": { "main": [ [ { "node": "Merge All", "type": "main", "index": 2 } ] ] }, "Merge All": { "main": [ [ { "node": "Summarize Work", "type": "main", "index": 0 } ] ] }, "Summarize Work": { "main": [ [ { "node": "Send Email", "type": "main", "index": 0 }, { "node": "Get Standup Meeting", "type": "main", "index": 0 } ] ] }, "Get Standup Meeting": { "main": [ [ { "node": "If Async", "type": "main", "index": 0 }, { "node": "Send to Myself", "type": "main", "index": 0 } ] ] }, "If Async": { "main": [ [ { "node": "Send to Channel", "type": "main", "index": 0 } ] ] } }, "active": false, "settings": { "executionOrder": "v1" }, "versionId": "c7a5241f-e1eb-42ca-9bc2-f1bc8b232ff6", "meta": { "instanceId": "d2a421903c728da792bc949a91dccee220af5188a35e67ca337aad8f6761dfe1" }, "id": "KhRBskEx76gUbPj4", "tags": [] }