better-tasks

Querying Better Tasks — Cookbook

Better Tasks stores everything as native Roam blocks, which means every native query surface in Roam can see your tasks. This cookbook covers the three ways to query them, from easiest to most powerful:

  1. `` — an interactive task list with a simple syntax (no Datalog needed)
  2. Native `` — Roam’s built-in query component
  3. :q Datalog blocks — full power, copy-paste snippets below

How tasks are stored (30-second recap)

 Prepare launch checklist
  BT_attrDue:: [[July 18th, 2026]]
  BT_attrProject:: [[Website Refresh]]
  BT_attrWaitingFor:: [[Alex]]
  BT_attrContext:: [[Deep Work]]

Two facts drive everything below:

All snippets use the default attribute names (BT_attr*). If you renamed attributes in settings, substitute your names.


1. `` — the easy way

Type this in any block:


Better Tasks replaces Roam’s plain button with a live task list: native checkboxes (completing spawns the next occurrence of recurring tasks), inline pills, a result count, and a refresh button.

Syntax

`` alone shows your 20 most relevant open tasks. Add space-separated key="value" filters:

Key Values Example
status TODO (default), DONE, all status="DONE"
project project name (brackets optional) project="Website Refresh" or project=[[Website Refresh]]
due overdue, today, upcoming, this-week, none, YYYY-MM-DD, YYYY-MM-DD..YYYY-MM-DD due="overdue"
completed today, this-week, last-24-hours, last-7-days, ISO date or range completed="last-7-days"
blocked blocked, actionable blocked="actionable"
assignee free text assignee="Sam"
query free-text search across title and metadata query="quarterly report"
limit 1–200 (default 20) limit=50
sort due (earliest first, undated last) sort="due"

Values with spaces need quotes (single or double) — except bare [[Page Title]] refs, which are read to the matching brackets. Unknown keys and malformed values render an inline error naming the problem, so typos never silently return wrong results.

Examples






Notes:


2. Native ``

Because attribute values are page refs, Roam’s native query component discovers Better Tasks with no extension involvement:

}

How to read the results: Roam’s native queries match a block when the conditions hit the block or its ancestors. The block that satisfies all three conditions here is the attribute child (BT_attrProject:: [[Website Refresh]], whose parent task references TODO) — so results show the attribute rows, with the task visible in each result’s breadcrumb/parent context. Including [[BT_attrProject]] narrows matches to genuine Better Tasks metadata rather than any block that happens to mention the project.

This is native Roam behaviour with native pros (works everywhere, saved queries, no learning curve if you already use queries) and cons (attribute rows instead of task rows). When you want task rows with checkboxes and pills, use `` instead.

More native examples:

}
}
}

3. :q Datalog snippets

Roam’s :q blocks accept Datalog plus Roam-specific additions — dnp/ symbols (dnp/today, dnp/this-week-start), ms/ time symbols (ms/today-start, ms/-14D-start) and built-in rules like (refs-page ?title ?b) and (refs-dnp-between ?start ?end ?b). These additions are documented at https://roamdocs.fyi/help/roam-specific-q-additions.md (they are not in the core Datascript docs). Type each snippet into a block starting with :q.

Keep each query on ONE line. Roam splits a multi-line paste into separate blocks, which leaves the :q block holding only the first line — it then reports “Invalid query. Please make sure you’re either passing a query vector or a ref to a block with it”. Datalog ignores whitespace, so the one-liners below are the whole query. (If you want a query formatted across lines for readability, paste it into a child block and reference it: :q ((block-uid)).)

Verify in your graph first. The dnp/ and ms/ symbols are Roam features that evolve; if a snippet returns nothing, check the roamdocs page for the current symbol names.

Every snippet joins child → parent: it matches the attribute child block and returns the task block. That join is what native `` can’t express. All of them only find tasks whose attribute values are page refs — tasks last written before the page-ref release still hold plain text and will not match until re-saved.

Two conventions in every snippet below, both learned the hard way:

Tasks in a project

:q [:find ?s :where (refs-page "BT_attrProject" ?c) (refs-page "Website Refresh" ?c) [?t :block/children ?c] (refs-page "TODO" ?t) [?t :block/string ?s]]

Overdue (due on or before today)

:q [:find ?s :where (refs-page "BT_attrDue" ?c) (refs-dnp-between "January 1st, 2020" dnp/today ?c) [?t :block/children ?c] (refs-page "TODO" ?t) [?t :block/string ?s]]

The window includes tasks due today; Better Tasks itself treats those as “due today”, not overdue. Tighten the start date to taste.

Due this week

:q [:find ?s :where (refs-page "BT_attrDue" ?c) (refs-dnp-between dnp/this-week-start dnp/this-week-end ?c) [?t :block/children ?c] (refs-page "TODO" ?t) [?t :block/string ?s]]

Respects Roam’s week-start; Better Tasks has its own first-day-of-week setting for its UI, so the two can differ by design.

Waiting on a person

:q [:find ?s :where (refs-page "BT_attrWaitingFor" ?c) (refs-page "Alex" ?c) [?t :block/children ?c] (refs-page "TODO" ?t) [?t :block/string ?s]]

Stalled-ish: open tasks created more than 14 days ago

:q [:find ?s :where (refs-page "BT_attrDue" ?c) [?t :block/children ?c] (refs-page "TODO" ?t) (created-between ms/-365D-start ms/-14D-start ?t) [?t :block/string ?s]]

This approximates by creation time. The dashboard’s Stalled filter uses last edit time (:edit/time) — the dashboard filter (or `` plus the Stalled chip) is the more accurate tool for this job.

Completed in a project (audit trail)

:q [:find ?s :where (refs-page "BT_attrProject" ?c) (refs-page "Website Refresh" ?c) [?t :block/children ?c] (refs-page "DONE" ?t) [?t :block/string ?s]]

Want the block uid too?

Add it to the find spec — handy for building block refs from results:

:q [:find ?uid ?s :where (refs-page "BT_attrProject" ?c) (refs-page "Website Refresh" ?c) [?t :block/children ?c] (refs-page "TODO" ?t) [?t :block/uid ?uid] [?t :block/string ?s]]

Appendix: roam/render + the Extension Tools API (power users)

Better Tasks exposes bt_search (and 15 other tools) on window.RoamExtensionTools["better-tasks"] — see the README’s Extension Tools API section for the full argument reference. The registry entry has a tools array; each tool carries an async execute(args).

Try it in the browser console first:

const bt = window.RoamExtensionTools["better-tasks"];
const search = bt.tools.find((t) => t.name === "bt_search");
const result = await search.execute({ due: "overdue", max_results: 10 });
console.log(result.tasks.map((t) => t.text));

A roam/render component can do the same (sketch — adapt to your setup):

(defn bt-overdue []
  (let [results (r/atom nil)]
    (fn []
      (when (nil? @results)
        (let [bt   (aget (.-RoamExtensionTools js/window) "better-tasks")
              tool (->> (array-seq (.-tools bt))
                        (filter #(= (.-name %) "bt_search"))
                        first)]
          (-> (.execute tool (clj->js {:due "overdue" :max_results 10}))
              (.then #(reset! results (js->clj % :keywordize-keys true))))))
      [:div
       (for [task (:tasks @results)]
         ^{:key (:uid task)} [:div (:text task)])])))

Caveats, and why is the supported path instead: `roam/render` is gated behind a Roam setting ("custom components"), the component lives as user-editable code in your graph, and invoking it embeds a graph-specific block uid. Verify the console snippet works in your graph before wiring the component. Use this route when you want a custom rendering can’t do.


Part of Phase 10 (Ecosystem & Insights). See also: README → “Querying Better Tasks”, the dashboard’s saved views, and the bt_search / bt_export Extension Tools.