This workflow follows the Google Sheets → HTTP Request 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 →
{
"name": "Sync Asana project tasks to a Google Sheet mirror",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours"
}
]
}
},
"id": "57957001-7a71-415a-b6b9-b8eb670696c3",
"name": "Every Hour",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.3,
"position": [
-32,
96
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c1",
"name": "asana_project_gid",
"value": "REPLACE_WITH_ASANA_PROJECT_GID",
"type": "string"
},
{
"id": "c2",
"name": "sheet_id",
"value": "REPLACE_WITH_GOOGLE_SHEET_ID",
"type": "string"
},
{
"id": "c3",
"name": "sheet_tab",
"value": "Tasks",
"type": "string"
}
]
},
"options": {}
},
"id": "8ebfcd34-5dbe-4718-9e65-4f42c1a85796",
"name": "Set Sync Config",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
192,
96
]
},
{
"parameters": {
"url": "=https://app.asana.com/api/1.0/projects/{{ $json.asana_project_gid }}/tasks",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "asanaApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "opt_fields",
"value": "name,assignee.name,due_on,completed,memberships.section.name,permalink_url,modified_at"
},
{
"name": "limit",
"value": "100"
}
]
},
"options": {
"pagination": {
"pagination": {
"parameters": {
"parameters": [
{
"name": "offset",
"value": "={{ $response.body.next_page?.offset }}"
}
]
},
"paginationCompleteWhen": "other",
"completeExpression": "={{ !$response.body.next_page }}"
}
}
}
},
"id": "e324eb19-1303-46e3-a921-e1f8aa52c48b",
"name": "Fetch Asana Tasks",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
448,
96
],
"credentials": {
"asanaApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Flatten Asana tasks into one flat sheet row each.\n// Handles the HTTP Request response ({ data: [...] } per page) and the Asana\n// node shape (one item per task). Each row is tagged _source='asana' so the\n// Diff node can tell it apart from existing sheet rows after the Merge.\nconst items = $input.all();\nconst tasks = [];\nfor (const item of items) {\n const j = item.json || {};\n if (Array.isArray(j.data)) {\n for (const t of j.data) tasks.push(t);\n } else {\n tasks.push(j);\n }\n}\nconst now = new Date().toISOString();\nreturn tasks\n .filter((t) => t && t.gid)\n .map((t) => {\n let section = '';\n if (Array.isArray(t.memberships)) {\n const m = t.memberships.find((x) => x && x.section && x.section.name);\n if (m) section = m.section.name;\n }\n return {\n json: {\n gid: String(t.gid),\n name: t.name || '',\n assignee: t.assignee && t.assignee.name ? t.assignee.name : '',\n section,\n due_on: t.due_on || '',\n completed: t.completed === true ? 'Yes' : 'No',\n permalink: t.permalink_url || '',\n modified_at: t.modified_at || '',\n synced_at: now,\n present: 'Yes',\n _source: 'asana',\n },\n };\n });"
},
"id": "30414356-d218-473c-bcf4-62121658cff6",
"name": "Flatten Tasks to Rows",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
704,
96
]
},
{
"parameters": {},
"id": "7e084300-95c6-49d2-b0a5-c6adbb850cf7",
"name": "Merge Tasks and Sheet Rows",
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
1008,
112
]
},
{
"parameters": {
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $(\"Set Sync Config\").first().json.sheet_id }}",
"cachedResultName": "Your Spreadsheet"
},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $(\"Set Sync Config\").first().json.sheet_tab }}",
"cachedResultName": "Tasks"
},
"options": {}
},
"id": "5b8d79da-e385-4f4f-b821-1f80946417d8",
"name": "Read Existing Rows in Sheets",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
576,
304
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "// Self-healing diff. Input = Asana rows (_source='asana') plus the existing\n// sheet rows, merged. Emit every Asana task as an upsert (present=Yes) and every\n// sheet row whose GID is gone from Asana as a soft-delete (present=No). Rows are\n// never dropped, only re-flagged. Counts ride on every item for the summary node.\nconst all = $input.all();\nconst now = new Date().toISOString();\nconst asanaRows = all.filter((i) => i.json && i.json._source === 'asana').map((i) => i.json);\nconst sheetRows = all.filter((i) => i.json && i.json._source !== 'asana').map((i) => i.json);\n\nconst asanaByGid = new Map(asanaRows.map((r) => [String(r.gid), r]));\nconst sheetByGid = new Map(sheetRows.filter((r) => r && r.gid).map((r) => [String(r.gid), r]));\n\nconst out = [];\nlet created = 0, updated = 0, removed = 0, alreadyRemoved = 0;\n\nfor (const [gid, row] of asanaByGid) {\n const existed = sheetByGid.has(gid);\n if (existed) updated++; else created++;\n const { _source, ...clean } = row;\n out.push({ json: { ...clean, present: 'Yes', _op: 'upsert', _status: existed ? 'updated' : 'created' } });\n}\n\nfor (const [gid, row] of sheetByGid) {\n if (asanaByGid.has(gid)) continue;\n const wasPresent = String(row.present).toLowerCase() !== 'no';\n if (wasPresent) removed++; else alreadyRemoved++;\n out.push({\n json: {\n gid: String(gid),\n name: row.name || '',\n assignee: row.assignee || '',\n section: row.section || '',\n due_on: row.due_on || '',\n completed: row.completed || '',\n permalink: row.permalink || '',\n modified_at: row.modified_at || '',\n synced_at: now,\n present: 'No',\n _op: 'softdelete',\n _status: wasPresent ? 'removed' : 'already_removed',\n },\n });\n}\n\nconst counts = {\n created, updated, removed, already_removed: alreadyRemoved,\n soft_delete_writes: removed + alreadyRemoved,\n asana_total: asanaByGid.size, sheet_total_before: sheetByGid.size,\n};\nfor (const o of out) o.json._counts = counts;\nreturn out;"
},
"id": "5d75edcf-d7ca-473a-846e-f20184a8e2b9",
"name": "Diff Tasks by GID",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1232,
112
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json._op }}",
"rightValue": "upsert",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "dbc7239e-b5b1-4c51-b313-8c0d2c1c2c98",
"name": "Route Upsert and Soft-Delete",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1456,
112
]
},
{
"parameters": {
"operation": "appendOrUpdate",
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $(\"Set Sync Config\").first().json.sheet_id }}",
"cachedResultName": "Your Spreadsheet"
},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $(\"Set Sync Config\").first().json.sheet_tab }}",
"cachedResultName": "Tasks"
},
"columns": {
"mappingMode": "defineBelow",
"matchingColumns": [
"gid"
],
"value": {
"gid": "={{ $json.gid }}",
"name": "={{ $json.name }}",
"assignee": "={{ $json.assignee }}",
"section": "={{ $json.section }}",
"due_on": "={{ $json.due_on }}",
"completed": "={{ $json.completed }}",
"permalink": "={{ $json.permalink }}",
"modified_at": "={{ $json.modified_at }}",
"synced_at": "={{ $json.synced_at }}",
"present": "={{ $json.present }}"
},
"schema": [
{
"id": "gid",
"displayName": "gid",
"required": false,
"defaultMatch": true,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "name",
"displayName": "name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "assignee",
"displayName": "assignee",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "section",
"displayName": "section",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "due_on",
"displayName": "due_on",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "completed",
"displayName": "completed",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "permalink",
"displayName": "permalink",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "modified_at",
"displayName": "modified_at",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "synced_at",
"displayName": "synced_at",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "present",
"displayName": "present",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
]
},
"options": {}
},
"id": "8ecc2578-c6f7-4470-b2f1-fcdfdc1829fd",
"name": "Upsert Rows in Sheets",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1744,
0
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {},
"id": "042924f9-5dac-4f3e-ae54-aa5aa3489175",
"name": "Merge Write Results",
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
1952,
112
]
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $(\"Set Sync Config\").first().json.sheet_id }}",
"cachedResultName": "Your Spreadsheet"
},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $(\"Set Sync Config\").first().json.sheet_tab }}",
"cachedResultName": "Tasks"
},
"columns": {
"mappingMode": "defineBelow",
"matchingColumns": [
"gid"
],
"value": {
"gid": "={{ $json.gid }}",
"present": "={{ $json.present }}",
"synced_at": "={{ $json.synced_at }}"
},
"schema": [
{
"id": "gid",
"displayName": "gid",
"required": false,
"defaultMatch": true,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "present",
"displayName": "present",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "synced_at",
"displayName": "synced_at",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
]
},
"options": {}
},
"id": "db3fb65d-f05e-488c-b172-aa72be642c96",
"name": "Flag Removed Rows in Sheets",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1744,
240
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// One-line run summary from the counts the Diff node attached to each item.\nconst diff = $('Diff Tasks by GID').all();\nconst c = diff.length\n ? diff[0].json._counts\n : { created: 0, updated: 0, removed: 0, already_removed: 0, soft_delete_writes: 0, asana_total: 0, sheet_total_before: 0 };\nconst summary = 'Mirror synced: ' + c.created + ' created, ' + c.updated + ' updated, ' + c.removed + ' newly removed (present=No). ' + c.asana_total + ' live tasks now in the sheet.';\nreturn [{ json: Object.assign({ summary: summary }, c, { run_at: new Date().toISOString() }) }];"
},
"id": "122588dd-81f0-4628-b97d-8360245c2d64",
"name": "Build Run Summary",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2208,
112
],
"executeOnce": true
},
{
"parameters": {},
"id": "2639bc6d-9849-4a2b-8a27-162985bd1546",
"name": "Finish Sync Run",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
2448,
112
]
},
{
"parameters": {
"content": "## Sync Asana project tasks to a Google Sheet mirror\n\n### How it works\n\n1. On a schedule, the workflow reads every task in one Asana project with its status, assignee, due date and section.\n2. It reads the current sheet, then diffs by task GID: tasks still in Asana are upserted, and rows whose task is gone are flagged `present` = `No`.\n3. The sheet becomes an always-current mirror you can pivot and filter, and no row is ever hard-deleted.\n\n### Setup steps\n\n- [ ] Add an Asana credential (Personal Access Token) and select it on `Fetch Asana Tasks`.\n- [ ] Add your Google Sheets credential and pick your spreadsheet and `Tasks` tab on the three Google Sheets nodes.\n- [ ] In `Set Sync Config`, set `asana_project_gid`, `sheet_id` and `sheet_tab`.\n- [ ] Add a header row to the tab: gid, name, assignee, section, due_on, completed, permalink, modified_at, synced_at, present.\n\n### Customization\n\nChange the schedule interval to sync more or less often. Optionally feed the run-summary counts to a Groq node for a one-line note on what changed; the mirror stays deterministic and the model never touches row data.",
"height": 736,
"width": 584
},
"id": "a6369dec-c912-47ce-9103-f21dbf1d2860",
"name": "Sticky Note 751080a2",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-752,
-224
]
},
{
"parameters": {
"content": "## Configure the sync\n\nSet the schedule, the Asana project GID and the target Sheet ID and tab in one place.",
"height": 360,
"width": 416,
"color": 7
},
"id": "681e7ea1-f9f0-43e8-9988-aaf33e68f18b",
"name": "Sticky Note 0cfb412a",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-80,
-80
]
},
{
"parameters": {
"content": "## Read from Asana and Sheets\n\nPull tasks from Asana with `opt_fields` for the section, flatten them to rows, and read the current sheet.",
"height": 560,
"width": 484,
"color": 7
},
"id": "62a6b5c8-16ee-4ae4-abd3-2ccab91b198a",
"name": "Sticky Note 04fc7ee5",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
384,
-80
]
},
{
"parameters": {
"content": "## Merge and diff by GID\n\nThe self-healing core: upsert tasks still in Asana, flag rows whose task is gone.",
"height": 328,
"width": 704,
"color": 7
},
"id": "84ccdc61-0fb6-4096-9ae5-1496c90e4f0f",
"name": "Sticky Note fdfce91d",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
928,
-32
]
},
{
"parameters": {
"content": "## Write the mirror\n\nUpsert live tasks, set `present` = `No` on removed rows, and converge both branches.",
"height": 624,
"width": 432,
"color": 7
},
"id": "5c3a6918-8f34-4e4a-abb5-c69f847a0c5a",
"name": "Sticky Note c2b1b0e1",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1680,
-192
]
},
{
"parameters": {
"content": "## Summarize the run\n\nLog a one-line count of created, updated and removed, kept for inspection.",
"height": 360,
"width": 496,
"color": 7
},
"id": "62e3b998-2123-4aac-a3ab-2a850965326b",
"name": "Sticky Note 81ad6fd5",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
2160,
-64
]
}
],
"connections": {
"Every Hour": {
"main": [
[
{
"node": "Set Sync Config",
"type": "main",
"index": 0
}
]
]
},
"Set Sync Config": {
"main": [
[
{
"node": "Fetch Asana Tasks",
"type": "main",
"index": 0
},
{
"node": "Read Existing Rows in Sheets",
"type": "main",
"index": 0
}
]
]
},
"Fetch Asana Tasks": {
"main": [
[
{
"node": "Flatten Tasks to Rows",
"type": "main",
"index": 0
}
]
]
},
"Flatten Tasks to Rows": {
"main": [
[
{
"node": "Merge Tasks and Sheet Rows",
"type": "main",
"index": 0
}
]
]
},
"Merge Tasks and Sheet Rows": {
"main": [
[
{
"node": "Diff Tasks by GID",
"type": "main",
"index": 0
}
]
]
},
"Read Existing Rows in Sheets": {
"main": [
[
{
"node": "Merge Tasks and Sheet Rows",
"type": "main",
"index": 1
}
]
]
},
"Diff Tasks by GID": {
"main": [
[
{
"node": "Route Upsert and Soft-Delete",
"type": "main",
"index": 0
}
]
]
},
"Route Upsert and Soft-Delete": {
"main": [
[
{
"node": "Upsert Rows in Sheets",
"type": "main",
"index": 0
}
],
[
{
"node": "Flag Removed Rows in Sheets",
"type": "main",
"index": 0
}
]
]
},
"Upsert Rows in Sheets": {
"main": [
[
{
"node": "Merge Write Results",
"type": "main",
"index": 0
}
]
]
},
"Merge Write Results": {
"main": [
[
{
"node": "Build Run Summary",
"type": "main",
"index": 0
}
]
]
},
"Flag Removed Rows in Sheets": {
"main": [
[
{
"node": "Merge Write Results",
"type": "main",
"index": 1
}
]
]
},
"Build Run Summary": {
"main": [
[
{
"node": "Finish Sync Run",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"tags": []
}
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.
asanaApigoogleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Sync Asana project tasks to a Google Sheet mirror. Uses httpRequest, googleSheets. Scheduled trigger; 19 nodes.
Source: https://github.com/exekyute/n8n-exekyute-templates/blob/main/published/n8n-asana-sheet-mirror/workflow.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.
This workflow monitors customer health by combining payment behavior, complaint signals, and AI-driven feedback analysis. It runs on daily and weekly schedules to evaluate risk levels, escalate high-r
Code Postgres. Uses httpRequest, splitInBatches, postgres, hubspot. Scheduled trigger; 23 nodes.
This workflow runs daily to sync SAP Business One Service Layer OData Business Partners to Google Sheets, handling SAP’s 20-record page limit, mapping fields into a sheet-friendly format, filtering ou
Continuous monitoring: Real-time surveillance of supplier performance, financial health, and operational status Risk scoring: AI-powered assessment of supplier risks across multiple dimensions (financ
Regulatory monitoring: Continuously tracks changes in laws, regulations, and compliance requirements across multiple jurisdictions Contract analysis: AI-powered review of existing contracts to identif