AutomationFlowsGeneral › Sync Asana Task Due Dates to Google Calendar

Sync Asana Task Due Dates to Google Calendar

Sync Asana task due dates to Google Calendar. Uses asana, googleCalendar. Scheduled trigger; 17 nodes.

Cron / scheduled trigger★★★★☆ complexity17 nodesAsanaGoogle Calendar
General Trigger: Cron / scheduled Nodes: 17 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "name": "Sync Asana task due dates to Google Calendar",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "id": "19d87d38-fd11-466f-8c4d-9f3608811bca",
      "name": "Every 15 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        128
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "asanaProjectGid",
              "value": "REPLACE_WITH_YOUR_ASANA_PROJECT_GID",
              "type": "string"
            },
            {
              "id": "a2",
              "name": "calendarId",
              "value": "REPLACE_WITH_YOUR_CALENDAR_ID",
              "type": "string"
            },
            {
              "id": "a3",
              "name": "timedEventMinutes",
              "value": 30,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "9a8281a5-ad63-4a8f-b8d2-f5de8af5e4d0",
      "name": "Set Sync Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        224,
        128
      ]
    },
    {
      "parameters": {
        "operation": "getAll",
        "returnAll": true,
        "filters": {
          "opt_fields": [
            "gid",
            "name",
            "due_on",
            "due_at",
            "completed",
            "permalink_url",
            "assignee.name"
          ],
          "project": "={{ $('Set Sync Config').first().json.asanaProjectGid }}"
        }
      },
      "id": "2f5befff-da59-4ee9-b700-3c8419d911eb",
      "name": "Get Asana Tasks",
      "type": "n8n-nodes-base.asana",
      "typeVersion": 1,
      "position": [
        80,
        544
      ],
      "alwaysOutputData": true,
      "credentials": {
        "asanaApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "getAll",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Sync Config').first().json.calendarId }}"
        },
        "returnAll": true,
        "timeMin": "={{ $now.minus({ years: 5 }) }}",
        "timeMax": "={{ $now.plus({ years: 5 }) }}",
        "options": {
          "query": "asana_gid:"
        }
      },
      "id": "fda8d57b-bc99-42f2-94a3-997e5598983b",
      "name": "Get Synced Events",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        304,
        544
      ],
      "executeOnce": true,
      "alwaysOutputData": true,
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Reconcile Asana tasks against the calendar events this workflow created.\n// Idempotency key: the \"asana_gid:<GID>\" token stored in each event description.\nconst cfg = $('Set Sync Config').first().json;\nconst calendarId = cfg.calendarId;\nconst durationMin = Number(cfg.timedEventMinutes) || 30;\n\nconst tasks = $('Get Asana Tasks').all().map((i) => i.json);\nconst events = $input.all().map((i) => i.json);\n\nconst TOKEN = 'asana_gid:';\nconst eventByGid = {};\nfor (const ev of events) {\n  const desc = String(ev.description || '');\n  const m = desc.match(/asana_gid:(\\d+)/);\n  if (m) eventByGid[m[1]] = ev;\n}\n\nconst dateOnly = (iso) => iso.slice(0, 10);\nconst out = [];\nconst seen = new Set();\n\nfor (const t of tasks) {\n  const gid = t.gid == null ? '' : String(t.gid);\n  if (!gid) continue;\n  seen.add(gid);\n\n  const hasDue = Boolean(t.due_on || t.due_at);\n  const isOpen = t.completed !== true;\n  const existing = eventByGid[gid];\n\n  if (isOpen && hasDue) {\n    const timed = Boolean(t.due_at);\n    let start;\n    let end;\n    let allday;\n    if (timed) {\n      allday = 'no';\n      start = t.due_at;\n      end = new Date(new Date(t.due_at).getTime() + durationMin * 60000).toISOString();\n    } else {\n      allday = 'yes';\n      start = t.due_on;\n      end = dateOnly(new Date(new Date(t.due_on + 'T00:00:00Z').getTime() + 86400000).toISOString());\n    }\n    const assignee = t.assignee && t.assignee.name ? t.assignee.name : 'Unassigned';\n    const link = t.permalink_url ? 'Asana task: ' + t.permalink_url : '';\n    const description = [\n      link,\n      'Assignee: ' + assignee,\n      '',\n      TOKEN + gid,\n      'Synced from Asana by n8n. Keep the line above so updates match this event.',\n    ].filter((l) => l !== '').join('\\n');\n\n    out.push({\n      json: {\n        action: existing ? 'update' : 'create',\n        eventId: existing ? existing.id || '' : '',\n        calendarId,\n        summary: t.name || 'Untitled task',\n        description,\n        start,\n        end,\n        allday,\n        gid,\n      },\n    });\n  } else if (existing) {\n    out.push({\n      json: { action: 'delete', eventId: existing.id || '', calendarId, gid, summary: t.name || '' },\n    });\n  }\n}\n\nfor (const gid of Object.keys(eventByGid)) {\n  if (!seen.has(gid)) {\n    out.push({\n      json: { action: 'delete', eventId: eventByGid[gid].id || '', calendarId, gid, summary: '' },\n    });\n  }\n}\n\nreturn out;"
      },
      "id": "62243335-0dbd-41eb-8fae-035d3d38b029",
      "name": "Reconcile Tasks and Events",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        592,
        544
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "create"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "create"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "update"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "update"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "delete"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "delete"
            }
          ]
        },
        "options": {}
      },
      "id": "2697aabf-cb8d-4c70-93d8-e06f5a5bf1fc",
      "name": "Route by Action",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        848,
        528
      ]
    },
    {
      "parameters": {
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.calendarId }}"
        },
        "start": "={{ $json.start }}",
        "end": "={{ $json.end }}",
        "additionalFields": {
          "allday": "={{ $json.allday }}",
          "description": "={{ $json.description }}",
          "summary": "={{ $json.summary }}"
        }
      },
      "id": "612af32b-33ef-4757-9c0b-d98cba781136",
      "name": "Create Calendar Event",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        1280,
        336
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Count what the reconcile step decided this run.\nconst actions = $('Reconcile Tasks and Events').all().map((i) => i.json.action);\nconst summary = {\n  created: actions.filter((a) => a === 'create').length,\n  updated: actions.filter((a) => a === 'update').length,\n  deleted: actions.filter((a) => a === 'delete').length,\n  total: actions.length,\n  ranAt: $now.toISO(),\n};\nreturn [{ json: summary }];"
      },
      "id": "8c65c933-fcf2-4043-b655-cf07e95d5d02",
      "name": "Build Run Summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        608,
        976
      ],
      "executeOnce": true
    },
    {
      "parameters": {},
      "id": "b52c0edb-a0a5-48e2-8e6a-e16eb7a587ed",
      "name": "Done",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        816,
        976
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.calendarId }}"
        },
        "eventId": "={{ $json.eventId }}",
        "updateFields": {
          "allday": "={{ $json.allday }}",
          "description": "={{ $json.description }}",
          "end": "={{ $json.end }}",
          "start": "={{ $json.start }}",
          "summary": "={{ $json.summary }}"
        }
      },
      "id": "005be5f0-9b2e-43d5-8da2-57f1e01f4f7a",
      "name": "Update Calendar Event",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        1280,
        544
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "delete",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.calendarId }}"
        },
        "eventId": "={{ $json.eventId }}",
        "options": {}
      },
      "id": "880fa3d0-7c90-4a08-8675-0dc316a3ef41",
      "name": "Delete Calendar Event",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        1280,
        768
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "content": "## Sync Asana task due dates to Google Calendar\n\n### How it works\n\n1. A schedule trigger runs every 15 minutes and reads the target project and calendar from one config node.\n2. It pulls every Asana task in the project with its due date, completion state, and assignee.\n3. It lists the calendar events this workflow already created, matched by the `asana_gid` token in each event.\n4. A code node compares the two sets and decides, per task, whether to create, update, or delete an event.\n5. A switch routes each task to the matching Google Calendar action, so a task never gets a duplicate event.\n\n### Setup steps\n\n- [ ] Add an Asana Personal Access Token credential and select it on the `Get Asana Tasks` node.\n- [ ] Add a Google Calendar credential and select it on the three calendar nodes.\n- [ ] Open `Set Sync Config` and set your Asana project GID and target calendar ID.\n- [ ] Run the workflow once to backfill, then activate it.\n\n### Customization\n\nChange the schedule interval, point it at a different project or calendar, or edit the event title and description in the `Reconcile Tasks and Events` node.",
        "height": 764,
        "width": 528
      },
      "id": "3c73dd2c-6a57-41b8-b99c-baf066371ec7",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -688,
        144
      ]
    },
    {
      "parameters": {
        "content": "## Schedule and configuration\n\nRuns on a timer and reads the one project and one calendar to sync.",
        "height": 368,
        "width": 444,
        "color": 7
      },
      "id": "6abdfa45-542b-466f-ba25-a9f5bf0f4b11",
      "name": "Section Config",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -48,
        -48
      ]
    },
    {
      "parameters": {
        "content": "## Read tasks and calendar events\n\nPulls Asana tasks with due dates and the events this workflow already created.",
        "height": 368,
        "width": 444,
        "color": 7
      },
      "id": "db119cfc-3109-4b0a-9d25-b959f5e45d49",
      "name": "Section Read",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        16,
        368
      ]
    },
    {
      "parameters": {
        "content": "## Match events by task GID\n\nThe `asana_gid` token in each event is the idempotency key. The code node diffs tasks against events.",
        "height": 368,
        "width": 492,
        "color": 7
      },
      "id": "03fa4b18-855b-4c91-8a61-b446856014d7",
      "name": "Section Match",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        544,
        368
      ]
    },
    {
      "parameters": {
        "content": "## Create, update, or delete events\n\nCreates an event for a new task, updates the existing one, or deletes it when the task is done or loses its date.",
        "height": 788,
        "width": 456,
        "color": 7
      },
      "id": "ffd912b5-c0b2-420f-8e18-6c07c845d608",
      "name": "Section Write",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1104,
        160
      ]
    },
    {
      "parameters": {
        "content": "## Summarize the run\n\nCounts how many events were created, updated, and deleted this run.",
        "height": 312,
        "width": 476,
        "color": 7
      },
      "id": "99281985-0fde-48cd-9f24-a74cec67b093",
      "name": "Section Summary",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        544,
        816
      ]
    }
  ],
  "connections": {
    "Every 15 Minutes": {
      "main": [
        [
          {
            "node": "Set Sync Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Sync Config": {
      "main": [
        [
          {
            "node": "Get Asana Tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Asana Tasks": {
      "main": [
        [
          {
            "node": "Get Synced Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Synced Events": {
      "main": [
        [
          {
            "node": "Reconcile Tasks and Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reconcile Tasks and Events": {
      "main": [
        [
          {
            "node": "Route by Action",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Run Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Action": {
      "main": [
        [
          {
            "node": "Create Calendar Event",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Update Calendar Event",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Delete Calendar Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Run Summary": {
      "main": [
        [
          {
            "node": "Done",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Sync Asana task due dates to Google Calendar. Uses asana, googleCalendar. Scheduled trigger; 17 nodes.

Source: https://github.com/exekyute/n8n-exekyute-templates/blob/main/published/n8n-asana-calendar-sync/workflow.json — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

Use this workflow to book, cancel, or reschedule appointments using Vapi and Google Calendar

Google Calendar
General

backup. Uses googleDrive, httpRequest. Scheduled trigger; 15 nodes.

Google Drive, HTTP Request
General

Smart google indexing: sitemap filter and url inspection. Uses httpRequest, xml, splitOut, scheduleTrigger. Scheduled trigger; 13 nodes.

HTTP Request, XML
General

Monitor Google AI Overview visibility. Uses @local-falcon/n8n-nodes-localfalcon. Scheduled trigger; 12 nodes.

@Local Falcon/N8N Nodes Localfalcon
General

Monitor competitor rankings with Local Falcon. Uses @local-falcon/n8n-nodes-localfalcon. Scheduled trigger; 11 nodes.

@Local Falcon/N8N Nodes Localfalcon