Skip to content

Instantly share code, notes, and snippets.

@brokosz
Last active December 14, 2025 21:29
Show Gist options
  • Select an option

  • Save brokosz/dd336e6c44ab6eddae7a36fe344288de to your computer and use it in GitHub Desktop.

Select an option

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.
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);
}`
});
@brokosz
Copy link
Author

brokosz commented Dec 14, 2025

Recurring Tasks Handler for TaskPaper

Automatically reschedules completed recurring tasks in TaskPaper 3.9.4.

Features

  • Detects tasks with @due(YYYY-MM-DD), @done(YYYY-MM-DD), and @repeat/@recurring/@freq tags
  • Supports word frequencies: daily, weekly, monthly, yearly, biweekly, quarterly
  • Supports numeric frequencies: 1d, 2w, 3m, 1y
  • Restores archived projects via @project(Project Name) tag
  • Creates new projects if they don't exist

Configuration

Set REMOVE_COMPLETED_TASK at the top of the script:

  • true - Move tasks from archive (removes completed version)
  • false - Copy tasks (keeps completed version in archive)

Behavior

  • If done ≥ due date: Creates new task with rescheduled due date based on recurrence
  • If done < due date: Creates new task with original due date (for early completions)

Usage

Run this script in TaskPaper to process all completed recurring tasks in your document.

Compatibility

Tested with TaskPaper 3.9.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment