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:
:q Datalog blocks — full power, copy-paste snippets below Prepare launch checklist
BT_attrDue:: [[July 18th, 2026]]
BT_attrProject:: [[Website Refresh]]
BT_attrWaitingFor:: [[Alex]]
BT_attrContext:: [[Deep Work]]
Two facts drive everything below:
TODO (or DONE) page.BT_attrDue, BT_attrProject, …) and the value page.
Dates were always [[page refs]]; project, waiting-for and context values
are written as [[page refs]] too since the page-ref consistency release
(setting: Write Project/Waiting/Context as page links). Tasks last
edited before that release may still hold plain-text values — re-saving the
attribute (or just editing it once) upgrades it.All snippets use the default attribute names (
BT_attr*). If you renamed attributes in settings, substitute your names.
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.
`` 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.
Notes:
bt_search
Extension Tools API; the list live-updates as tasks change on the page, and
the ↻ button forces a fresh read (useful for edits made in another tab —
Roam’s cross-tab sync can lag ~30 seconds).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:
}
}
}
:q Datalog snippetsRoam’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
:qblock 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/andms/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:
(refs-page "TODO" ?task) binds
every TODO block in the graph before anything narrows it; on a large graph
the query exceeds Roam’s :q timeout and reports “No results” — a
passing-looking failure. Start from the attribute child (there are few of
them), then join up to the task.pull. A (pull ?task [...]) in :find renders as
[object Object] in the :q results table. Bind ?s (the block string)
and ?uid instead.: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]]
: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.
: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.
: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]]
: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.
: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]]
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]]
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.