How ZotLit Query works
How ZotLit Query reads papers, files, and marks, and how filters, groups, and exact names shape the answer.
Experimental feature
ZotLit Query CLI is experimental. Parameters, output schemas, and diagnostic reports may change between releases.
ZotLit Query answers questions about your Zotero libraries from the Obsidian CLI. You, or an agent that works for you, run one command, zotlit:query, and receive the answer as JSON.
Three kinds of records
Your Zotero library holds three kinds of records that matter for research questions. ZotLit Query calls each kind a Query Dataset, and from selects one:
from | One record is | Examples |
|---|---|---|
items | A paper, book, or other work | A journal article, a thesis, a web page |
attachments | A file or link that belongs to it | A PDF, an EPUB, a saved snapshot |
annotations | A mark that you made in that file | A highlight, a note, an image, ink |
from=items is the default. Standalone files and records in the trash are outside all three datasets.
Each dataset connects to the others in both directions:
attachments ──► annotations ──►
Paper ───────────────────── File ───────────────────── Mark
(items) ◄── item (attachments) ◄── attachment (annotations)
Paper ── annotations ──► every mark on all of its files
Mark ── item ─────────► the paper that the mark belongs toA paper lists its files in attachments and all its marks in annotations. A file names its paper in item and lists its marks in annotations. A mark names its file in attachment and its paper in item. The lists, such as a paper's attachments, are called Relation Lists.
A paper also lists its Zotero child notes in notes, with the title and dates of each note. Notes are not a dataset of their own: you reach them from a paper. For example, !notes.isEmpty() finds the papers that have a child note.
Because the connections run both ways, choose the dataset by the record that you count:
- "Which papers have no PDF?" counts papers: start from
itemsand look at each paper's files. - "Which linked files are missing?" counts files: start from
attachments. - "Show my highlights on one paper" lists marks: start from
annotationsand select by their paper.
What a query does
Every query passes the same stages, in this order:
Target Libraries ──► filter ──► sort ──► limit ──► fields ──► rows
(where to look) (which (order) (how (which
records) many) values)librarynames the Target Libraries that the query reads:personalfor My Library,group:<groupID>for a group library, a list of them, orall. Withoutlibrary, it reads the libraries in the Library scope setting.filteris a condition such astags.contains("to-read"). A record joins the result when the condition is true for it. A condition can look through the Relation Lists, for example at the files of each paper.sortputs the records in order, such as-datefor the newest first.limitsets the most records to return. The default is 100, andlimit=allreturns every match.truncated: truein the answer tells you that more records matched.fieldsnames the values that each row returns, such astitle,date.year. A path with[]reads one value from each element of a list:attachments[].pathgives the location of every file of a paper.
Missing values and Error Values
A filter or a formula can meet two kinds of gaps on one record. ZotLit Query keeps them apart:
number(volume)
volume: "12" ──► 12
volume: (empty) ──► null missing data
volume: "Spring" ──► { "kind": "error", an Error Value
"code": "unparseable-text",
"message": "number() cannot read \"Spring\" as a number." }null means that the record has no data there. A missing field gives null, and so does a calculation on null, such as date.year + 1 on a paper with no date. An empty text given to number(), date(), or duration() also gives null, and so does an index past the end of a list.
An Error Value means that the expression cannot compute a result for this record, although the data is there. These give an Error Value:
- A text that is not empty and that
number(),date(), orduration()cannot read - A comparison such as
<between two different types, such as a number and a text - A value of the wrong type in an operation, for example in a list that mixes texts and numbers
- A result that is not a finite number, such as a division by zero
- An argument out of range, or an index that is not a whole number
- Date arithmetic that leaves the range of dates
In a Query Row, an Error Value has kind: "error", a stable code, and a message. A query view shows a warning icon in the cell, with the message in a tooltip. Copy and CSV export write Error: and the message.
How a filter treats an Error Value
An Error Value passes through every operator, method, and function, so the result of a larger expression is the same error. Where a filter asks true or false, an Error Value is false:
- A filter never selects a record because of an Error Value.
!xis true whenxis null or an Error Value.- An Error Value equals no value, itself included, so
!=is true. isType("error")is true for an Error Value. For example,number(volume).isType("error")selects the papers whose volume is not a number.
A mistake that fails on every record, such as title * 2, stops the whole query before it reads any record. The answer is a failure that names the mistake.
this: a query relative to one paper
A filter can name one paper or one file as this.item or this.attachment. The paper or file is the Query Context: the query does not name it, the caller supplies it.
zotlit:query from=annotations filter='item == this.item' this-item=ABCD2345
└──────┬──────┘ └───────┬───────┘
the same filter the paper that
for every paper this.item meansOn the command line, this-item and this-attachment supply the context. In Obsidian, a query file or query block takes it from the note that shows it, so one filter shows the marks of each Literature Note's own paper.
A filter that reads this without a context cannot run. The answer is a failure that names the missing value.
How grouping counts records
group puts the matching records into groups by one value and counts each group. A query that counts papers by year gives one group per year. Each group has its value, its count, and its first rows up to the limit. totalCount is the number of matching records.
A paper has one year, but it can have several tags. With [], as in group='tags[].name', a paper joins one group for each of its tags:
Paper A tags: methods, review ──► group "methods" ◄── Paper B tags: methods
──► group "review"
Paper C no tags ──► group null
groups: methods 2, review 1, null 1 totalCount: 3Paper A counts in two groups, so the group counts add up to more than totalCount. A paper with no tag joins the null group. The same rule applies to Collections (collections[]) and to file types (attachments[].fileType).
Exact names come first
Collection paths and Tag names in a filter must match Zotero exactly, including letter case. A Collection path starts at the top-level Collection and joins each level with /, such as Query thesis/Chapter 1.
zotlit:query-values lists the exact names, so a question runs in two steps:
"my thesis collection"
│
▼
zotlit:query-values kind=collections match=thesis
│ returns: "Query thesis", "Query thesis/Chapter 1"
▼
zotlit:query filter='collections.within("Query thesis")'When a filter names a Collection path that no Target Library holds, the query still runs, and no record matches that path. The answer carries an unknown-collection warning with the closest existing paths. When another library holds the path, the warning also suggests the library value that includes it.
Agents, the guide, and the schema
The zotlit-query agent skill teaches an AI agent how to turn a research question into these commands. The skill holds the research workflow. Before it builds a query, the agent reads the details from two live sources in ZotLit:
zotlit:query-guideprints the guide for the ZotLit version that you run. It explains filters, fields, sorting, grouping, results, and cancelling, with tested examples.zotlit:query-schemanames every field of each dataset, the custom fields of your library, and the default values.
Because the agent reads both sources at each start, its queries follow the ZotLit version installed in your vault.
See also
Query your Zotero library
Steps for common research questions, with commands to copy.
Commands
Every ZotLit Query command and parameter.
Query files and query blocks
The same queries as live views in tabs, embeds, and notes.
Install the ZotLit skill
Add the ZotLit agent skills, including zotlit-query, to a vault.
Last updated on