I’ve recently read David Allen’s Getting Things Done [1], more commonly known as GTD. I’ve taken to the approach, especially the concept of the next action list and the six level model for categorizing work. I’ve even integrated [2] my next action list into Obsidian, after years of avoiding the latter for task tracking.
One other tool that Allen swears by in GTD is the tickler file, or suspense file [3]. It is a technique for ensuring that you are reminded of tasks/projects/notes at a particular time. You could implement this with physical files and folders (see Fig. 1 below for the GTD version), a To-do app that supports reminders, post-it notes, etc. I spend the majority of my time working in front of my computer, and a lot of in in Obsidian, so having my suspense file right there is valuable.
I’ve written a short DataviewJS implementation of a tickler file that I use in my Daily Note template. Below, I go over the additional metadata that I put in the frontmatter of my notes, show the JS implementation, and show how it gets used in my templates. You can skip ahead to the Gist with the implementation if you’d like!
Implementation
I wanted to be able to easily “move” the tickler file ahead in time, as well as do something you cannot do with a physical suspense: put it in multiple folders at the same time. The easiest way to achieve this is to add tickler and a tickled arrays in my frontmatter. The first is used to indicate in which folder the file (or note in this case) should be, and the latter to mark the note as reviewed at a particular date. This allows me to have mutliple reminders in the future.
---
tickler: [2025-09-11, 2026-03-01]
tickled: [2025-09-11]
---
In the example above, I set a reminder for September 11th, and I have marked it as reviewed (through the tickled field) on the same date. I would then expect this note to only appear in my March 1st, 2026 daily note. If tickled had been empty, I would expect the note to appear in my daily note template starting from September 11th onwards (i.e. before March 1st, 2026).
My initial, simpler, query did not produce the expected behaviour:
TABLE aliases as Title, file.etags as Tags, tickler as Date
WHERE tickler AND max(tickler) <= date(2025-10-04) AND max(tickled) < max(tickler)
The use of max broke the ability to mark notes to be tickled at multiple future dates, as this query would only show the later date. I had to switch to a simple DataviewJS implementation that allowed more complex logic around matching time periods. Here is the script in its entirety below and in this Gist.
// Match up time periods for the tickler file and show items that haven't been tickled yet.
// Match up time periods for the tickler file and show items that haven't been tickled yet.
//
// My tickler notes have two properties set up:
// - tickler: [YYYY-MM-DD, YYYY-MM-DD,...]
// - tickled: [YYYY-MM-DD, ...]
//
// I want a note to appear in my tickler file whenever the current date is larger than the largest element
// in tickler smaller than the largest element in tickled. That will allow me to have multiple tickler
// dates in the future.
// @param { dv.page } page - page fed through a dv.page query.
// @param { dv.luxon.DateTime } date - date passed as as luxon.DateTime object,
// the same as the DataView dates. Makes it easier to do
// date comparisons.
function tickler_file(page, date) {
// Helper function for comparing Date objects.
function isSameOrBeforeDay(d1, d2) {
return d1.hasSame(d2, "day") || d1 < d2;
}
debugger;
if (!date.isLuxonDateTime) {
throw new Error(`You passed an invalid date into the query.
Make sure to wrap the date in dv.luxon.DateTime.fromISO('YYYY-MM-DD')`);
}
if (!page.tickler) {
// No tickler field, probably a bad query. Simply return false.
return false;
}
const tickler_dates = Array.isArray(page.tickler)
? page.tickler
: [page.tickler];
// If it hasn't been tickled yet, find the minimum date in the tickler array.
if (!page.tickled) {
// Find the min
const min_tickler_date = tickler_dates.reduce((latest, current) =>
latest < current ? latest : current,
);
return isSameOrBeforeDay(min_tickler_date, date);
}
// If it has been tickled in the past, check whether the next tickler date is the same or before
// the current date.
const tickled_dates = Array.isArray(page.tickled)
? page.tickled
: [page.tickled];
const latest_tickled = tickled_dates.reduce((a, b) => (b > a ? b : a));
const remaining_ticklers = tickler_dates.filter((d) => d > latest_tickled);
// If there are no remaining ticklers, we are done.
if (remaining_ticklers.length === 0) return false;
const next_tickler = remaining_ticklers.reduce((a, b) => (a <= b ? a : b));
return isSameOrBeforeDay(next_tickler, date);
}
exports.tickler_file = tickler_file;
If tickled is not set, we just need to check the minimal value of tickler against the current date. If both fields are populated, we find the the latest date in tickled, and check whether there are any fields in tickler that are later than that. If there are, we check that against the current date. And that’s about the size of it.
Note that we reuse the Luxon library to compare dates here. It is already used by DataView to parse the fields of the tickler and tickled array. It is easier to do this on query side, because this function is being loaded in its own sandbox, and doesn’t have access to the same environment DataView does. The script checks whether the date passed it is a Luxon.DateTime object.
This function gets called in my Daily note template as:
var utils = await dv.io.load("/Utils/dataviewjs_utils.js", "text");
eval(utils);
let pages = dv.pages()
.where(p => p.tickler)
.where(p => exports.tickler_file(
p,
dv.luxon.DateTime.fromISO("<% tp.date.now("YYYY-MM-DD", 0, tp.file.title, "YYYY-MM-DD-dddd") %>"))
);
dv.table(
["File", "Tags", "Date", "Date tickled"],
pages.map(p => [p.file.link, p.file.etags, p.tickler, p.tickled])
);
I am using Templater to name my daily notes as “YYYY-MM-DD-dddd”, and so the tp.date.now call above just parses out the date from the file name.
dv.io.load() is used to ensure that the query works on mobile as well as desktop. My previous approach used app.vault.adapter.basePath, but that isn't available on mobile. Let me know if you have a better solution!
Fig. 2 shows a rendered query in my Obsidian Vault. Once that blog post is published I can finally close that project and mark the file as reviewed in my suspense file!
Conclusion
Hopefully you’ve found the post useful, especially with the link to the Gist at the top. This post basically like reads like a recipe blog, except I didn’t refer to 9/11.
If you’d like to see my other posts, the easiest way to consume this site is through RSS. There are even feed categories for selected viewing: music, writing, technology. I love RSS, and I think you should [4] too!
Resources
- D. Allen, Getting Things Done: The Art of Stress-Free Productivity, Revised edition (Penguin Books, 2015).
- AlanG, “GTD with Obsidian: A Ready-to-Go GTD System with Task Sequencing, Quick-add Template, Waiting-on, Someday/Maybe, and More”. Published on August 21, 2023. Accessed on September 16, 2025.
- Cory Doctorow, “Keeping a Suspense File Gives You Superpowers”. Published on October 26, 2024. Accessed on April 21, 2025.
- Cory Doctorow, “You Should Be Using an RSS Reader”. Published on October 16, 2024. Accessed on April 21, 2025.