This workflow corresponds to n8n.io template #16801 — we link there as the canonical source.
This workflow follows the HTTP Request → Notion 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": "Deduplicate a Notion database by keeping the newest row and archiving the rest",
"tags": [],
"nodes": [
{
"name": "Every Day at 2am",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
240,
400
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 2
}
]
}
},
"typeVersion": 1.3
},
{
"name": "Get Database Rows",
"type": "n8n-nodes-base.notion",
"position": [
448,
400
],
"parameters": {
"simple": false,
"options": {},
"resource": "databasePage",
"operation": "getAll",
"returnAll": true,
"databaseId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"credentials": {
"notionApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"name": "Find Duplicates and Build Recap",
"type": "n8n-nodes-base.code",
"position": [
720,
400
],
"parameters": {
"jsCode": "// ==== CONFIG: edit these two lines to match your database ====\nconst MATCH_PROPERTY = 'Name'; // property used to detect duplicates\nconst KEEP_RULE = 'newest'; // 'newest' | 'oldest' | 'mostFilled'\n// ============================================================\n\nconst pages = items.map((i) => i.json);\n\nfunction readValue(page, propName) {\n const props = (page && page.properties) || {};\n const p = props[propName];\n if (!p) return '';\n const t = p.type;\n let v = '';\n if (t === 'title') v = (p.title || []).map((x) => x.plain_text).join('');\n else if (t === 'rich_text') v = (p.rich_text || []).map((x) => x.plain_text).join('');\n else if (t === 'url') v = p.url || '';\n else if (t === 'email') v = p.email || '';\n else if (t === 'phone_number') v = p.phone_number || '';\n else if (t === 'number') v = p.number == null ? '' : String(p.number);\n else if (t === 'select') v = (p.select && p.select.name) || '';\n else if (t === 'status') v = (p.status && p.status.name) || '';\n else if (t === 'formula') v = p.formula ? (p.formula.string ?? p.formula.number ?? '') : '';\n else v = '';\n return String(v).trim().toLowerCase();\n}\n\nfunction filledCount(page) {\n const props = (page && page.properties) || {};\n let n = 0;\n for (const k of Object.keys(props)) {\n if (readValue(page, k) !== '') n += 1;\n }\n return n;\n}\n\nfunction editedTime(page) {\n return page.last_edited_time || page.created_time || '';\n}\n\nconst groups = {};\nlet keyless = 0;\nfor (const page of pages) {\n const key = readValue(page, MATCH_PROPERTY);\n if (!key) { keyless += 1; continue; }\n (groups[key] = groups[key] || []).push(page);\n}\n\nconst losers = [];\nlet dupGroups = 0;\nfor (const key of Object.keys(groups)) {\n const group = groups[key];\n if (group.length < 2) continue;\n dupGroups += 1;\n const sorted = group.slice();\n if (KEEP_RULE === 'mostFilled') {\n sorted.sort((a, b) => filledCount(b) - filledCount(a) || editedTime(b).localeCompare(editedTime(a)));\n } else if (KEEP_RULE === 'oldest') {\n sorted.sort((a, b) => editedTime(a).localeCompare(editedTime(b)));\n } else {\n sorted.sort((a, b) => editedTime(b).localeCompare(editedTime(a)));\n }\n const keeper = sorted[0];\n for (let i = 1; i < sorted.length; i++) {\n const loser = sorted[i];\n losers.push({ json: {\n _type: 'archive',\n pageId: loser.id,\n keyValue: key,\n keptPageId: keeper.id,\n keptUrl: keeper.url || '',\n reason: 'duplicate of kept page ' + keeper.id\n } });\n }\n}\n\nconst recap = { json: {\n _type: 'recap',\n scanned: pages.length,\n keyless: keyless,\n duplicateGroups: dupGroups,\n toArchive: losers.length,\n property: MATCH_PROPERTY,\n keepRule: KEEP_RULE\n} };\n\nreturn [...losers, recap];"
},
"typeVersion": 2
},
{
"name": "Route by Item Type",
"type": "n8n-nodes-base.switch",
"position": [
944,
400
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "archive",
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json._type }}",
"rightValue": "archive"
}
]
},
"renameOutput": true
},
{
"outputKey": "recap",
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json._type }}",
"rightValue": "recap"
}
]
},
"renameOutput": true
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"typeVersion": 3.4
},
{
"name": "Archive Duplicate Page",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
1280,
336
],
"parameters": {
"url": "=https://api.notion.com/v1/pages/{{ $json.pageId }}",
"method": "PATCH",
"options": {},
"jsonBody": "{\"archived\": true}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "Notion-Version",
"value": "2022-06-28"
}
]
},
"nodeCredentialType": "notionApi"
},
"credentials": {
"notionApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"name": "Append Run to Audit Log",
"type": "n8n-nodes-base.notion",
"position": [
1280,
480
],
"parameters": {
"blockId": {
"__rl": true,
"mode": "url",
"value": ""
},
"blockUi": {
"blockValues": [
{
"textContent": "=Dedup run {{ $now.toFormat(\"yyyy-LL-dd HH:mm\") }}: scanned {{ $json.scanned }} rows, {{ $json.keyless }} without a key, {{ $json.duplicateGroups }} duplicate groups, archived {{ $json.toArchive }} duplicates. Match property: {{ $json.property }}. Keep rule: {{ $json.keepRule }}."
}
]
},
"resource": "block",
"operation": "append"
},
"credentials": {
"notionApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"name": "Overview Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-448,
64
],
"parameters": {
"width": 588,
"height": 748,
"content": "## Deduplicate a Notion database by keeping the newest row and archiving the rest\n\n### How it works\n\n1. On a daily schedule, it reads every row from the Notion database you choose.\n2. A Code node groups the rows by a property you set and finds any group with more than one row.\n3. For each group it keeps one row (the newest by default) and marks the others as duplicates.\n4. Each duplicate is archived to the Notion trash, where it stays recoverable for 30 days.\n5. A one-line recap is appended to a log page on every run, including runs that find no duplicates.\n\n### Setup steps\n\n- [ ] Connect a Notion credential and share the target database and the log page with the integration.\n- [ ] In Get Database Rows, select the database you want to deduplicate.\n- [ ] In Find Duplicates and Build Recap, set MATCH_PROPERTY and KEEP_RULE at the top of the code.\n- [ ] In Append Run to Audit Log, paste the URL of the page that should receive the recap.\n\n### Customization\n\nSet KEEP_RULE to newest, oldest, or mostFilled to decide which row survives, and adjust Every Day at 2am to run on any schedule you prefer."
},
"typeVersion": 1
},
{
"name": "Fetch Section Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
160,
224
],
"parameters": {
"color": 7,
"width": 460,
"height": 356,
"content": "## Fetch database rows\n\nRuns on a daily schedule and pulls every row with full property data."
},
"typeVersion": 1
},
{
"name": "Find Section Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
640,
224
],
"parameters": {
"color": 7,
"width": 484,
"height": 356,
"content": "## Find and route duplicates\n\nGroups rows by your chosen property, picks a keeper per group, and routes the duplicates onward."
},
"typeVersion": 1
},
{
"name": "Archive Section Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
1152,
160
],
"parameters": {
"color": 7,
"width": 372,
"height": 504,
"content": "## Archive duplicates and log the run\n\nArchives each duplicate to the Notion trash and appends a recap line to your log page."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"connections": {
"Every Day at 2am": {
"main": [
[
{
"node": "Get Database Rows",
"type": "main",
"index": 0
}
]
]
},
"Get Database Rows": {
"main": [
[
{
"node": "Find Duplicates and Build Recap",
"type": "main",
"index": 0
}
]
]
},
"Route by Item Type": {
"main": [
[
{
"node": "Archive Duplicate Page",
"type": "main",
"index": 0
}
],
[
{
"node": "Append Run to Audit Log",
"type": "main",
"index": 0
}
]
]
},
"Find Duplicates and Build Recap": {
"main": [
[
{
"node": "Route by Item Type",
"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.
notionApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs daily and deduplicates a Notion database by grouping pages on a chosen property, keeping one “best” page per group (newest by default) and archiving the rest, then appending an audit recap line to a Notion log page. Runs every day at 2am on a schedule. Fetches…
Source: https://n8n.io/workflows/16801/ — 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.
🧹 Archive (delete) duplicate items from a Notion database. Uses scheduleTrigger, notion, notionTrigger, stickyNote. Scheduled trigger; 11 nodes.
This workflow runs daily to normalize and backfill properties in a Notion database, standardizing Status values, deriving a Key slug and Created week stamp, updating only changed pages, and appending
WorkFlow 05. Uses notion, httpRequest. Scheduled trigger; 44 nodes.
Social Content Intelligence — Weekly Report. Uses httpRequest, notion, errorTrigger. Scheduled trigger; 42 nodes.
WorkFlow 08. Uses notion, httpRequest. Scheduled trigger; 37 nodes.