{
  "name": "Roll overdue Notion tasks forward and flag ones rolled too often",
  "tags": [],
  "nodes": [
    {
      "name": "Every Morning at 7am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        240,
        464
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 7
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "name": "Get Open Tasks",
      "type": "n8n-nodes-base.notion",
      "position": [
        416,
        464
      ],
      "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": "Roll Overdue Dates and Flag Stale",
      "type": "n8n-nodes-base.code",
      "position": [
        720,
        464
      ],
      "parameters": {
        "jsCode": "// ==== CONFIG: edit these to match your Notion database ====\nconst DUE_PROPERTY    = 'Due';      // Date property that holds the deadline\nconst STATUS_PROPERTY = 'Status';   // Status or Select property that holds task state\nconst DONE_VALUES     = ['Done', 'Complete', 'Completed', 'Cancelled', 'Archived']; // states treated as finished, never rolled\nconst ROLLED_PROPERTY = 'Rolled';   // Number property: how many times this task has been rolled\nconst STALE_PROPERTY  = 'Stale';    // Checkbox property: set true once a task is rolled past the threshold\nconst STALE_THRESHOLD = 3;          // flag Stale once a task has been rolled MORE than this many times\nconst ROLL_TO         = 'today';    // 'today' | 'nextBusinessDay'\n// ==========================================================\n\n// \"Today\" comes from n8n's $now (a Luxon DateTime), taken at calendar-date granularity.\nconst todayISO = $now.toISODate(); // 'yyyy-MM-dd'\n\nconst doneSet = new Set(DONE_VALUES.map((v) => String(v).trim().toLowerCase()));\n\nfunction propOf(page, name) {\n  return ((page && page.properties) || {})[name];\n}\n\nfunction readDueDate(page) {\n  const p = propOf(page, DUE_PROPERTY);\n  if (!p) return '';\n  if (p.type === 'date' && p.date && p.date.start) return String(p.date.start);\n  if (p.type === 'formula' && p.formula && p.formula.date && p.formula.date.start) return String(p.formula.date.start);\n  return '';\n}\n\nfunction readStatus(page) {\n  const p = propOf(page, STATUS_PROPERTY);\n  if (!p) return '';\n  if (p.type === 'status') return (p.status && p.status.name) || '';\n  if (p.type === 'select') return (p.select && p.select.name) || '';\n  return '';\n}\n\nfunction readRolled(page) {\n  const p = propOf(page, ROLLED_PROPERTY);\n  if (p && p.type === 'number' && p.number != null) return Number(p.number);\n  return 0;\n}\n\nfunction readTitle(page) {\n  const props = (page && page.properties) || {};\n  for (const k of Object.keys(props)) {\n    if (props[k] && props[k].type === 'title') {\n      return (props[k].title || []).map((x) => x.plain_text).join('');\n    }\n  }\n  return '';\n}\n\nfunction nextBusinessDay(iso) {\n  // Snap a weekend date up to Monday. Weekday dates are returned unchanged.\n  const d = new Date(iso + 'T00:00:00Z');\n  let dow = d.getUTCDay(); // 0 = Sunday, 6 = Saturday\n  while (dow === 0 || dow === 6) {\n    d.setUTCDate(d.getUTCDate() + 1);\n    dow = d.getUTCDay();\n  }\n  return d.toISOString().slice(0, 10);\n}\n\nconst rollTargetISO = ROLL_TO === 'nextBusinessDay' ? nextBusinessDay(todayISO) : todayISO;\n\nconst out = [];\nfor (const item of items) {\n  const page = item.json;\n\n  const dueStart = readDueDate(page);\n  if (!dueStart) continue;                 // no due date, nothing to roll\n  const dueDateOnly = dueStart.slice(0, 10);\n  if (dueDateOnly >= todayISO) continue;    // due today or in the future, not overdue\n\n  const status = readStatus(page);\n  if (doneSet.has(status.trim().toLowerCase())) continue; // finished tasks are never touched\n\n  if (rollTargetISO === dueDateOnly) continue; // no-op guard: skip if the date would not change\n\n  const newRolled = readRolled(page) + 1;\n  const stale = newRolled > STALE_THRESHOLD;\n\n  out.push({\n    json: {\n      pageId: page.id,\n      taskName: readTitle(page),\n      statusValue: status,\n      oldDue: dueDateOnly,\n      newDate: rollTargetISO,\n      newRolled: newRolled,\n      stale: stale,\n    },\n  });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "name": "Update Rolled Tasks",
      "type": "n8n-nodes-base.notion",
      "onError": "continueRegularOutput",
      "position": [
        1088,
        464
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.pageId }}"
        },
        "options": {},
        "resource": "databasePage",
        "operation": "update",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Due|date",
              "date": "={{ $json.newDate }}",
              "includeTime": false
            },
            {
              "key": "Rolled|number",
              "numberValue": "={{ $json.newRolled }}"
            },
            {
              "key": "Stale|checkbox",
              "checkboxValue": "={{ $json.stale }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "name": "Overview Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        128
      ],
      "parameters": {
        "width": 560,
        "height": 768,
        "content": "## Roll overdue Notion tasks forward and flag ones rolled too often\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 finds rows whose due date is in the past and whose status is not done.\n3. Each overdue task has its due date rolled forward to today, or the next business day.\n4. A rolled counter is incremented, and a Stale flag is set once a task has been rolled past a threshold.\n5. Only the rows that changed are written back in place, so finished and not-yet-due tasks are never touched.\n\n### Setup steps\n\n- [ ] Connect a Notion credential and share the target database with the integration.\n- [ ] In Get Open Tasks, select the database that holds your tasks.\n- [ ] In Roll Overdue Dates and Flag Stale, set the property names, DONE_VALUES, STALE_THRESHOLD, and ROLL_TO at the top of the code.\n- [ ] In Update Rolled Tasks, map the Due, Rolled, and Stale property values to your own property names.\n\n### Customization\n\nSet ROLL_TO to today or nextBusinessDay, change STALE_THRESHOLD to control how many rolls make a task stale, and adjust Every Morning at 7am to run on any schedule you prefer."
      },
      "typeVersion": 1
    },
    {
      "name": "Read Section Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        192,
        272
      ],
      "parameters": {
        "color": 7,
        "width": 376,
        "height": 364,
        "content": "## Read open tasks on a schedule\n\nRuns each morning and pulls every row from the database with full property data."
      },
      "typeVersion": 1
    },
    {
      "name": "Roll Section Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        608,
        272
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 364,
        "content": "## Find overdue tasks and roll them\n\nSkips finished and not-yet-due rows, rolls each overdue task forward, and flags the ones rolled too often."
      },
      "typeVersion": 1
    },
    {
      "name": "Update Section Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        976,
        272
      ],
      "parameters": {
        "color": 7,
        "width": 316,
        "height": 364,
        "content": "## Write the rolled tasks back\n\nUpdates only the rows that changed: the new due date, the incremented counter, and the Stale flag."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Get Open Tasks": {
      "main": [
        [
          {
            "node": "Roll Overdue Dates and Flag Stale",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Morning at 7am": {
      "main": [
        [
          {
            "node": "Get Open Tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Roll Overdue Dates and Flag Stale": {
      "main": [
        [
          {
            "node": "Update Rolled Tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}