I use the “Archive Tasks” action to clear completed todos, similar to Taskpaper.
The Obsidian Archiver plugin performs the same action, but retains existing H2, H3, etc. groupings allowing me to maintain a log of the projects, areas, or outcomes the archived tasks were from.
And the Obsidian Tasks plugin time stamps creation and completion date.
This script Claude wrote for me rolls both features into a single action. It’s working nicely for me.
// =====================================================
// Archive Tasks — Heading-Aware + Timestamped
//
// Moves -
tasks to ## Archive, grouped under their
// original H2/H3/H4 heading, with creation date (draft)
// and completion datetime (system clock, to the minute).
//
// HOW TO INSTALL:
// 1. In Drafts, create a new Action
// 2. Add a single “Script” step
// 3. Paste this entire file into that step
// 4. Save. Run on any daily draft.
// =====================================================
(() => {
const ARCHIVE_HDR = ‘## Archive’;
const COMPLETED_RE = /^(\s*-\s[x]\s)/i;
const HEADING_RE = /^#{1,6}\s/;
const pad = n => String(n).padStart(2, ‘0’);
// — Timestamps —
const now = new Date();
const completionStamp =
${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())} +
${pad(now.getHours())}:${pad(now.getMinutes())};
const cd = draft.createdAt;
const creationStamp =
${cd.getFullYear()}-${pad(cd.getMonth()+1)}-${pad(cd.getDate())};
// — Split draft at archive boundary —
const text = editor.getText();
const lines = text.split(‘\n’);
const archiveIdx = lines.findIndex(l => l.trim() === ARCHIVE_HDR);
const activeLines = archiveIdx >= 0 ? lines.slice(0, archiveIdx) : […lines];
const existingLines = archiveIdx >= 0 ? lines.slice(archiveIdx + 1) :
;
// — Walk active section —
// Track the most recent heading; collect completed tasks grouped by it.
let currentHeading = null;
const groups = new Map(); // heading string → [stamped task lines]
const remaining =
;
for (const line of activeLines) {
if (HEADING_RE.test(line)) {
currentHeading = line.trim();
remaining.push(line);
continue;
}
if (COMPLETED_RE.test(line)) {
const stamped = line.trimEnd() + ➕ ${creationStamp} ✅ ${completionStamp};
if (!groups.has(currentHeading)) groups.set(currentHeading,
);
groups.get(currentHeading).push(stamped);
} else {
remaining.push(line);
}
}
// — Nothing to archive —
if (groups.size === 0) {
app.displayInfoMessage(‘No completed tasks found.’);
context.cancel(‘No completed tasks found.’);
return;
}
// — Build new archive block —
// New tasks, grouped under their original headings, newest at top.
const newBlock =
;
for (const [heading, tasks] of groups) {
if (heading) newBlock.push(heading);
newBlock.push(…tasks);
newBlock.push(‘’); // blank line between groups
}
// — Tidy whitespace at seams —
while (remaining.length && remaining[remaining.length - 1].trim() === ‘’) {
remaining.pop();
}
while (existingLines.length && existingLines[0].trim() === ‘’) {
existingLines.shift();
}
// — Reconstruct —
// Order: active content → ## Archive → new tasks → prior archived tasks
const output = [
remaining.join(‘\n’),
‘’,
ARCHIVE_HDR,
‘’,
…newBlock,
…(existingLines.length ? existingLines :
)
].join(‘\n’).trimEnd();
editor.setText(output);
})();