thcThought Control

Query language

Every term thc q understands, from status and dates to ancestors, relations, sorting, grouping and vaults.

One small language finds things everywhere: thc q on the command line, the Tasks filter (f), saved views, contexts, and the sections of Today. It compiles to a single SQL query over the local store, so it answers in well under a millisecond.

$ thc q 'status:open due<=+7d sort:due'
edx71   [ ] Summarize unread newsletters into [[Reading List]]  due tomorrow · ◆ claude · in today's journal
ykwkc   [ ] Collect last quarter's metrics  due thu · in Draft Q4 OKRs
ny82x   [ ] Call dentist to reschedule #health  due fri · !high · in today's journal
pa3e6   [ ] Review draft with the team  due sun · in Draft Q4 OKRs
fk5x2   [ ] Draft Q4 OKRs  today · due Oct 12 (in 7d) · !high · in Q4 Planning

Quote the query, so your shell leaves !, #, < and parentheses alone. The examples on this page come from a sample vault.

Combining terms

WriteMeans
a bBoth. Terms are joined by AND unless you say otherwise. and is allowed but optional
a or bEither
( … )A group
-a, -( … )Not: a leading - negates a term or a group

AND binds tighter than OR, so #work !high or #urgent means (#work !high) or #urgent.

$ thc q 'status:open (due<=+3d or !high) -#someday sort:due' --explain
query    status:open (due<=+3d or !high) -#someday sort:due
means    open tasks
         and (due on or before Thu Oct 8, or priority high)
         and not tagged #someday
         sorted by due date, undated last
matches  4 · 0.1 ms

A term the language doesn’t know is an error, never a silent empty list. It exits 6 with a suggestion:

$ thc q 'status:opn'
thc: invalid: unknown status "opn" · did you mean open?

Status

TermMatches
status:opentodo, doing or waiting
status:closeddone or cancelled
status:todo status:doing status:waiting status:done status:cancelledExactly that status
status:anyAny task (anything with a status)
status:noneNot a task
status!=doneAny status but that one

A task isn’t a kind of note: it’s any note with a status. Remove the status and it’s plain text again.

Dates

Compare a date field with = (or :), <, <=, >, >= or !=, against any date form: today, fri, +3d, -1w, eom, nov 1, 2026-10-10. Comparisons are by day, so due=fri matches anything due on Friday, at any time.

FieldIs
dueThe hard deadline
sched, scheduledThe planned or start date
done, done_atWhen it was completed
createdWhen the note was made
updatedWhen it last changed
journalThe journal day a note sits in: journal=today, journal>=-7d
happensAny of: scheduled, due, or an alert’s time
TermMatches
due<=+3dDue in the next three days, or overdue
due=todayDue today
sched<=todayScheduled for today or earlier: ready to start
done>=-7dFinished in the last week
created>=-1wMade in the last week
happens<=+7dAnything with a date in the next week (scheduled, due or an alert)
due:none, sched:none, happens:noneNo such date
due:any, sched:any, happens:anyHas one
$ thc q 'happens<=+7d -is:task'
qz8ze   Dentist appointment [[Health]]  tomorrow 14:00 · in today's journal

Tags, priority and text

TermMatches
#workTagged work
-#somedayNot tagged someday
!high !med !lowThat priority. The long form is priority:high (or prio:high); priority!=low works too
lisbonA bare word: in the text or the title
text:lisbonThe same, said explicitly
"lisbon flat"A phrase
title:"Q4 Planning"A title, exactly (case doesn’t matter)

Kinds: is:

TermMatches
is:taskAnything with a status
is:pagePages
is:journalJournal days
is:inboxInbox items: top-level, no title, not a day
is:eventA note with a time and no task status (at:"tue 2pm")
is:repeatingHas a repeat
is:overdueOpen, and due before today
is:alertHas an alert (a reminder)
is:conflictHas an open conflict: two versions waiting for you
is:blockedSomething open blocks it
is:blockingIt blocks something open
is:readyThe next thing to pick up: todo or doing, scheduled for today or earlier (or not scheduled), and nothing open blocking it
is:to-reviewAn agent marked it done and you haven’t accepted that yet
is:image / is:fileAn attachment: a picture, or any other file
is:attachmentEither

Where it lives

Each of these takes an id (any unique prefix) or a page title. Quote a title with spaces.

TermMatches
under:<page>, in:<page>Anywhere under it, at any depth
parent:<page>Directly under it
ancestor:( … )Under something that matches the inner query
has:childHas children
has:child( … )Has a child that matches the inner query
$ thc q 'under:"Q4 Planning"'
fk5x2   [ ] Draft Q4 OKRs  today · due Oct 12 (in 7d) · !high · in Q4 Planning
ykwkc   [ ] Collect last quarter's metrics  due thu · in Draft Q4 OKRs
pa3e6   [ ] Review draft with the team  due sun · in Draft Q4 OKRs
584x8   Theme for the quarter: fewer, deeper bets  in Q4 Planning

$ thc q 'ancestor:(#work) status:open sort:due'
ykwkc   [ ] Collect last quarter's metrics  due thu · in Draft Q4 OKRs
pa3e6   [ ] Review draft with the team  due sun · in Draft Q4 OKRs
fk5x2   [ ] Draft Q4 OKRs  today · due Oct 12 (in 7d) · !high · in Q4 Planning

ancestor:(#work) finds every open task anywhere inside a page tagged #work, however deep. has:child(status:open) finds the projects that still have something open.

Relations and people

TermMatches
blocks:<id>Blocks that note (thc link a b --rel blocks: a blocks b)
blocked-by:<id>Blocked by that note
embeds:<id>The notes that show that attachment
rel:<name>Has a link of that kind: rel:relates, or one of your own
by:claudeMade by that agent. by:human (or by:me) for you
$ thc q 'by:claude'
edx71   [ ] Summarize unread newsletters into [[Reading List]]  due tomorrow · ◆ claude · in today's journal

Your own fields

thc set <id> client=acme adds a field; its type is fixed on first use (text, number, date or true/false). Query it by name:

TermMatches
client=acmeText, case doesn’t matter
hours>=3Numbers compare as numbers
renewal<=eomDates compare as dates
billable=trueTrue or false

Saved views: @name

A saved view is a query with a name. Use it as a term, and add more:

$ thc view add work 'status:open #work sort:due'
$ thc q '@work !high' --explain
query    @work !high
@work    (status:open #work sort:due)
means    open tasks
         and tagged #work
         and priority high
         sorted by due date, undated last

A view can use other views; one that refers to itself is refused. A view with sections (like Today) runs on its own: thc q @today. See Every view.

Sorting: sort:

TermOrder
sort:dueDue date, undated last (ties by scheduled date)
sort:scheduledScheduled date
sort:dateScheduled or due, whichever is set
sort:priorityHigh first
sort:statusBy status
sort:titleA to Z
sort:createdOldest first
sort:updatedLeast recently changed first
sort:done_atCompletion date

Add - for the reverse: sort:updated- is most recently changed first. Several sort: terms sort by the first, then the next. With none, results come by date (scheduled or due), then most recently changed.

Grouping: group:

group: changes how results are laid out, never which ones match.

TermSections
group:parentBy the page or day each one lives in
group:tagBy tag (a note with two tags shows under both)
group:statusBy status
group:dueOverdue, today, tomorrow, this week, later, no due date
group:actorBy who made it
group:vaultBy vault (with vault:)
$ thc q 'is:task group:due'
Tomorrow  1
  edx71   [ ] Summarize unread newsletters into [[Reading List]]  due tomorrow · ◆ claude · in today's journal

This week  3
  ykwkc   [ ] Collect last quarter's metrics  due thu · in Draft Q4 OKRs
  ny82x   [ ] Call dentist to reschedule #health  due fri · !high · in today's journal
  pa3e6   [ ] Review draft with the team  due sun · in Draft Q4 OKRs
…

With --json, a grouped query returns groups[].

Vaults: vault:

By default a query reads the current vault. vault: at the top level widens it:

TermReads
vault:acmeThat vault
vault:(acme or personal)Those vaults
vault:*Every registered vault
vault:* -vault:sideAll but one

Rows from more than one vault say which they came from, and in JSON each carries vault.

Explain, the past, and output

FlagDoes
--explainSays what the query means, with relative dates resolved, and how many match. Runs no listing
--explain=sqlAdds the SQL it compiles to
--as-of <when>Runs against the vault as it was then: --as-of=-2d, --as-of 2026-09-30
--jsonMachine-readable: {count, items[], …}
--fields id,short,dueOnly those fields (implies --json)
--limit <n>At most n rows (default 50)
$ thc q 'sort:due' --limit 2 --fields id,short,due,status
{"count":2,"items":[{"id":"edx71eyt9xsq","short":"edx71","status":"todo","due":"2026-10-06"},{"id":"ykwkcpyfwf8p","short":"ykwkc","status":"todo","due":"2026-10-08"}],…}

--as-of is possible because nothing is ever overwritten: thc replays the log up to that moment and asks the question there. “What was open last Monday?” is one flag.

Quick reference

status:open | todo | doing | waiting | done | cancelled | closed | any | none
due<=+3d   sched=today   done>=-7d   due:none   created>=-1w   updated>=-1d   journal=today
#tag   -#tag   !high   priority:med   text:word   title:"…"   bare words   "a phrase"
is:task | page | journal | inbox | event | repeating | overdue | alert | conflict
   | blocked | blocking | ready | to-review | image | file | attachment
under:<id|"Title">   parent:<id|"Title">   ancestor:( … )   has:child   has:child( … )
blocks:<id>   blocked-by:<id>   embeds:<id>   rel:<name>   by:claude   by:human   <field>=<value>
happens<=+7d   @view   a b (and)   a or b   ( … )   -term   -( … )
sort:due | scheduled | date | priority | status | title | created | updated | done_at   (add - to reverse)
group:parent | tag | status | due | actor | vault
vault:acme   vault:(a or b)   vault:*   -vault:side