{
  "id": "MkbKiJAGRIrZVoj8",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Automated project tracking with analysing Github commits & Google Tasks",
  "tags": [],
  "nodes": [
    {
      "id": "29455a9a-f6f5-4b1a-80f3-abaea11ddfc4",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        96,
        -1728
      ],
      "parameters": {
        "width": 480,
        "height": 928,
        "content": "## Automated project tracking with analysing Github commits & Google Tasks\n\n### How it works\n\nThe workflow triggers automatically when a new event occurs in GitHub (e.g., a push).\nIt extracts commit details and fetches ongoing tasks from Google Tasks.\nAn AI agent powered by Google Gemini analyzes the commit messages alongside the open tasks.\nThe agent automatically marks tasks as complete in Google Tasks if they correspond to the recent commits.\nA summary of the matched tasks is formatted and sent as a Slack notification.\nFinally, the workflow execution details are logged in a Google Sheet for auditing.\n\n### Setup steps\n\n- [ ] Authenticate the GitHub Trigger with your credentials and select the repository to monitor.\n- [ ] Connect your Google account to authorize access to Google Tasks and Google Sheets.\n- [ ] Provide a Google Gemini API key in the AI agent's chat model node.\n- [ ] Connect your Slack workspace and specify the target channel or user for notifications.\n- [ ] Configure the Google Sheets node with your desired spreadsheet ID and sheet name for logging.\n\n### Customization\n\nYou can adjust the AI Agent instructions to be more strict or lenient when matching commit messages to task descriptions. Additionally, you can customize the Slack message layout to include details like pusher name or specific commit links."
      },
      "typeVersion": 1
    },
    {
      "id": "590c5397-74be-485d-807b-9562086658a1",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        -1696
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 304,
        "content": "## Trigger and extract commits\n\nListens for GitHub events and extracts relevant commit data."
      },
      "typeVersion": 1
    },
    {
      "id": "4f8fd861-1247-4e8b-a4d5-5c4cdc8eccc5",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1088,
        -1696
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 304,
        "content": "## Fetch and prepare tasks\n\nRetrieves tasks from Google Tasks, aggregates them, and formats the context for the AI agent."
      },
      "typeVersion": 1
    },
    {
      "id": "0e547e82-b5d3-4baa-8bdb-d00193e7f722",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1760,
        -1696
      ],
      "parameters": {
        "color": 7,
        "width": 512,
        "height": 512,
        "content": "## AI task completion agent\n\nUses Google Gemini to compare commits with tasks and update completed ones."
      },
      "typeVersion": 1
    },
    {
      "id": "6ddbe3e0-7e28-44f1-a313-7a4ce519b25a",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2304,
        -1696
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 304,
        "content": "## Notify and log results\n\nFormats a summary of the execution, posts it to Slack, and records the run in Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "86ff6790-8323-4bef-ae1c-160dd58fda41",
      "name": "Set Commit Details",
      "type": "n8n-nodes-base.set",
      "position": [
        912,
        -1568
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1a1a1a1-+123456789001",
              "name": "repoName",
              "type": "string",
              "value": "={{ $json.body.repository.full_name }}"
            },
            {
              "id": "a1a1a1a1-+123456789002",
              "name": "pusher",
              "type": "string",
              "value": "={{ $json.body.pusher.name }}"
            },
            {
              "id": "a1a1a1a1-+123456789003",
              "name": "commitCount",
              "type": "number",
              "value": "={{ ($json.body.commits || []).length }}"
            },
            {
              "id": "a1a1a1a1-+123456789004",
              "name": "commitsSummary",
              "type": "string",
              "value": "={{ ($json.body.commits || []).map(c => `- [${c.id.slice(0,7)}] ${c.message}`).join('\\n') || 'No commits in this push (branch delete or empty push).' }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "99263e6b-ef1e-4b9a-bef8-45cf47e42a90",
      "name": "Aggregate Task List",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1360,
        -1568
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "tasks"
      },
      "typeVersion": 1
    },
    {
      "id": "46f9284c-797e-4538-82de-4ad72300fea3",
      "name": "Build AI Agent Context",
      "type": "n8n-nodes-base.set",
      "position": [
        1584,
        -1568
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d4d4d4d4-+123456789001",
              "name": "repoName",
              "type": "string",
              "value": "={{ $('Set Commit Details').item.json.repoName }}"
            },
            {
              "id": "d4d4d4d4-+123456789002",
              "name": "commitCount",
              "type": "number",
              "value": "={{ $('Set Commit Details').item.json.commitCount }}"
            },
            {
              "id": "d4d4d4d4-+123456789003",
              "name": "commitsSummary",
              "type": "string",
              "value": "={{ $('Set Commit Details').item.json.commitsSummary }}"
            },
            {
              "id": "d4d4d4d4-+123456789004",
              "name": "taskCount",
              "type": "number",
              "value": "={{ $json.tasks.length }}"
            },
            {
              "id": "d4d4d4d4-+123456789005",
              "name": "tasksJson",
              "type": "string",
              "value": "={{ JSON.stringify(($json.tasks || []).map(t => ({ id: t.id, taskListId: '@default', title: t.title, notes: t.notes || '', status: t.status }))) }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "3538e130-337e-480b-b4ee-8131a252d401",
      "name": "Task Matcher Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1904,
        -1568
      ],
      "parameters": {
        "text": "=Repository: {{ $json.repoName }}\nPush commit count: {{ $json.commitCount }}\nOpen (incomplete) task count: {{ $json.taskCount }}\n\nCommits in this push:\n{{ $json.commitsSummary }}\n\nOpen Google Tasks (JSON array, each has id, taskListId, title, notes, status):\n{{ $json.tasksJson }}",
        "options": {
          "systemMessage": "You are an engineering task-completion assistant for blankarray. You receive commit messages from a GitHub push and a list of currently open (incomplete) Google Tasks. Your job is to decide which open tasks were completed by these commits, and mark ONLY those tasks complete using the mark_task_complete tool.\n\nMatching rules:\n- A commit references a task if it contains the task's number/id (e.g. '#12', 'GH-12'), OR the commit message clearly and specifically describes the same work as the task title (strong semantic match), OR uses keywords like 'closes', 'fixes', 'resolves', 'done:', 'completed:' tied directly to that task's subject.\n- Matching is case-insensitive; ignore minor wording differences.\n- If several commits reference the same task, call the tool only once for that task.\n- If a possible match is not confident (vague commit like 'fix bug', 'update code', 'wip' with no clear link to a specific task), do NOT call the tool. Add it to 'ambiguous' for human review instead.\n- Never guess. When uncertain, leave the task open.\n- If taskCount is 0, set noTasksFound=true and skip matching entirely.\n- If commitCount is 0, set noCommitsFound=true and skip matching entirely.\n- If a tool call returns success=false, record it under 'errors' with the reason (already completed, not found, permission denied, etc). Do not count it as matched.\n- Always return the full structured JSON exactly matching the schema, using an empty array (never omitted) for any category with nothing in it."
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.7
    },
    {
      "id": "aab5e1c5-a95c-40a1-9e31-4b3e889af45a",
      "name": "Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        1808,
        -1344
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-3.1-flash-lite"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9fe4f00f-501f-4871-a675-0c60c9c50e15",
      "name": "Parse Structured Output",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        2144,
        -1344
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"required\": [\"matched\", \"ambiguous\", \"errors\", \"noTasksFound\", \"noCommitsFound\", \"summary\"],\n  \"properties\": {\n    \"matched\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"taskId\": { \"type\": \"string\" },\n          \"taskTitle\": { \"type\": \"string\" },\n          \"commitSha\": { \"type\": \"string\" },\n          \"reason\": { \"type\": \"string\" }\n        },\n        \"required\": [\"taskId\", \"taskTitle\", \"reason\"]\n      }\n    },\n    \"ambiguous\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"taskId\": { \"type\": \"string\" },\n          \"taskTitle\": { \"type\": \"string\" },\n          \"reason\": { \"type\": \"string\" }\n        },\n        \"required\": [\"taskId\", \"taskTitle\", \"reason\"]\n      }\n    },\n    \"errors\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"taskId\": { \"type\": \"string\" },\n          \"taskTitle\": { \"type\": \"string\" },\n          \"error\": { \"type\": \"string\" }\n        },\n        \"required\": [\"taskId\", \"error\"]\n      }\n    },\n    \"noTasksFound\": { \"type\": \"boolean\" },\n    \"noCommitsFound\": { \"type\": \"boolean\" },\n    \"summary\": { \"type\": \"string\" }\n  }\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "47d65eee-8fee-421b-a11b-74bc3faa5f39",
      "name": "Set Notification Summary",
      "type": "n8n-nodes-base.set",
      "position": [
        2352,
        -1568
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "10101010-+123456789001",
              "name": "repoName",
              "type": "string",
              "value": "={{ $('Build AI Agent Context').item.json.repoName }}"
            },
            {
              "id": "10101010-+123456789002",
              "name": "commitCount",
              "type": "number",
              "value": "={{ $('Build AI Agent Context').item.json.commitCount }}"
            },
            {
              "id": "10101010-+123456789003",
              "name": "taskCount",
              "type": "number",
              "value": "={{ $('Build AI Agent Context').item.json.taskCount }}"
            },
            {
              "id": "10101010-+123456789004",
              "name": "matchedCount",
              "type": "number",
              "value": "={{ $json.output.matched.length }}"
            },
            {
              "id": "10101010-+123456789005",
              "name": "ambiguousCount",
              "type": "number",
              "value": "={{ $json.output.ambiguous.length }}"
            },
            {
              "id": "10101010-+123456789006",
              "name": "errorCount",
              "type": "number",
              "value": "={{ $json.output.errors.length }}"
            },
            {
              "id": "10101010-+123456789007",
              "name": "slackMessage",
              "type": "string",
              "value": "=*Task Auto-Completion \u2014 {{ $json.output.repoName || $('Build AI Agent Context').item.json.repoName }}*\n{{ $json.output.summary }}\n\n\u2705 Completed: {{ $json.output.matched.length }}{{ $json.output.matched.length ? ('\\n' + $json.output.matched.map(m => `  \u2022 ${m.taskTitle}`).join('\\n')) : '' }}\n\ud83d\udfe1 Needs review: {{ $json.output.ambiguous.length }}{{ $json.output.ambiguous.length ? ('\\n' + $json.output.ambiguous.map(a => `  \u2022 ${a.taskTitle} \u2014 ${a.reason}`).join('\\n')) : '' }}\n\u274c Errors: {{ $json.output.errors.length }}{{ $json.output.errors.length ? ('\\n' + $json.output.errors.map(e => `  \u2022 ${e.taskId} \u2014 ${e.error}`).join('\\n')) : '' }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "b82bb2d2-ec64-4b00-8bdc-4188156e66d6",
      "name": "Post Summary to Slack",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        2576,
        -1568
      ],
      "parameters": {
        "text": "={{ $json.slackMessage }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0B8VH1M5PX",
          "cachedResultName": "general"
        },
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b1fed63a-6070-4846-a90a-fc5930efc594",
      "name": "Append Log to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "position": [
        2800,
        -1568
      ],
      "parameters": {
        "columns": {
          "value": {
            "repoName": "={{ $('Set Notification Summary').item.json.repoName }}",
            "taskCount": "={{ $('Set Notification Summary').item.json.taskCount }}",
            "timestamp": "={{$now}}",
            "errorCount": "={{ $('Set Notification Summary').item.json.errorCount }}",
            "commitCount": "={{ $('Set Notification Summary').item.json.commitCount }}",
            "matchedCount": "={{ $('Set Notification Summary').item.json.matchedCount }}",
            "ambiguousCount": "={{ $('Set Notification Summary').item.json.ambiguousCount }}"
          },
          "schema": [
            {
              "id": "timestamp",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "repoName",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "repoName",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "commitCount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "commitCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "taskCount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "taskCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "matchedCount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "matchedCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ambiguousCount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ambiguousCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "errorCount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "errorCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1CAjfwJm4yT5VMh9GFQGZit9bomloq0aRyEPAeAHoCpE/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1CAjfwJm4yT5VMh9GFQGZit9bomloq0aRyEPAeAHoCpE",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1CAjfwJm4yT5VMh9GFQGZit9bomloq0aRyEPAeAHoCpE/edit?usp=drivesdk",
          "cachedResultName": "google tasks Log"
        },
        "authentication": "serviceAccount"
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "387b9b21-4a9d-4848-a5d4-4b383afc70cb",
      "name": "Fetch Google Tasks",
      "type": "n8n-nodes-base.googleTasks",
      "position": [
        1136,
        -1568
      ],
      "parameters": {
        "task": "MTA3MDkwMTQyNDY0NzUyNjMzMTQ6MDow",
        "operation": "getAll",
        "returnAll": true,
        "additionalFields": {
          "showCompleted": false
        }
      },
      "credentials": {
        "googleTasksOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c0a14847-6261-473f-ba86-99c1527b8759",
      "name": "Update Task Tool",
      "type": "n8n-nodes-base.googleTasksTool",
      "position": [
        1984,
        -1344
      ],
      "parameters": {
        "task": "MTA3MDkwMTQyNDY0NzUyNjMzMTQ6MDow",
        "taskId": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Task_ID', ``, 'string') }}",
        "operation": "update",
        "updateFields": {
          "status": "completed"
        }
      },
      "credentials": {
        "googleTasksOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9e9d2280-c230-41b1-b977-1c3575006e3d",
      "name": "When Github Push Received",
      "type": "n8n-nodes-base.githubTrigger",
      "position": [
        688,
        -1568
      ],
      "parameters": {
        "owner": {
          "__rl": true,
          "mode": "list",
          "value": "iamvaar-dev",
          "cachedResultName": "iamvaar-dev"
        },
        "events": [
          "push"
        ],
        "options": {},
        "repository": {
          "__rl": true,
          "mode": "list",
          "value": "gohspy",
          "cachedResultName": "gohspy"
        }
      },
      "credentials": {
        "githubApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "97cfdc5d-7e16-4377-9c36-0423754e7f66",
  "nodeGroups": [],
  "connections": {
    "Update Task Tool": {
      "ai_tool": [
        [
          {
            "node": "Task Matcher Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Task Matcher Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Google Tasks": {
      "main": [
        [
          {
            "node": "Aggregate Task List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Commit Details": {
      "main": [
        [
          {
            "node": "Fetch Google Tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Task Matcher Agent": {
      "main": [
        [
          {
            "node": "Set Notification Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Task List": {
      "main": [
        [
          {
            "node": "Build AI Agent Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Summary to Slack": {
      "main": [
        [
          {
            "node": "Append Log to Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build AI Agent Context": {
      "main": [
        [
          {
            "node": "Task Matcher Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Structured Output": {
      "ai_outputParser": [
        [
          {
            "node": "Task Matcher Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Set Notification Summary": {
      "main": [
        [
          {
            "node": "Post Summary to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Github Push Received": {
      "main": [
        [
          {
            "node": "Set Commit Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}