agentsclimarketplace

Obsidian tasks cli

Skill lexbritvin/obsidian-skills-pack/skills/obsidian-tasks-cli

Agent Skills for Obsidian β€” plugin syntax (queries, tasks, charts, interactive UI), graph view tuning, vault auditing, heatmap visualizations. Compatible with Claude Code, Codex CLI, OpenCode, and any agent-skills runtime.

Install
npx -y skills add lexbritvin/obsidian-skills-pack --skill obsidian-tasks-cli

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Reads, filters, and updates Obsidian tasks using the CLI. Lists tasks, marks them done, toggles status, counts tasks, and runs advanced queries via the Tasks plugin API. Use when the user asks to see their tasks, check what's overdue, mark something done, analyze task progress, get a task summary, count tasks, or retrieve task data programmatically β€” even if they don't mention CLI or API.

SKILL.md

11.1 KB, as published. Nobody here has run it

Managing Tasks via CLI

Use the obsidian CLI to read and update tasks. Obsidian must be running.

Start with simple CLI commands. Only use obsidian eval when you need filtering that CLI flags can't express (by priority, dates, tags, or custom logic).

Listing Tasks

obsidian tasks todo                    # all incomplete tasks
obsidian tasks done                    # completed tasks
obsidian tasks todo format=json        # JSON output for parsing
obsidian tasks verbose                 # grouped by file
obsidian tasks todo total              # count only
obsidian tasks todo path="Projects"    # filter by folder
obsidian tasks todo file="Shopping"    # filter by file name
obsidian tasks daily todo              # tasks from daily note

JSON output shape:

[
  {
    "status": " ",
    "text": "- [ ] Buy groceries πŸ“… 2025-04-10",
    "file": "Projects/Shopping.md",
    "line": "12"
  }
]

Status characters: " " = TODO, "/" = IN_PROGRESS, "x" = DONE, "-" = CANCELLED.

Updating Tasks

obsidian task path="note.md" line=12 toggle      # toggle status
obsidian task path="note.md" line=12 done         # mark done
obsidian task path="note.md" line=12 todo         # mark todo
obsidian task path="note.md" line=12 status="/"   # set in-progress

To add a new task to a file:

obsidian append path="note.md" content="- [ ] New task πŸ“… 2025-04-10"

Advanced Queries via Plugin API

For filtering by priority, dates, tags, or any combination β€” use obsidian eval to access the Tasks plugin cache. The cache holds all parsed task objects with rich queryable properties.

Wrap eval code in an async IIFE so you can call loadData() (used to read settings like globalQuery) and because obsidian eval doesn't support top-level return:

obsidian eval code="(async()=>{
  const plugin = app.plugins.plugins['obsidian-tasks-plugin'];
  const tasks = plugin.cache.getTasks();
  // filter, map, return
})()"

Respecting globalQuery

The Tasks plugin has a globalQuery setting (Settings β†’ Tasks β†’ "Global query") that applies to every rendered query block. The cache returned by getTasks() does not automatically apply it β€” you'll see all tasks, including ones the user has globally excluded.

To make eval recipes consistent with what the user sees in their query blocks, read globalQuery and translate the filters to JS:

const plugin = app.plugins.plugins['obsidian-tasks-plugin'];
const data = await plugin.loadData();
const globalQuery = data.globalQuery || '';

Then translate each line into a JS predicate. The Tasks query DSL is rich, but the common cases map cleanly:

Tasks DSL lineJS predicate on t
folder includes Xt.taskLocation.path.startsWith('X')
folder does not include X!t.taskLocation.path.startsWith('X')
path includes Xt.taskLocation.path.includes('X')
path does not include X!t.taskLocation.path.includes('X')
filename includes Xt.taskLocation.path.split('/').pop().includes('X')
tag includes #X / tags include #Xt.tags.includes('#X')
tag does not include #X!t.tags.includes('#X')
filter by function (task.file.tags ?? []).includes('#X') (file frontmatter tag)(t.taskLocation._tasksFile?.tags ?? []).includes('#X')
filter by function ! (task.file.tags ?? []).includes('#X')!(t.taskLocation._tasksFile?.tags ?? []).includes('#X')
not done!t.isDone
donet.isDone
is recurringt.isRecurring
is blocked / is not blockedt.isBlocked / !t.isBlocked
priority is highest/high/medium/low/lowestt.priorityName === 'Highest' etc.
priority is above medium['Highest','High'].includes(t.priorityName)
has due date / no due datet.due.moment !== null / t.due.moment === null
due before todayt.due.moment && t.due.moment.isBefore(window.moment().startOf('day'))
due this weekt.due.moment && t.due.moment.isSame(window.moment(), 'week')

For more complex DSL (boolean combinations, custom scripting, regex), translate inline as needed or warn the user that the filter is too complex to auto-apply.

Practical pattern β€” read globalQuery once, parse the common exclusion lines, then filter:

const data = await plugin.loadData();
const lines = (data.globalQuery || '').split('\n');
const excludedFolders = lines
  .map(l => l.match(/^\s*folder does not include\s+(.+?)\s*$/i))
  .filter(Boolean).map(m => m[1]);
const excludedFileTags = lines
  .map(l => l.match(/^\s*filter by function\s*!\s*\(task\.file\.tags\s*\?\?\s*\[\]\)\.includes\(["'](#?[^"']+)["']\)/))
  .filter(Boolean).map(m => m[1].startsWith('#') ? m[1] : '#' + m[1]);

const tasks = plugin.cache.getTasks().filter(t => {
  if (excludedFolders.some(f => t.taskLocation.path.startsWith(f))) return false;
  const fileTags = t.taskLocation._tasksFile?.tags ?? [];
  if (excludedFileTags.some(tag => fileTags.includes(tag))) return false;
  return true;
});

This covers folder exclusions and file-frontmatter tag exclusions β€” the two most common globalQuery patterns. For other DSL filters, extend the parsing or write a tailored predicate.

Note: changing globalQuery via saveData() does not refresh rendered query blocks until the plugin reloads (app.plugins.disablePlugin('obsidian-tasks-plugin'); app.plugins.enablePlugin('obsidian-tasks-plugin')). The cache itself never applies globalQuery β€” that's why these recipes filter manually.

Task Object Properties

PropertyTypeDescription
descriptionstringTask text without emoji signifiers
status.symbolstring , x, /, -
isDonebooleanWhether task is complete
isBlockedbooleanBlocked by a dependency
isBlockingbooleanOther tasks depend on this
priorityNamestringHighest, High, Medium, Normal, Low, Lowest
urgencynumberComputed urgency score
dueTasksDateDue date (check due.moment for presence)
scheduledTasksDateScheduled date
startTasksDateStart date
createdTasksDateCreated date
doneTasksDateCompletion date
cancelledTasksDateCancellation date
tagsstring[]Tags on the task
recurrenceRulestringRecurrence pattern
isRecurringbooleanHas recurrence
idstringTask ID (for dependencies)
taskLocation.pathstringFile path from vault root
taskLocation.lineNumbernumberLine number in file
originalMarkdownstringRaw markdown line

TasksDate

A TasksDate object is always present, but the underlying date may be empty. Check task.due.moment β€” it's null when no date is set:

task.due.moment                          // Moment.js object or null
task.due.moment.format('YYYY-MM-DD')     // "2025-04-10"
task.due.moment.isBefore(window.moment()) // overdue check
task.due.formatAsDate()                  // formatted string
task.due.category.groupText              // "Overdue", "Today", "Future", "Undated"

Recipes

Each recipe uses an async IIFE, reads globalQuery for folder exclusions, and applies them before filtering. Add more globalQuery translations from the table above when relevant.

Task count by status:

obsidian eval code="(async()=>{
  const plugin = app.plugins.plugins['obsidian-tasks-plugin'];
  const data = await plugin.loadData();
  const excluded = (data.globalQuery||'').split('\n').map(l=>l.match(/^\s*folder does not include\s+(.+?)\s*$/i)).filter(Boolean).map(m=>m[1]);
  const tasks = plugin.cache.getTasks().filter(t => !excluded.some(f => t.taskLocation.path.startsWith(f)));
  const counts = {};
  tasks.forEach(t => { const s = t.isDone ? 'done' : 'todo'; counts[s] = (counts[s]||0)+1; });
  return JSON.stringify(counts);
})()"

Overdue tasks:

obsidian eval code="(async()=>{
  const plugin = app.plugins.plugins['obsidian-tasks-plugin'];
  const data = await plugin.loadData();
  const excluded = (data.globalQuery||'').split('\n').map(l=>l.match(/^\s*folder does not include\s+(.+?)\s*$/i)).filter(Boolean).map(m=>m[1]);
  const today = window.moment().startOf('day');
  return plugin.cache.getTasks()
    .filter(t => !excluded.some(f => t.taskLocation.path.startsWith(f)))
    .filter(t => !t.isDone && t.due.moment && t.due.moment.isBefore(today))
    .map(t => t.description + ' πŸ“… ' + t.due.formatAsDate() + ' (' + t.taskLocation.path + ')')
    .join('\n') || 'No overdue tasks';
})()"

High priority tasks:

obsidian eval code="(async()=>{
  const plugin = app.plugins.plugins['obsidian-tasks-plugin'];
  const data = await plugin.loadData();
  const excluded = (data.globalQuery||'').split('\n').map(l=>l.match(/^\s*folder does not include\s+(.+?)\s*$/i)).filter(Boolean).map(m=>m[1]);
  return plugin.cache.getTasks()
    .filter(t => !excluded.some(f => t.taskLocation.path.startsWith(f)))
    .filter(t => !t.isDone && ['Highest','High'].includes(t.priorityName))
    .map(t => t.priorityName + ': ' + t.description)
    .join('\n') || 'No high priority tasks';
})()"

Tasks by folder:

obsidian eval code="(async()=>{
  const plugin = app.plugins.plugins['obsidian-tasks-plugin'];
  const data = await plugin.loadData();
  const excluded = (data.globalQuery||'').split('\n').map(l=>l.match(/^\s*folder does not include\s+(.+?)\s*$/i)).filter(Boolean).map(m=>m[1]);
  const groups = {};
  plugin.cache.getTasks()
    .filter(t => !excluded.some(f => t.taskLocation.path.startsWith(f)))
    .filter(t => !t.isDone)
    .forEach(t => {
      const folder = t.taskLocation.path.split('/').slice(0,-1).join('/') || '/';
      groups[folder] = (groups[folder]||0)+1;
    });
  return Object.entries(groups).sort((a,b)=>b[1]-a[1]).map(([f,c])=>c+' '+f).join('\n');
})()"

Tasks due this week:

obsidian eval code="(async()=>{
  const plugin = app.plugins.plugins['obsidian-tasks-plugin'];
  const data = await plugin.loadData();
  const excluded = (data.globalQuery||'').split('\n').map(l=>l.match(/^\s*folder does not include\s+(.+?)\s*$/i)).filter(Boolean).map(m=>m[1]);
  const start = window.moment().startOf('week');
  const end = window.moment().endOf('week');
  return plugin.cache.getTasks()
    .filter(t => !excluded.some(f => t.taskLocation.path.startsWith(f)))
    .filter(t => !t.isDone && t.due.moment && t.due.moment.isBetween(start, end, null, '[]'))
    .sort((a,b) => a.due.moment.valueOf() - b.due.moment.valueOf())
    .map(t => t.due.formatAsDate() + ' ' + t.description)
    .join('\n') || 'No tasks due this week';
})()"

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.