Realtime Notion Todoist 2-way Sync Template
Configured with Aggregate, Compare Datasets, Crypto, Execute Workflow, and additional nodes. Review the connections and setup before adapting this template.
Node categories in this workflow
- Web / API
- Integration
- Code
- Routing
- Transform
- Trigger
- Data store
- Messaging
- Subworkflow
- Wait
Icons describe general node roles, inferred from node types. Inspect the parameters for the configured action.
Drag to pan; use + and − to zoom. Reconstructed from the exported node positions; nothing runs here. Dashed lines represent non-main connections. Full node names and notes appear below.
How it works
The exported workflow contains the following configured nodes. Their connections are shown above; inspect the JSON for the complete logic, expressions, and external actions.
| Node | Category | Type | Operation |
|---|---|---|---|
| Get projects | Web / API | HTTP Request | See node parameters |
| Get sections | Web / API | HTTP Request | See node parameters |
| Get Notion Databases | Integration | Notion | database / getAll |
| Prep Dropdown | Code | Code | See node parameters |
| Prep Dropdown1 | Code | Code | See node parameters |
| Generate config | Code | Code | See node parameters |
| Choose Notion Database | Integration | Form | See node parameters |
| Choose Todoist Project | Integration | Form | See node parameters |
| Verify security token | Routing | If | See node parameters |
| Generate security token | Transform | Crypto | See node parameters |
| Store variables | Code | Code | See node parameters |
| Get variables | Code | Code | See node parameters |
| Redirect to Auth Page | Integration | Form | completion |
| OAuth redirect | Trigger | Webhook | See node parameters |
| Exchange Tokens | Web / API | HTTP Request | POST |
| Respond with success | Web / API | Respond To Webhook | See node parameters |
| Respond with error | Web / API | Respond To Webhook | See node parameters |
| Get Notion Database ID | Code | Code | See node parameters |
| Get Todoist Project ID | Code | Code | See node parameters |
| Notion-Todoist Sync Setup Helper | Trigger | Form Trigger | See node parameters |
| Todoist Webhook Setup Helper | Trigger | Form Trigger | See node parameters |
| Return config JSON | Integration | Form | completion |
| Todoist | Integration | Todoist | getAll |
| Notion | Integration | Notion | databasePage / getAll |
| Compare Datasets | Transform | Compare Datasets | See node parameters |
| Exists/Completed in Notion | Routing | If | See node parameters |
| Get Todoist Task | Integration | Todoist | get |
| If Todoist ID exists | Routing | If | See node parameters |
| Store Todoist ID | Integration | Notion | databasePage / update |
| Mark as Incomplete in Todoist | Integration | Todoist | reopen |
| Mark as Completed in Todoist | Integration | Todoist | close |
| Delete Task in Todoist | Integration | Todoist | delete |
| Loop Over Items | Routing | Loop Over Items | See node parameters |
| Get Notion Task | Integration | Notion | databasePage / getAll |
| Update Task in Todoist | Web / API | HTTP Request | POST |
| Pick Todoist Fields | Transform | Edit Fields | See node parameters |
| Map Notion to Todoist | Code | Code | See node parameters |
| Update task in Todoist before closing | Web / API | HTTP Request | POST |
| Map Notion to Todoist1 | Code | Code | See node parameters |
| Handle empty dates | Code | Code | See node parameters |
| Handle empty dates1 | Code | Code | See node parameters |
| Handle empty dates2 | Code | Code | See node parameters |
| Create task in Todoist | Web / API | HTTP Request | POST |
| Add project ID | Transform | Edit Fields | See node parameters |
| Lock Todoist ID | Data store | Redis | set |
| Generate UUID | Transform | Crypto | See node parameters |
| Update section (Sync API) | Web / API | HTTP Request | POST |
| Status changed | Routing | Filter | See node parameters |
| Aggregate | Transform | Aggregate | See node parameters |
| Prepare Batching Items | Transform | Edit Fields | See node parameters |
| Loop Over Items1 | Routing | Loop Over Items | See node parameters |
| Status is Done | Routing | If | See node parameters |
| Lock Todoist ID1 | Data store | Redis | set |
| Lock Todoist ID2 | Data store | Redis | set |
| Update task in Todoist | Web / API | HTTP Request | POST |
| Get mapped item | Transform | Edit Fields | See node parameters |
| Get mapped item1 | Transform | Edit Fields | See node parameters |
| Schedule Trigger | Trigger | Schedule Trigger | See node parameters |
| Prepare summary data | Transform | Edit Fields | See node parameters |
| Prepare summary data1 | Transform | Edit Fields | See node parameters |
| Merge | Transform | Merge | See node parameters |
| Prepare summary data2 | Transform | Edit Fields | See node parameters |
| Filter out status changes | Routing | Filter | See node parameters |
| Map Todoist to Notion | Code | Code | See node parameters |
| Map summary fields | Transform | Edit Fields | See node parameters |
| Convert to HTML Table | Transform | Html | convertToHtmlTable |
| Generate email body | Transform | Html | See node parameters |
| Get mapped item2 | Transform | Edit Fields | See node parameters |
| Gmail | Messaging | Gmail | See node parameters |
| Merge summary | Transform | Merge | See node parameters |
| Globals | Transform | Edit Fields | See node parameters |
| Execute Workflow Trigger | Trigger | Execute Workflow Trigger | See node parameters |
| Differences exist1 | Routing | Filter | See node parameters |
| Requires completion change1 | Routing | Filter | See node parameters |
| Create task in Notion | Integration | Notion | databasePage |
| Update task in Notion | Integration | Notion | databasePage / update |
| Mark as Done in Notion | Integration | Notion | databasePage / update |
| Mark as Obsolete in Notion | Integration | Notion | databasePage / update |
| Mark as In Progress in Notion | Integration | Notion | databasePage / update |
| Only continue if not locked | Routing | Filter | See node parameters |
| Lock Notion ID | Data store | Redis | set |
| Check if Todoist ID is locked | Data store | Redis | get |
| Update Description in Todoist | Integration | Todoist | update |
| Description has changed in Todoist | Routing | Filter | See node parameters |
| Update Description in Todoist1 | Integration | Todoist | update |
| Generate UUID1 | Transform | Crypto | See node parameters |
| Update section (Sync API)1 | Web / API | HTTP Request | POST |
| Get Backlog Section ID | Code | Code | See node parameters |
| Notion Task not found | Routing | If | See node parameters |
| Check if creating flag exists1 | Data store | Redis | get |
| Only continue if flag does not exist1 | Routing | Filter | See node parameters |
| Set creating flag1 | Data store | Redis | set |
| Neither focussed nor planned1 | Routing | Filter | See node parameters |
| Mark as Focussed in Notion | Integration | Notion | databasePage / update |
| Todoist trigger reference | Routing | No Op | See node parameters |
| Get Notion Task2 | Integration | Notion | databasePage / getAll |
| Notion Task found1 | Routing | Filter | See node parameters |
| Switch by Event | Routing | Switch | See node parameters |
| If event is not :deleted | Routing | If | See node parameters |
| Execution Data | Integration | Execution Data | See node parameters |
| Execution Data1 | Integration | Execution Data | See node parameters |
| Execution Data2 | Integration | Execution Data | See node parameters |
| Execution Data3 | Integration | Execution Data | See node parameters |
| Execution Data4 | Integration | Execution Data | See node parameters |
| Globals1 | Transform | Edit Fields | See node parameters |
| Map Todoist to Notion1 | Code | Code | See node parameters |
| Get Todoist Task1 | Integration | Todoist | get |
| Get Notion Task1 | Integration | Notion | databasePage / getAll |
| Requires completion change | Routing | Filter | See node parameters |
| Has been completed? | Routing | If | See node parameters |
| Differences exist | Routing | Filter | See node parameters |
| Only continue if not locked1 | Routing | Filter | See node parameters |
| Check if Notion ID is locked | Data store | Redis | get |
| Get Notion task | Integration | Notion | databasePage / get |
| Check if creating flag exists | Data store | Redis | get |
| Only continue if flag does not exist | Routing | Filter | See node parameters |
| Set creating flag | Data store | Redis | set |
| Todoist ID exists? | Routing | If | See node parameters |
| Get todoist ID | Transform | Edit Fields | See node parameters |
| Todoist ID exists | Routing | Filter | See node parameters |
| Get todoist ID1 | Transform | Edit Fields | See node parameters |
| Status changed but not empty | Routing | Filter | See node parameters |
| Status is Obsolete? | Routing | If | See node parameters |
| Status is not Done | Routing | Filter | See node parameters |
| Neither focussed nor planned | Routing | If | See node parameters |
| Notion trigger reference | Routing | No Op | See node parameters |
| Notion Webhook | Trigger | Webhook | See node parameters |
| Store Todoist ID1 | Integration | Notion | databasePage / update |
| Mark as Completed in Todoist1 | Integration | Todoist | close |
| Update task in Todoist1 | Web / API | HTTP Request | POST |
| Globals2 | Transform | Edit Fields | See node parameters |
| Mark as Incomplete in Todoist1 | Integration | Todoist | reopen |
| Generate UUID2 | Transform | Crypto | See node parameters |
| Update section (Sync API)2 | Web / API | HTTP Request | POST |
| Lock Todoist ID4 | Data store | Redis | set |
| Delete Task in Todoist2 | Integration | Todoist | delete |
| Map Notion to Todoist2 | Code | Code | See node parameters |
| Get Todoist Task2 | Integration | Todoist | get |
| Create task in Todoist1 | Web / API | HTTP Request | POST |
| Execution Data5 | Integration | Execution Data | See node parameters |
| Execution Data6 | Integration | Execution Data | See node parameters |
| Todoist Webhook | Trigger | Webhook | See node parameters |
| Execute Workflow | Subworkflow | Execute Workflow | See node parameters |
| Extract IDs | Transform | Edit Fields | See node parameters |
| Split out Notion changes | Transform | Split Out | See node parameters |
| Body is array? | Routing | If | See node parameters |
| Switch by project | Routing | Switch | See node parameters |
| Webhook | Trigger | Webhook | See node parameters |
| Switch | Routing | Switch | See node parameters |
| Execute Workflow1 | Subworkflow | Execute Workflow | See node parameters |
| Due date empty | Routing | Filter | See node parameters |
| Has not been completed? | Routing | If | See node parameters |
| End here | Routing | No Op | See node parameters |
| Retry limit reached | Routing | Stop And Error | See node parameters |
| Set tries | Transform | Edit Fields | See node parameters |
| Update tries | Transform | Edit Fields | See node parameters |
| Wait | Wait | Wait | See node parameters |
| Catch known error | Routing | If | See node parameters |
| If tries left | Routing | If | See node parameters |
| Retry limit reached1 | Routing | Stop And Error | See node parameters |
| Set tries1 | Transform | Edit Fields | See node parameters |
| Update tries1 | Transform | Edit Fields | See node parameters |
| Wait1 | Wait | Wait | See node parameters |
| Catch known error1 | Routing | If | See node parameters |
| If tries left1 | Routing | If | See node parameters |
| Set tries2 | Transform | Edit Fields | See node parameters |
| Update tries2 | Transform | Edit Fields | See node parameters |
| Wait2 | Wait | Wait | See node parameters |
| Catch known error2 | Routing | If | See node parameters |
| If tries left2 | Routing | If | See node parameters |
| Retry limit reached2 | Routing | Stop And Error | See node parameters |
| Turn Markdown into Notion Blocks | Code | Code | See node parameters |
| Handle each block separately | Transform | Split Out | See node parameters |
| Append Notion Block | Integration | Notion | block |
Setup
- Download the JSON and import it into an isolated n8n instance. Leave the workflow inactive.
- Connect your own credentials through n8n’s credential store. Replace
REPLACE_WITH_*values with your resource identifiers. - Inspect API destinations, Code nodes, database queries, file paths, and outbound actions.
- Run with synthetic data and harmless destinations. Enable triggers only after checking the actual outputs.
Requirements
An n8n instance with the listed node types and compatible versions. Service-connected workflows may require accounts, credentials, or paid APIs.
Credential types declared in the source
gmailOAuth2notionApiredistodoistApi
Workflow notes
Keep in sync
Keep in sync
if you update the mapping, make sure to change it in the other node as well!
Set Globals
Set Globals
Use Sync Setup Helper Workflow to generate the JSON and paste it in every Globals Nodes
Sticky Note14
Update section for each task in Todoist using the Sync API in full sync mode. Use batching and split into multiple batches, if needed, since there is a request limit of 100 full syncs within a 15 minute period.
Sticky Note16
Get open/incomplete tasks from both Notion and Todoist
Sticky Note17
Generate an email report containing all changes made by this workflow, since changes in Todoist are being overwritten.
Sticky Note23
Store more information, which was not available earlier
Sticky Note26
Set focussed flag, so it remains in Todoist even after updating in Notion
Sticky Note27
Create Notion page if it does not exist
Sticky Note28
Update Notion page if it needs to be changed
Sticky Note29
Checking for a cached flag in Redis, to prevent overrides and endless loops.
Sticky Note31
Retrieve Notion page, so it can be compared against
Sticky Note33
Store relevant information in highlighted execution data, in case of debugging
Sticky Note36
Following nodes reference to this node, so the trigger can easily be replaced
Sticky Note37
This prevents the creation of duplicates in Notion, by storing a special flag in Redis containing the Todoist ID
Sticky Note22
Update section for in Todoist using the Sync API in full sync mode (maximum of 100 requests within a 15 minute period!)
Sticky Note24
Delete the task if it does not fit into the filter anymore
Sticky Note25
Update the task if it needs to be changed
Sticky Note38
Notion tries to deliver webhooks again. To ensure the data is up to date, the payload must be retrieved again.
Sticky Note41
Continues on error, so it is not necessary to retrieve task beforehand
Sticky Note45
Instead of deleting pages in Notion, it is a safer practice to mark them as obsolete instead.
Sticky Note46
In rare cases Notion fires multiple webhooks within a short period of time, when creating a new task. This prevents the creation of duplicates in Todoist.
Sticky Note47
The mapping is more advanced and also includes fall-back values for edge-cases
Configure Webhook
Configure Webhook
Grab the webhook URL and use it in the Notion automation. If using the NotionWebhookEmulator instead, replace this node with the nodes shown below.
Sticky Note7
Only sync focussed & planned tasks for efficiency reasons
Sticky Note19
The mapping between the Notion and Todoist tasks is stored in Notion
Sticky Note11
The Todoist ID is being cached in Redis for 15 seconds, so the other Diff sync does not get triggered.
Sticky Note13
Create task if it does not exist (anymore)
Sticky Note6
Edge case: the task has been moved from Obsolete to Done
Todoist Webhook Router Example (optional)
Todoist Webhook Router Example (optional)
Since Todoist sends the updates of all projects to a single webhook, the requests need to be routed accordingly. To do so, create a separate workflow, each containing the flow above, except replace the webhook and switch nodes with an "Execute Workflow Trigger". Then activate the nodes below and configure it, so it calls the sub-workflows accordingly.
Sticky Note51
Using the event type from Todoist is the only option to actually recognize, if a task was deleted
Sticky Note53
Previous node always returns output data, since it uses a filter to retrieve a matching item
Sticky Note54
Reverse mapping the Section/Status
Sticky Note57
The Notion ID is being cached in Redis for 15 seconds, so the other Diff sync does not get triggered.
Sticky Note58
Delete Notion page if Todoist task has been deleted
Sticky Note60
Handle tasks only found in Notion. This can still mean, that the task exists in Todoist, but does not match the filter, since it has been completed. In that case the Todoist task is being retrieved. If it exist, the Todoist Task will be reopened, otherwise it will be created. The Todoist ID is being cached in Redis for 15 seconds, so the Diff sync does not get triggered.
Sticky Note61
Handle tasks which show differences between both systems. If there are differences, the Todoist task will be overwritten by all properties from Notion. Also the status is being updated in the following step accordingly. The Todoist ID is being cached in Redis for 15 seconds, so the Diff sync does not get triggered.
Sticky Note62
Handle tasks only found in Todoist. This can still mean, that the task exists in Notion, but does not match the filter, since it has been completed. In that case the Notion page is being retrieved. If it does not exist, because it was deleted, the Todoist Task will be deleted as well. The Todoist ID is being cached in Redis for 15 seconds, so the Diff sync does not get triggered.
Set Schedule
Set Schedule
Set the preferred period. Usually once a day, likely outside working hours should be a solid option.
Sticky Note64
Preparing data for comparing
Set mail address
Set mail address
Provide the email address where the report should be sent to. This node can also be replaced by an SMTP or similar node.
Sticky Note66
If consuming data from the Notion Webhook Emulator instead of a regular Notion Webhook, the data is structured differently and can contain multiple items. This section makes the workflow compatible with both approaches.
Register Webhook in Todoist App
Register Webhook in Todoist App
Grab the URL from here and add it to the Todosit developer app as described in the setup instructions
Set project ID and name
Set project ID and name
The ID can be copied from JSON in Globals Node. If syncing multiple Todoist projects, use the router described below.
Replace trigger
Replace trigger
Use this trigger in the sub-workflows instead
Register Webhook
Register Webhook
Configure this webhook in the Todoist App as mentioned above
Set project IDs and names
Set project IDs and names
Copy each project ID from the JSON of the different Globals Nodes and name the branches by the projects.
Select sub-workflows
Select sub-workflows
Choose the sub-workflows based on the branch of the Switch Node
0. Start here
0. Start here
Notion
- A database must already exist (get a basic template here) with the following properties (case matters!):
- Text: "Name"
- Status: "Status", containing at least the options "Backlog", "In progress", "Done", "Obsolete"
- Select: "Priority", containing the options "do first", "urgent", "important"
- Date: "Due"
- Checkbox: "Focus"
- Text: "Todoist ID"
Todoist
- A project must already exist with the same sections like defined as Status in Notion (except Done and Obsolete)
Redis
- Create a Free Redis Cloud instance or self-host
Setup credentials in n8n
- Notion: https://docs.n8n.io/integrations/builtin/credentials/notion/
- Todoist: https://docs.n8n.io/integrations/builtin/credentials/todoist/
- Redis: https://docs.n8n.io/integrations/builtin/credentials/redis/
Clone workflow
- Clone this workflow into your n8n environment and map the credentials (beware, Todoist credentials are also used in HTTP request nodes)
- Check for every yellow sticky notes. They show instructions which need to be done once
1. Generate config JSON for Globals Nodes
1. Generate config JSON for Globals Nodes
The sync workflows have a Node named "Globals", which contain mapping information for both Notion and Todoist. This flow helps to generate the config, using a multi-form.
How to use
- Check if the credentials in the Notion and HTTP Request Nodes are set correctly
- Activate this workflow
- Open production URL of the form trigger and complete the form
- Copy the resulting JSON code into all 3 "Globals" nodes
2. Setup Todoist Webhook
2. Setup Todoist Webhook
-
In the Todoist App open Settings > Integrations > Developer > Build Integrations
-
Click "Create a new app"
- App name: "Notion-Todoist Sync" / Name of your n8n instance
- App service URL: (leave empty)
-
Click "Create app"
-
Update settings of the app:
- OAuth redirect URL: Grab the production URL of the "OAuth redirect" Node (from step 3 on the right)
-
Click on "Save settings", leave tab open
-
Activate this workflow
-
Open the Production URL of the "Todoist Webhook Setup Helper" Node (from step 3 on the right)
-
Copy the Client ID and secret from the App settings into the form and submit it
-
When prompted for permissions, click on "Agree"
-
Go back to App settings tab
-
Scroll further down to "Webhook"
-
Update settings of the app:
- Webhook callback URL: Grab the production URL of the "Todoist Webhook" Node (Todoist to Notion Diff sync)
- Select these options:
- item:added
- item:updated
- item:completed
- item:uncompleted
- item:deleted
-
Click "Save webhook configuration"
-
Click "Activate webhook"
-
Settings can now be closed
4. Setup Notion Webhook
4. Setup Notion Webhook
You can choose from two options and also choose different options for different workspaces, e.g. optionA for a business related workspace and option B for a private workspace. Both options work similarly, option A is a bit easier to setup, yet it is more expensive.
Option A: Notion Webhook via Automation (paid plan required)
- Create a new automation on the database which should be synced
- Rename the automation to "Notion-Todoist Sync" or similar
- Add trigger: "Any property edited"
- Add action: "Send webhook"
- URL: Grab the production URL from the "Notion Webhook" Node of this workflow (Notion to Todoist Diff sync)
- Content: Select all existing properties
- Click "Save"
Option B: Notion Webhook Emulator (no subscription required)
- Follow the instructions here: https://n8n.io/workflows/2284-provide-real-time-updates-for-notion-databases-via-webhooks-with-supabase/
- Use the Webhook URL from the "Notion Webhook" Node of this workflow to push the updates to
3. Activate Todoist Webhook
3. Activate Todoist Webhook
Webhooks are a bit different in Todoist then usual. A Developer App needs to be created and once be connected to via OAuth, to activate the webhook functionality. This flow only works when using the production URL of it's form trigger.
Sticky Note18
Retrieve Todoist task, which can be completed or deleted at this point. Using advanced retry logic, since the "Get many tasks" endpoint does not return completed tasks.
Sticky Note4
Todoist webhooks can arrive late. To ensure the data is up to date, the payload must be retrieved again. Triggers on deletion can overlap, thus try 3 times, then just stop. Using advanced retry logic, since the "Get many tasks" endpoint does not return completed tasks. No more retries needed, if Task does not exist.
Sticky Note30
Overwrite description with Notion URL
Sticky Note34
Copy task description initially to Notion, before it get's replaced in Todoist by the Notion URL
5. Finishing Touches
5. Finishing Touches
Cleanup
Now technically everything is ready, but it is missing better usability and maintainability. So start by moving (cut & paste) the 3 big flows (outlined in blue) to separate n8n workflows. You can name them as follows:
- Notion-Todoist Full Sync ()
- Notion-Todoist Diff Sync ()
- Todoist-Notion Diff Sync ()
Make sure to activate every workflow which requires it. Also save this workflow to avoid duplicates.
First run
- Execute the Notion-Todoist Full Sync workflow manually first
- Now that the initial sync is done and you can start using the realtime sync
Realtime Notion Todoist 2-way Sync Template
Realtime Notion Todoist 2-way Sync Template
Purpose and structure
This folder contains the Realtime Notion Todoist 2-way Sync Template workflow. Its executable nodes use aggregate, code, compareDatasets, crypto, executeWorkflow, executeWorkflowTrigger, executionData, filter, form, formTrigger, gmail, html, httpRequest, if, merge, noOp, notion, redis, respondToWebhook, scheduleTrigger, set, splitInBatches, splitOut, stopAndError, switch, todoist, wait, webhook. The table and connection map below are derived from the JSON, not an inferred guarantee of behavior.
Setup
Import inactive. Configure your own credentials and replace REPLACE_WITH_* values. Review external destinations and Code nodes before running.
Entry points
OAuth redirect, Notion-Todoist Sync Setup Helper, Todoist Webhook Setup Helper, Schedule Trigger, Execute Workflow Trigger, Notion Webhook, Todoist Webhook, Webhook
Verification
Sanitized source candidate; not runtime-tested. This overview is generated from node metadata. Read the folder README for the connection map, credential types, and license.
Attribution
https://github.com/Zie619/n8n-workflows/blob/94007c1445d9258a7da116646b79473e7c7c3282/workflows/Webhook/1897_Webhook_Filter_Sync_Webhook.json MIT; preserve source notices.
Canvas section labels
These name areas of the original canvas; they do not include explanatory instructions.
- Unidirectional sync from Notion to Todoist
- Trigger based synchronization from Todoist to Notion (part of bi-directional sync)
- Trigger based synchronization from Notion to Todoist (part of bi-directional sync)