You’re reading the ZotLit v2 docs. Still on v1? Read the v1 docs

ZotLit

How to query your Zotero library

Use ZotLit Query to find papers, check files, count by tag or year, and export reading marks from the Obsidian CLI.

Experimental feature

ZotLit Query CLI is experimental. Parameters, output schemas, and diagnostic reports may change between releases.

ZotLit Query answers questions about the papers, files, and reading marks in your Zotero libraries. Use it for a list, a count, or an export that takes several steps in Zotero's own search.

Before you start

You need Obsidian desktop with ZotLit loaded and the Obsidian CLI activated. Run the commands from a terminal.

Add vault=<vault-name> after obsidian when more than one vault is open, for example obsidian vault=Research zotlit:query.

How ZotLit Query works explains the records, filters, and groups behind these steps.

Run a query

Choose the record that you count

Set from to the kind of record that each result row represents:

  • Use from=items for papers, books, and other works. This is the default.
  • Use from=attachments for files and links, such as PDFs and saved web pages.
  • Use from=annotations for reading marks: highlights, notes, images, and ink.

To find papers that have no PDF, count papers: use from=items and look at their files. To find broken files, count files: use from=attachments.

Run the question

This command lists papers tagged to-read with their year and first creator:

obsidian zotlit:query filter='tags.contains("to-read")' fields='title,date.year,creators[0].fullName' limit=20

Put the filter value in single quotes and the text inside it in double quotes. Put fields and group in single quotes when they contain [ and ].

Read the answer

A successful answer has ok: true and rows, or groups for a grouped query. Read warnings before you accept an empty result.

truncated: true means that more records matched than the answer shows. The default limit is 100 rows. Add limit=all to return every match.

A failed answer has ok: false. Read diagnostic.report, follow its last line, and run the corrected command.

Filter by a Collection or Tag

A filter matches Collection paths and Tag names exactly, including letter case. List the exact names first with zotlit:query-values, then use them in the filter.

For example, you want the papers in your thesis Collection that have no highlight yet.

List the matching Collections

obsidian zotlit:query-values kind=collections match=thesis

match keeps the names that contain the text, in any letter case. The answer lists the paths for each library (shortened):

{
  "ok": true,
  "values": [
    {
      "library": "personal",
      "name": "My Library",
      "totalCount": 2,
      "values": ["Query thesis", "Query thesis/Chapter 1"]
    }
  ]
}

A path joins each level with /, so Query thesis/Chapter 1 is a subcollection of Query thesis.

Use the exact path in the query

obsidian zotlit:query filter='collections.within("Query thesis") && annotations.filter(value.type == "highlight").isEmpty()' fields=title,citationKey

collections.within matches the Collection and every subcollection below it. Use collections.contains to match one Collection only.

A path that no library holds gives no rows and an unknown-collection warning. For example, collections.within("query thesis") has the wrong letter case. The answer suggests the exact path (shortened):

{
  "ok": true,
  "returnedCount": 0,
  "rows": [],
  "warnings": [
    {
      "code": "unknown-collection",
      "message": "Collection path \"query thesis\" was not found in the selected Libraries. List the Collection paths with zotlit:query-values kind=collections.",
      "suggestions": ["Query thesis"]
    }
  ]
}

When another library holds the path, the suggestions also include a library value, such as library=group:4815. Add it to the query to read that library.

Tags work the same way. List them with kind=tags, and add library=all to list the tags of every library:

obsidian zotlit:query-values kind=tags match=method

Each Tag value has a name and a type: manual for a tag that you added, or auto for a tag that Zotero added automatically. Use the name in a filter, such as tags.contains("methods").

Count papers, files, or marks

Add group to count records by one value. The answer has one group for each value. Each group has its value, its count, and its first rows up to limit. totalCount is the number of matching records.

Count papers per year, with one example paper in each group:

obsidian zotlit:query group=date.year limit=1 fields=title,date.year

count is exact before the limit, so limit=1 keeps the answer short. To count many groups, such as every tag in a library, also add fields='[]'. Each row then holds only the record's key:

obsidian zotlit:query group='tags[].name' fields='[]' limit=1

The answer looks like this (shortened, rows left out):

{
  "ok": true,
  "totalCount": 3,
  "groups": [
    { "value": "methods", "count": 2 },
    { "value": "review", "count": 1 },
    { "value": null, "count": 1 }
  ]
}

[] groups by each element of a list. A paper with the tags methods and review counts in both groups, so the group counts can add up to more than totalCount. Papers with no tag form the null group.

Other lists work the same way:

QuestionCommand
Papers per Collection inside "Query thesis"obsidian zotlit:query group='collections[]' filter='collections.within("Query thesis")' fields='[]' limit=1
Papers per file typeobsidian zotlit:query group='attachments[].fileType' fields='[]' limit=1
Files per file typeobsidian zotlit:query from=attachments group=fileType
Papers per libraryobsidian zotlit:query group=library library=all limit=3

A paper with two PDF files counts once in the pdf group.

To count marks, start from Annotations. This command returns the three most recent marks on each paper:

obsidian zotlit:query from=annotations group=item.indexedKey sort=-dateModified limit=3 fields=text,item.title

limit applies inside each group. The group's count gives the number of marks on that paper.

Check files and their locations

fileType sorts files into four kinds: pdf, epub, web for snapshots and links to web pages, and other.

Start from Attachments to find linked files that are missing on this computer:

obsidian zotlit:query from=attachments filter='linkMode == "linked_file" && !exists' fields=item.title,path

Find papers with no PDF that ZotLit can open on this computer:

obsidian zotlit:query filter='attachments.filter(value.fileType == "pdf" && value.exists).isEmpty()' fields=title,citationKey

List each paper with the kind and location of every file:

obsidian zotlit:query filter='tags.contains("to-read")' fields='title,library,attachments[].fileType,attachments[].path'

attachments[].path returns one location for each file of the paper, in the same order as attachments[].fileType. A link to a web page has no location on this computer, so its path is null.

These fields identify a record and its library:

FieldValue
keyThe Zotero key of the record, such as ABCD2345. Two libraries can hold the same key.
indexedKeyThe key that names one record across all libraries: ABCD2345 in My Library, ABCD2345g4815 in group 4815.
librarypersonal for My Library, or group:<groupID> for a group library.

Use indexedKey to select the files of one paper:

obsidian zotlit:query from=attachments filter='item.indexedKey == "ABCD2345"' fields=title,fileType,path

Replace ABCD2345 with the paper's Indexed Key. Explore template data shows how to copy it.

Compute a value with a formula

A formula computes one value for each record, in the same language as filter. Pass formulas as a JSON object in formulas: each key is a name, and each value is an expression. Read a formula as formula.<name> in fields, in filter, or in another formula.

This command lists the papers from 2020 on, with the year and the number of creators of each:

obsidian zotlit:query formulas='{"year":"date.year","authors":"creators.length"}' filter='formula.year >= 2020' fields='title,formula.year,formula.authors' limit=3

Each row holds the formula values under their paths (shortened):

{
  "ok": true,
  "returnedCount": 3,
  "truncated": true,
  "warnings": [],
  "rows": [
    {
      "indexedKey": "AAAAAAAA",
      "values": {
        "title": "Alpha of the personal library",
        "formula.year": 2024,
        "formula.authors": 3
      }
    },
    {
      "indexedKey": "BBBB2222",
      "values": {
        "title": "Within-library duplicate, first item",
        "formula.year": 2020,
        "formula.authors": 1
      }
    },
    {
      "indexedKey": "CCCC3333",
      "values": {
        "title": "Within-library duplicate, second item",
        "formula.year": 2020,
        "formula.authors": 1
      }
    }
  ]
}

A formula name has letters, digits, and _, and starts with a letter or _. Letter case counts.

A formula that no field, filter, or other formula uses does not run. The answer then has an unused-formula warning that names it.

A mistake in a formula fails the query. diagnostic.location.path names the formula, and diagnostic.report marks the mistake. For example, formula.yaer + 1 names a formula that the request does not define (shortened):

{
  "ok": false,
  "diagnostic": {
    "code": "unknown-formula",
    "report": [
      "In formula.next: formula.yaer reads the Formula \"yaer\", which the request does not define.",
      "formula.yaer + 1",
      "^^^^^^^^^^^^",
      "Read a Formula of the request: formula.year, formula.next."
    ]
  }
}

A formula that works for most records can fail on one record, such as number(volume) on a volume that is text. That value is an error object with a code and a message. Error Values describes it.

Export a large result

An answer in the terminal holds at most 1 MB. For a complete export, add output with the absolute path of a new file in an existing folder:

obsidian zotlit:query limit=all fields='title,citationKey,date.year,creators[0].fullName' output="$HOME/Documents/zotlit-export.json"

ZotLit writes the complete answer to that file. The terminal shows the counts and file.path in place of the rows. ZotLit keeps an existing file intact, so choose a new name for each export.

To open the result in a spreadsheet such as Excel, add format=csv, or format=tsv for tab-separated text:

obsidian zotlit:query limit=all fields='title,citationKey,date.year,creators' format=csv output="$HOME/Documents/zotlit-export.csv"

The file has a header row, then one row for each result: its Indexed Key, then each field in the order of fields. A grouped query adds a group column first. Each cell holds a value as text, so a list of creators reads Ada Lovelace, Alan Turing. The header and the cells keep the names that the query uses: a column is named by its field, such as date.year, and an item type reads as its Zotero name, such as journalArticle. CSV and TSV need output, because the terminal answer is always JSON.

Cancel a long query

Give a long query a name with id when you start it:

obsidian zotlit:query id=export-1 limit=all fields='[]' output="$HOME/Documents/zotlit-keys.json"

To stop it, run this command in a second terminal:

obsidian zotlit:query-cancel id=export-1

The answer has cancelRequested: true when the query was running. The first terminal then shows Error: The query 'export-1' was cancelled by zotlit:query-cancel. and ZotLit removes the unfinished export. cancelRequested: false means that the query had already finished.

An id has 1 to 128 letters, digits, ., _, or -.

Ask an AI agent

The zotlit-query agent skill lets an AI agent, such as Claude Code or Codex, run these commands for you. Install it with the other ZotLit skills, as Install the ZotLit skill describes. Then ask your question in plain language:

Use the ZotLit query skill to count the papers in my thesis collection by tag.

The agent reads the live guide and schema from ZotLit, lists the exact Collection names, runs the query, and reports the counts with their scope.

To read the full guide yourself, run obsidian zotlit:query-guide. Add a topic, such as topic=group, to print one section.

See also

Last updated on

On this page