This workflow corresponds to n8n.io template #12579 — we link there as the canonical source.
This workflow follows the Google Sheets → OpenAI recipe pattern — see all workflows that pair these two integrations.
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 →
{
"id": "",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Employee Performance AI Dashboard",
"tags": [],
"nodes": [
{
"id": "c868124d-d47d-4cac-b7f6-227e5eec08eb",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-2720,
496
],
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"typeVersion": 1.3
},
{
"id": "d19f2e70-81eb-4eae-947a-3364cd8a234c",
"name": "Get many folders",
"type": "n8n-nodes-base.clickUp",
"position": [
-2496,
496
],
"parameters": {
"team": "YOUR_TEAM_ID",
"limit": 100,
"space": "YOUR_SPACE_ID",
"filters": {},
"resource": "folder",
"operation": "getAll"
},
"credentials": {
"clickUpApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "5b6aeedf-b801-47dd-9465-546221d7de7c",
"name": "Get many tasks",
"type": "n8n-nodes-base.clickUp",
"position": [
-2016,
512
],
"parameters": {
"list": "={{ $json.lists[0].id }}",
"team": "YOUR_TEAM_ID",
"space": "YOUR_SPACE_ID",
"filters": {},
"operation": "getAll",
"folderless": true
},
"credentials": {
"clickUpApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "166ad36b-ceca-4cd2-9602-4e1c8e6f6aca",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-2272,
496
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "908cebd1-eadd-4cc7-a806-b5ded51ee1b4",
"name": "Message a model1",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-1808,
512
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini",
"cachedResultName": "GPT-4.1-MINI"
},
"options": {},
"responses": {
"values": [
{
"content": "=You are a project management analytics assistant.\n\nGenerate a concise, professional task summary using ONLY the data provided from ClickUp. \nDo not invent data or make assumptions. Base all calculations strictly on the task data.\n\nTask Data:\nTask ID: {{$json.id}}\nName: {{$json.name}}\nDescription: {{$json.description}}\nStatus: {{$json.status.status}}\nPriority: {{ $json.priority }}\nAssignees: {{$json.assignees.map(a => a.username).join(', ')}}\nCreator: {{$json.creator.username}}\nDue Date: {{$json.due_date}}\nDate Created: {{$json.date_created}}\nDate Updated: {{$json.date_updated}}\nURL: {{$json.url}}\nProject: {{$json.project.name}}\nList: {{$json.list.name}}\nFolder: {{$json.folder.name}}\nSpace ID: {{$json.space.id}}\n\nMetrics (calculate based on task status and priority):\n- Completed: {{$json.status.status === 'done' ? 1 : 0}}\n- In Progress: {{$json.status.status === 'in progress' ? 1 : 0}}\n- Pending: {{$json.status.status === 'to do' || $json.status.status === 'pending' ? 1 : 0}}\n- Overdue: {{($json.due_date && Number($json.due_date) < Date.now() && $json.status.status !== 'done') ? 1 : 0}}\n- Productivity: {{($json.status.status === 'done' ? 100 : 0)}}%\n- KPI Score: [calculate based on completed tasks, priority, and overdue metrics]\n- Rating: [High priority & overdue \u2192 Critical, Medium priority \u2192 Needs Improvement, Low \u2192 Good]\n\nOutput plain text ONLY, in this format:\n\nTask Summary\nTask ID: <id>\nName: <name>\nProject: <project>\nFolder: <folder>\nList: <list>\nSpace ID: <space_id>\n\nOverview:\n- Description: <description>\n- Status: <status>\n- Priority: <priority>\n- Assignees: <assignees>\n- Creator: <creator>\n- Dates:\n \u2022 Due: <due_date>\n \u2022 Created: <date_created>\n \u2022 Updated: <date_updated>\n\nMetrics:\n- Completed: <number>\n- In Progress: <number>\n- Pending: <number>\n- Overdue: <number>\n- Productivity: <percentage>%\n- KPI Score: <score>\n- Rating: <rating>\n\nTask Link: <url>\n\nInsight:\nProvide a one-line performance summary of this task's status and priority, explaining the current KPI score, rating, and any action required.\n"
}
]
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "016ebf61-710c-49c1-b97c-ddaa5ba20fc0",
"name": "Code in JavaScript",
"type": "n8n-nodes-base.code",
"position": [
-1472,
512
],
"parameters": {
"jsCode": "// Get all items from previous node\nconst items = $input.all();\n\n// Helper function to calculate KPI score\nfunction calculateKPI(task) {\n // Base KPI: 100 if done, 50 if in progress, 0 if pending\n let base = 0;\n if (task.overview.status.toLowerCase() === 'done') base = 100;\n else if (task.overview.status.toLowerCase() === 'in progress') base = 60;\n else base = 30;\n\n // Adjust based on priority\n const priority = task.overview.priority.toLowerCase();\n if (priority === 'high') base += 10;\n else if (priority === 'low') base -= 10;\n\n // Reduce if overdue\n if (task.metrics.overdue > 0) base -= 20;\n\n // Clamp 0-100\n return Math.max(0, Math.min(100, base));\n}\n\n// Helper to get Rating based on KPI Score\nfunction calculateRating(kpiScore) {\n if (kpiScore >= 80) return 'Excellent';\n if (kpiScore >= 60) return 'Needs Improvement';\n if (kpiScore >= 40) return 'Average';\n return 'Poor';\n}\n\n// Initialize result array\nconst parsedTasks = [];\n\nitems.forEach(item => {\n // Safely access the ChatGPT text\n const content = item.json.output?.[0]?.content?.[0]?.text;\n if (!content) return; // skip if no text\n\n const lines = content.split('\\n').map(l => l.trim()).filter(l => l);\n\n // Initialize task object\n let taskData = {\n taskSummary: '',\n taskId: '',\n name: '',\n project: '',\n folder: '',\n list: '',\n spaceId: '',\n overview: {\n description: '',\n status: '',\n priority: '',\n assignees: [],\n creator: '',\n dates: {\n due: null,\n created: null,\n updated: null\n }\n },\n metrics: {\n completed: 0,\n inProgress: 0,\n pending: 0,\n overdue: 0,\n productivity: '',\n kpiScore: 0,\n rating: '',\n },\n taskLink: '',\n insight: ''\n };\n\n let currentSection = '';\n\n function parseNumber(str) { const n = Number(str); return isNaN(n)?0:n; }\n\n // Parse each line\n lines.forEach(line => {\n if (line.startsWith('Task Summary')) taskData.taskSummary = 'Task Summary';\n else if (line.startsWith('Task ID:')) taskData.taskId = line.replace('Task ID:','').trim();\n else if (line.startsWith('Name:')) taskData.name = line.replace('Name:','').trim();\n else if (line.startsWith('Project:')) taskData.project = line.replace('Project:','').trim();\n else if (line.startsWith('Folder:')) taskData.folder = line.replace('Folder:','').trim();\n else if (line.startsWith('List:')) taskData.list = line.replace('List:','').trim();\n else if (line.startsWith('Space ID:')) taskData.spaceId = line.replace('Space ID:','').trim();\n else if (line.startsWith('Overview:')) currentSection = 'overview';\n else if (line.startsWith('Metrics:')) currentSection = 'metrics';\n else if (line.startsWith('Task Link:')) taskData.taskLink = line.replace('Task Link:','').trim();\n else if (line.startsWith('Insight:')) currentSection = 'insight';\n else {\n if (currentSection === 'overview') {\n if (line.startsWith('- Description:')) taskData.overview.description = line.replace('- Description:','').trim();\n else if (line.startsWith('- Status:')) taskData.overview.status = line.replace('- Status:','').trim();\n else if (line.startsWith('- Priority:')) taskData.overview.priority = line.replace('- Priority:','').trim();\n else if (line.startsWith('- Assignees:')) {\n const assigneesText = line.replace('- Assignees:','').trim();\n taskData.overview.assignees = assigneesText ? assigneesText.split(',').map(a=>a.trim()) : [];\n }\n else if (line.startsWith('- Creator:')) taskData.overview.creator = line.replace('- Creator:','').trim();\n else if (line.startsWith('\u2022 Due:')) taskData.overview.dates.due = line.replace('\u2022 Due:','').trim() || null;\n else if (line.startsWith('\u2022 Created:')) taskData.overview.dates.created = Number(line.replace('\u2022 Created:','').trim()) || null;\n else if (line.startsWith('\u2022 Updated:')) taskData.overview.dates.updated = Number(line.replace('\u2022 Updated:','').trim()) || null;\n } else if (currentSection === 'metrics') {\n if (line.startsWith('- Completed:')) taskData.metrics.completed = parseNumber(line.replace('- Completed:','').trim());\n else if (line.startsWith('- In Progress:')) taskData.metrics.inProgress = parseNumber(line.replace('- In Progress:','').trim());\n else if (line.startsWith('- Pending:')) taskData.metrics.pending = parseNumber(line.replace('- Pending:','').trim());\n else if (line.startsWith('- Overdue:')) taskData.metrics.overdue = parseNumber(line.replace('- Overdue:','').trim());\n else if (line.startsWith('- Productivity:')) taskData.metrics.productivity = line.replace('- Productivity:','').trim();\n // KPI and Rating will be recalculated\n } else if (currentSection === 'insight') {\n taskData.insight += (taskData.insight ? ' ' : '') + line;\n }\n }\n });\n\n // Calculate KPI Score and Rating based on parsed metrics\n taskData.metrics.kpiScore = calculateKPI(taskData);\n taskData.metrics.rating = calculateRating(taskData.metrics.kpiScore);\n\n // Optional: Generate summary insight automatically if not present\n if (!taskData.insight) {\n taskData.insight = `Task has status \"${taskData.overview.status}\" with priority \"${taskData.overview.priority}\" and KPI ${taskData.metrics.kpiScore}, rating ${taskData.metrics.rating}.`;\n }\n\n parsedTasks.push({ json: taskData });\n});\n\nreturn parsedTasks;\n"
},
"typeVersion": 2
},
{
"id": "174d1b30-281b-424f-b70a-09f156e6fde4",
"name": "Append or update row in sheet1",
"type": "n8n-nodes-base.googleSheets",
"position": [
-1264,
512
],
"parameters": {
"columns": {
"value": {
"Folder": "={{ $json.folder }}",
"Rating": "={{ $json.metrics.rating }}",
"Status": "={{ $json.overview.status }}",
"Overdue": "={{ $json.metrics.overdue }}",
"Pending": "={{ $json.metrics.pending }}",
"Project": "={{ $json.project }}",
"Summary": "={{ $json.insight }}",
"Priority": "={{ $json.overview.priority }}",
"Completed": "={{ $json.metrics.completed }}",
"KPI Score": "={{ $json.metrics.kpiScore }}",
"Task Link": "={{ $json.taskLink }}",
"Task Name": "={{ $json.name }}",
"In Progress": "={{ $json.metrics.inProgress }}",
"Productivity": "={{ $json.metrics.productivity }}",
"Employee Name": "={{ $json.overview.creator }}"
},
"schema": [
{
"id": "Employee Name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Employee Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Task Name",
"type": "string",
"display": true,
"required": false,
"displayName": "Task Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Project",
"type": "string",
"display": true,
"required": false,
"displayName": "Project",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Folder",
"type": "string",
"display": true,
"required": false,
"displayName": "Folder",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Priority",
"type": "string",
"display": true,
"required": false,
"displayName": "Priority",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Completed",
"type": "string",
"display": true,
"required": false,
"displayName": "Completed",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "In Progress",
"type": "string",
"display": true,
"required": false,
"displayName": "In Progress",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Pending",
"type": "string",
"display": true,
"required": false,
"displayName": "Pending",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Overdue",
"type": "string",
"display": true,
"required": false,
"displayName": "Overdue",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Productivity",
"type": "string",
"display": true,
"required": false,
"displayName": "Productivity",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "KPI Score",
"type": "string",
"display": true,
"required": false,
"displayName": "KPI Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Rating",
"type": "string",
"display": true,
"required": false,
"displayName": "Rating",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Summary",
"type": "string",
"display": true,
"required": false,
"displayName": "Summary",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Task Link",
"type": "string",
"display": true,
"required": false,
"displayName": "Task Link",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Employee Name"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "YOUR_SHEET_GID",
"cachedResultName": "Employee summary"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID",
"cachedResultName": "Employee summary"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "c130ffc3-1018-46a0-b601-fe2f05ec0c34",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3440,
288
],
"parameters": {
"width": 592,
"height": 672,
"content": "## ClickUp Task Performance \u2192 Employee KPI Sheet\n\n### How it works\nThis workflow runs on a schedule and pulls tasks from ClickUp. Each task is processed individually and sent to an AI model to generate a structured performance summary. A JavaScript step normalizes the data and recalculates KPI scores and ratings. The final task performance data is then appended or updated in a Google Sheet to maintain a live employee KPI dashboard.\n\n### Setup steps\n1. Connect your ClickUp account and select the correct Team and Space IDs \n2. Ensure task statuses and priorities are standardized in ClickUp \n3. Connect Google Sheets and select the target spreadsheet and sheet \n4. Confirm column names match the workflow mappings \n5. Configure the schedule trigger based on reporting frequency\n\n### Customization tips\n- Adjust KPI scoring logic inside the JavaScript node \n- Change Google Sheets matching logic to track per task or per employee \n- Extend the AI prompt to add more insights if needed\n"
},
"typeVersion": 1
},
{
"id": "0e72a237-d0f9-439d-8c7b-ceaf2a3ff755",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2800,
288
],
"parameters": {
"color": 7,
"width": 912,
"height": 672,
"content": "## Step 1: ClickUp Data Collection\nFetches folders and tasks from the selected ClickUp space.\nLoops through each task so they can be analyzed individually.\n"
},
"typeVersion": 1
},
{
"id": "f035c693-de1d-48ff-9247-f3bfb83cdea8",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1856,
288
],
"parameters": {
"color": 7,
"width": 528,
"height": 672,
"content": "## Step 2: Task Analysis & KPI Processing\nGenerates structured task summaries using AI.\nParses output and recalculates KPI scores and ratings using JavaScript.\n"
},
"typeVersion": 1
},
{
"id": "be504b93-cc5f-4614-8a26-37cc3f1423b0",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1296,
288
],
"parameters": {
"color": 7,
"width": 528,
"height": 672,
"content": "## Step 3: KPI Reporting to Sheet\nWrites task-level performance data into Google Sheets.\nCreates a live employee KPI and productivity report.\n"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "",
"connections": {
"Get many tasks": {
"main": [
[
{
"node": "Message a model1",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[],
[
{
"node": "Get many tasks",
"type": "main",
"index": 0
}
]
]
},
"Get many folders": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Message a model1": {
"main": [
[
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Get many folders",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "Append or update row in sheet1",
"type": "main",
"index": 0
}
]
]
},
"Append or update row in sheet1": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
clickUpApigoogleSheetsOAuth2ApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs on a schedule to collect task data from ClickUp and evaluate employee performance using AI. Tasks are analyzed to generate structured summaries, productivity metrics, and KPI scores. JavaScript logic refines and standardizes the results. The final performance…
Source: https://n8n.io/workflows/12579/ — 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.
The workflow runs automatically every day and collects analytics data for both today and yesterday. It cleans and standardizes both datasets in the same way so they are easy to compare. After that, it
AI Institutional Stock Valuation Engine with Risk Scoring & Scenario Targets
Overview This is a production-grade, fully automated stock analysis system built entirely in n8n. It combines institutional-level financial analysis, dual AI model consensus, and a self-improving back
This workflow enables the automatic and regular tracking of competitors' Instagram Reels, providing rich insights for each video (summary, topic, hook, angles, tags, etc) through ChatGPT, and storing
Personalized Outreach & Follow-Up - Phase 2. Uses googleSheets, openAi, gmail, gmailTrigger. Scheduled trigger; 59 nodes.