Last active
December 14, 2025 21:29
-
-
Save brokosz/dd336e6c44ab6eddae7a36fe344288de to your computer and use it in GitHub Desktop.
Reschedules completed recurring tasks in TaskPaper 3.9.4. Supports daily/weekly/monthly patterns and restores archived projects.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Application('TaskPaper').documents[0].evaluate({ | |
| script: `function(editor, options) { | |
| // CONFIGURATION | |
| const REMOVE_COMPLETED_TASK = false; // true = remove old task, false = keep it in archive | |
| let result = {found: 0, processed: 0, tasks: []}; | |
| editor.outline.groupUndoAndChanges(function() { | |
| const items = editor.outline.root.descendants; | |
| const duePattern = /@due\\((\\d{4}-\\d{2}-\\d{2})\\)/; | |
| const recurringPattern = /@(?:recurring|freq|repeat)\\(([^)]+)\\)/; | |
| const donePattern = /@done\\((\\d{4}-\\d{2}-\\d{2})\\)/; | |
| const projectTagPattern = /@project\\(([^)]+)\\)/; | |
| const tasksToReschedule = []; | |
| let currentProject = null; | |
| const projectMap = {}; | |
| // First pass: map all projects by name | |
| items.forEach(item => { | |
| const bodyText = item.bodyString; | |
| const projectMatch = bodyText.match(/^(.+):$/); | |
| if (projectMatch) { | |
| projectMap[projectMatch[1]] = item; | |
| } | |
| }); | |
| // Second pass: find tasks to reschedule | |
| items.forEach(item => { | |
| const bodyText = item.bodyString; | |
| if (bodyText.match(/^.+:$/)) { | |
| currentProject = item; | |
| } | |
| const dueMatch = bodyText.match(duePattern); | |
| const recurringMatch = bodyText.match(recurringPattern); | |
| const doneMatch = bodyText.match(donePattern); | |
| if (dueMatch && recurringMatch && doneMatch) { | |
| const projectTagMatch = bodyText.match(projectTagPattern); | |
| let targetProject = currentProject; | |
| let projectName = null; | |
| if (projectTagMatch) { | |
| projectName = projectTagMatch[1]; | |
| if (projectMap[projectName]) { | |
| targetProject = projectMap[projectName]; | |
| } else { | |
| targetProject = null; // Project doesn't exist, need to create | |
| } | |
| } | |
| result.found++; | |
| result.tasks.push(bodyText); | |
| tasksToReschedule.push({ | |
| item: item, | |
| dueDate: parseDate(dueMatch[1]), | |
| doneDate: parseDate(doneMatch[1]), | |
| recurring: recurringMatch[1], | |
| project: targetProject, | |
| projectName: projectName | |
| }); | |
| } | |
| }); | |
| tasksToReschedule.forEach(task => { | |
| result.processed++; | |
| // Create project if it doesn't exist but is tagged | |
| if (task.projectName && !task.project) { | |
| const newProject = editor.outline.createItem(task.projectName + ':'); | |
| editor.outline.root.insertChildrenBefore(newProject, editor.outline.root.firstChild); | |
| task.project = newProject; | |
| } | |
| if (task.doneDate >= task.dueDate) { | |
| // Normal case: done on or after due date, reschedule | |
| const newDueDate = rescheduleTask(task.dueDate, task.recurring); | |
| const newBodyText = task.item.bodyString | |
| .replace(duePattern, '') | |
| .replace(donePattern, '') | |
| .replace(projectTagPattern, '') | |
| .trim() + ' @due(' + formatDate(newDueDate) + ')'; | |
| const newItem = editor.outline.createItem(newBodyText); | |
| if (task.project) { | |
| task.project.insertChildrenBefore(newItem, task.project.firstChild); | |
| } else { | |
| editor.outline.root.insertChildrenBefore(newItem, editor.outline.root.firstChild); | |
| } | |
| if (REMOVE_COMPLETED_TASK) { | |
| task.item.removeFromParent(); | |
| } | |
| } else { | |
| // Done before due - keep original due date | |
| const newBodyText = task.item.bodyString | |
| .replace(donePattern, '') | |
| .replace(projectTagPattern, '') | |
| .trim(); | |
| if (REMOVE_COMPLETED_TASK) { | |
| task.item.bodyString = newBodyText; | |
| task.item.removeFromParent(); | |
| if (task.project) { | |
| task.project.insertChildrenBefore(task.item, task.project.firstChild); | |
| } else { | |
| editor.outline.root.insertChildrenBefore(task.item, editor.outline.root.firstChild); | |
| } | |
| } else { | |
| const newItem = editor.outline.createItem(newBodyText); | |
| if (task.project) { | |
| task.project.insertChildrenBefore(newItem, task.project.firstChild); | |
| } else { | |
| editor.outline.root.insertChildrenBefore(newItem, editor.outline.root.firstChild); | |
| } | |
| } | |
| } | |
| }); | |
| function parseDate(dateStr) { | |
| const parts = dateStr.split('-'); | |
| return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); | |
| } | |
| function rescheduleTask(dueDt, recurring) { | |
| recurring = recurring.toLowerCase(); | |
| const wordMap = { | |
| 'daily': '1d', | |
| 'weekly': '1w', | |
| 'monthly': '1m', | |
| 'yearly': '1y', | |
| 'biweekly': '2w', | |
| 'quarterly': '3m' | |
| }; | |
| if (wordMap[recurring]) { | |
| recurring = wordMap[recurring]; | |
| } | |
| const dMatch = recurring.match(/(.+)[dD]/); | |
| const wMatch = recurring.match(/(.+)[wW]/); | |
| const mMatch = recurring.match(/(.+)[mM]/); | |
| const yMatch = recurring.match(/(.+)[yY]/); | |
| let newDt = new Date(dueDt); | |
| if (dMatch) { | |
| newDt.setDate(newDt.getDate() + parseInt(dMatch[1])); | |
| } else if (wMatch) { | |
| newDt.setDate(newDt.getDate() + (parseInt(wMatch[1]) * 7)); | |
| } else if (mMatch) { | |
| newDt.setMonth(newDt.getMonth() + parseInt(mMatch[1])); | |
| } else if (yMatch) { | |
| newDt.setFullYear(newDt.getFullYear() + parseInt(yMatch[1])); | |
| } | |
| return newDt; | |
| } | |
| function formatDate(date) { | |
| const year = date.getFullYear(); | |
| const month = String(date.getMonth() + 1).padStart(2, '0'); | |
| const day = String(date.getDate()).padStart(2, '0'); | |
| return year + '-' + month + '-' + day; | |
| } | |
| }); | |
| return JSON.stringify(result); | |
| }` | |
| }); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Recurring Tasks Handler for TaskPaper
Automatically reschedules completed recurring tasks in TaskPaper 3.9.4.
Features
@due(YYYY-MM-DD),@done(YYYY-MM-DD), and@repeat/@recurring/@freqtagsdaily,weekly,monthly,yearly,biweekly,quarterly1d,2w,3m,1y@project(Project Name)tagConfiguration
Set
REMOVE_COMPLETED_TASKat the top of the script:true- Move tasks from archive (removes completed version)false- Copy tasks (keeps completed version in archive)Behavior
Usage
Run this script in TaskPaper to process all completed recurring tasks in your document.
Compatibility
Tested with TaskPaper 3.9.4