GTD with Obsidian: a ready-to-go GTD system with Task Sequencing, Quick-add template, Waiting-on, Someday/Maybe, and more

This is excellent, a small, what I’d consider improvement, if you alter the end of the tasks.js to have:

// Projects without next action
if (noNextAction.length && !globalExcludeString) {
dv.header(2, ‘:triangular_flag_on_post: Projects without next actions’)
dv.list(noNextAction.map(p => p.link))
}

It means you cam put completed projects in a “Finished Projects” folder and they don’t show as projects withouut tasks on the master tasks note.

Projects will already be excluded from the master tasks note if you either add a #completed tag, or check a completed YAML checkbox.

You can then make a “Finished Projects” list by searching for projects with either of those properties.

2 Likes

That’s a much better solution…:slight_smile:

I know this is breaking with true GTD, but…I’m trying to work out how to edit the tasks.js such that if there is a scheduled date in the future, such as :hourglass_flowing_sand: 2024-09-02 in the task it doesn’t show on next steps. Either by creating a separate scheduled group (my preference) or just excluding it. I can’t for the life of me think how to access the text in the task to check this - any pointers?

@AlanG Getting the following error when trying to run the Task Menu: plugin:templater-obsidian:61 Templater Error: Template parsing error, aborting. Template syntax error: Unexpected token 'return'
I have all of the correct files and have the Template Folder location specified as well as User Scripts. Any ideas what I can try? I have the latest versions of Templater and Dataviewer as well.

If you download the demo vault and test it do you still have the same issues?

I have downloaded and tested just now and it works without any errors.

Hi Alan, I found your GTD script and am struggling with getting it to work (Obsidian n00b as well for that matter).

I imported (copied) you scripts and templates folder into my vault, activated dataview and templater.

When I start a new note and insert one of your templates at first it looks fine (the headers are being filled and the table “Notes referencing Untitled 1” are being filled (Untitled 1 being the default title of the note). But as soon as I change the title or insert any text into the note the table is gone and I get this error message:

Dataview: Failed to execute view '99 Utilities/Scripts/Dataview/mentions-table.js'.

TypeError: Cannot read properties of undefined (reading 'file')
1 Like

This is such a great tool, thank you @AlanG for creating it!

TL:DR I am new to Dataview and have messed something up. The “Tasks” note is no longer updating tasks from anywhere in my vault. Can someone point me in the right direction on how to fix this?

In depth: I opened the example vault (“obsidian-gtd-main”), toyed around with it and decided to adopt it. Instead of building my new vault from scratch, I simply added my notes to the example vault. But something in that integration broke the automation process of gathering all tasks from throughout my vault. I’m sure this is a simple fix and that it is user error not like direction please.

1 Like

I’m having similar issues to @jsmith967 and @RickG477 . I have copied the entire test vault over, works just fine. But when I move the scripts / templates etc. only into my existing vault, the Tasks note breaks (but all others work fine). Here is the dataview error I’m getting:

Dataview: Failed to execute view ‘Extras/Scripts/Dataview/tasks.js’.
TypeError: task.subtasks.push is not a function

@AlanG I’m excited to get this working, I love the simplicity of what you’ve built here and thank for you sharing it!

Hi @mcs , can you edit your tasks.js file and change line 110 from this:

task.subtasks.push({

to this:

task?.subtasks?.push({

Let me know if that solves your problem.

@jsmith967 I’m afraid without more detail there’s not much I could advise :confused:

1 Like

@Overjoyed4424 you need to add a check for a scheduled date into the task where clause, like this:

In tasks.js, change this section:

page.tasks
  .where(t =>
    t.text && // where the task has text (is not blank)
    !t.completed && // and not completed
    !t.tags.includes('#exclude') && // and not excluded
    (!t.header.subpath || !t.header.subpath.includes('exclude')) &&
    !globalExclude.headings.includes(t.header.subpath) // and the heading is not excluded
  )

to check for a scheduled date:

page.tasks
  .where(t => {
    // Check for a scheduled date
    const scheduledDate = t?.text?.match(/⏳\s?(\d{4}-\d{2}-\d{2})/)
    if (scheduledDate) {
      // Check if the date is in the future
      if (moment(scheduledDate[1]) > moment()) {
        // Skip and move on to the next task
        return false
      }
    }

    return t.text && // where the task has text (is not blank)
      !t.completed && // and not completed
      !t.tags.includes('#exclude') && // and not excluded
      (!t.header.subpath || !t.header.subpath.includes('exclude')) &&
      !globalExclude.headings.includes(t.header.subpath) // and the heading is not excluded
  })
3 Likes

Thank you for the response @AlanG , I’m sorry I never got a notif for your message.

I just changed line 110 to what you provided but I’m still getting the same error:
TypeError: task?.subtasks?.push is not a function

I’ll leave this tab open today in case you have another suggestion, much appreciated!

First of all, thank you very much for this much needed implementation of GTD in Obsidian!

When i tried to implement the tasks.js in my vault, i am getting a weird problem that when i check of the task from the dataview view , the next task in the sequence is not updated ; but if i check the task off manually by going to the original file containing that task then the dataview view of tasks.js is updated correctly…

This problem was SOLVED when i commented out the whole piece of code that was used to fetch the value of the headingLine variable? I believe that piece of code might have some problem that i am not knowledgeable enough to pin point.

PS. I did make so changes to the tasks.js file to make it suitable for my needs - the main changes being that i) i commented out the entire section on fetching tasks that are not linked to particular project ii) i added a folder path in the dv.pages() argument so that it will only look for tasks in that folder which is what i need.

My whole modified tasks.js file given below.

/*
 * ---------------------
 * CONFIGURATION OPTIONS
 * ---------------------
 */

// Add the folders, tags, and headings which should be excluded.
// Tasks found on pages with these tags or paths will not appear in the Tasks page.
// Headings will to exclude tasks that fall under a heading with this name.
const globalExclude = {
  folders: [
    'Utility'
  ],
  tags: [
    '#exclude-master-tasklist',
    '#completed'
  ],
  headings: [
    '🌱 Daily Habits'
  ]
}

// When displaying tasks from a project, tasks that fall under headings with these
// names won't have these headings displayed when showing the project info
const hideSubsectionName = [
  'Todo',
  'Tasks'
]

// If you are using the Tasks plugin, you can hide tasks which are scheduled in the future.
// They will have a date in the format ⏳ 2021-04-09
const hideFutureScheduledTasks = false

/*
 * ----------------------------
 * END OF CONFIGURATION OPTIONS
 * ----------------------------
 */

// Set up the variables to store the tasks and projects
const tasks = []
const noNextAction = []
const excludeItems = ['', ...globalExclude.tags]
globalExclude.folders.forEach(folder => excludeItems.push(`"${folder}"`))
const globalExcludeString = excludeItems.join(' AND -')

// Define groups for master tasklist page
const Groups = {
  Waiting: 0,
  Priority: 1,
  Normal: 2,
  Someday: 3
}

/**
 * Take a task and the page it's from, and return a formatted element for the tasks array
 * @param {*} task
 * @param {*} page
 * @returns {object}
 */
function generateTaskElement (task, page) {
  let group = Groups.Normal
  if (task.tags.includes('#someday')) {
    group = Groups.Someday
  } else if (task.tags.includes('#waiting-on')) {
    group = Groups.Waiting
  } else if (task.text.includes('🔼') || page.tags.includes('#🔼')) {
    group = Groups.Priority
  }

  // Get the created date of a task for sorting, either from the task text
  // in the format of ➕2024-04-20, or from the page metadata.
  let date
  const hasDate = task?.text?.match(/➕\s?(\d{4}-\d{2}-\d{2})/)
  if (hasDate) { // Task inline created date
    date = moment(hasDate[1].valueOf())
  } else if (page.created) { // Page `created` frontmatter property
    date = page.created?.ts || moment(page.created).valueOf()
  }
  date = date || page.ctime.ts

  return {
    task,
    date,
    group
  }
}

/*
 * Process projects
 */
dv.pages('(#active-project or "path/to/folder/with/my/tasks") ' + globalExcludeString)
  .where(project => !project.completed).file
  .forEach(project => {
    const sections = []
    if (!project.tasks.filter(t => !t.completed && t.text).length) {
      // There is no next action for this project
      noNextAction.push(project)
    } else {
      project.tasks
        .where(t => !t.completed && t.text)
        .forEach(task => {
          const sectionName = task.section.subpath || 'root'
          // Select only the first task from each section. This allows task sequencing.
          if ((!sections.includes(sectionName) && !sectionName.includes('exclude')) || sectionName.includes('🟰')) {
            sections.push(sectionName)
            let subSection = ''
            let headingLine = 1
            // if (
            //   // There is a subpath/heading
            //   task.section.subpath &&
            //   // And it's not one of our whitelisted headings which don't require a notated sub-section
            //   !hideSubsectionName.includes(sectionName) &&
            //   // And it doesn't match the project name
            //   sectionName !== project.name.replace(/\W/g, ' ')
            // ) {
            //   // Add it as a sub-section to the project name
            //   subSection = ` > ${sectionName}`
            //   // Find the line-position of the heading for this task
            //   const file = app.fileManager.vault.getAbstractFileByPath(project.path)
            //   const headings = app.metadataCache.getFileCache(file).headings
            //   const match = headings.find(x => x.heading === sectionName)
            //   headingLine = match ? match.position.start.line : task.line - 1
            // }
            // By adding this subtask, we can get a sub-line with the project and/or heading names
            task?.subtasks?.push({
              children: [],
              path: task.path,
              link: task.link,
              // line: headingLine, // We need this to be able to jump to the heading when clicked
              lineCount: task.lineCount,
              position: task.position,
              text: `🗃️ *${project.name}${subSection}*`
            })
            tasks.push(generateTaskElement(task, project))
          }
        })
    }
  })

/*
 * Process tasks

dv.pages('-#project' + globalExcludeString)
  .where(p => p.file.tasks.length && !p['kanban-plugin'] && !p['exclude_master_tasklist']).file
  .forEach(page => {
    page.tasks
      .where(t => {
        if (hideFutureScheduledTasks) {
          // Check for a scheduled date in the format of ⏳2024-04-20
          const scheduledDate = t?.text?.match(/⏳\s?(\d{4}-\d{2}-\d{2})/)
          if (scheduledDate) {
            // Check if the date is in the future
            if (moment(scheduledDate[1]) > moment()) {
              // Skip and move on to the next task
              return false
            }
          }
        }

        return t.text && // where the task has text (is not blank)
          !t.completed && // and not completed
          !t.tags.includes('#exclude') && // and not excluded
          (!t.header.subpath || !t.header.subpath.includes('exclude')) &&
          !globalExclude.headings.includes(t.header.subpath) // and the heading is not excluded
      })
      .forEach(task => tasks.push(generateTaskElement(task, page)))
  })
 */



// Sort tasks into groups, then ascending by created time
tasks.sort((a, b) => a.group - b.group || a.date - b.date)

/**
 * Output a formatted task list
 * @param {number|null} group - Filter for tasks in a particular group, or null for all tasks
 * @param {string|null} header - The text header for the task list
 */
function taskList (group, header) {
  const list = isNaN(group) ? tasks : tasks.filter(x => x.group === group)
  if (list.length) {
    if (header) dv.header(2, header)
    dv.taskList(list.map(x => x.task), false)
  }
}

// Projects without next action


// Output the task list
taskList(Groups.Priority, '🔼 Priority',)
taskList(Groups.Normal, '✅ Next actions')
taskList(Groups.Waiting, '⏳ Waiting on...')
taskList(Groups.Someday, '💤 Someday')

if (noNextAction.length) {
  dv.header(2, '🚩 Projects without next actions')
  dv.list(noNextAction.map(p => p.link))
}

Are you sure you’re waiting long enough for the Dataview view to refresh? The next task won’t display instantly - it will show up when the Dataview query refreshes. If you’ve raised Dataview’s “Refresh interval” setting in your vault, it could be many seconds or even minutes before that happens.

I have tested using the demo vault just now and sequenced tasks update without issues (after waiting a few seconds for the Dataview update).

However, if commenting out that section works for your workflow - go for it! :+1:

2 Likes

Thanks for replying Alan!

I must have waited for around a minute but probably not more than that. My vault is quite large so it might be that getting that headingLine is computationally intensive somehow.

Thanks again for your time and effort put into making this! And thank you for making the code so readable with helpful comments that even a JavaScript newbie like me could understand things, make some changes and get it to work for my use-case .

Commenting out the line

// line: headingLine,

Like I had explained in the earlier comment somehow causes the subtask line (that shows the heading section under which the task occurs) to not show the heading name correctly.

This can be reproduced in the demo vault by commenting that line out.

Any idea how to circumvent this problem?

Change it to

line: task.line - 1,

How many notes do you have in your vault? I have 5000 and don’t experience issues with subtasks. The code to process the subtasks should be quick as it uses Obsidian’s cache.

2 Likes

Changing to that line worked! Thank you so much!

I only have 3432 files, but theg are not all in one folder, rather there are several long chains of sub-folders with only a few notes inside a sub-folder.
Also I use quite a number of other plugins. Maybe some other plugin might be causing the issue.
A list of the plugins that are enabled in my vault are given in the code block below:-

Advanced Mobile Toolbar
Advanced New File
Archiver
Breadcrumbs 
Commander
Dataview
Footnote shortcut
Obsidian42 text transporter
Omnisearch
Outliner
Quick Switcher++
QuickAdd
Recent Files 
Slurp
Tasks 
Timestamper

Hi @AlanG any thoughts on this error? I still can’t use your script unfortunately. Any ideas much appreciated.

I’m not sure without seeing the vault in question, sorry.

The only thing I can suggest is:

  1. Make a new demo copy of your vault
  2. Set it up the same way without any of your real notes
  3. Add some demo notes
  4. Make sure the same error is showing up
  5. Zip up the vault and post the link here so I can have a look.
1 Like