Command reference
Every zotio command, generated directly from the binary. Add --agent to any command for JSON output and non-interactive defaults; mutating commands preview unless --yes is passed.
Read commands emit a {meta, results} envelope in JSON mode; for ordinary list/single-resource reads, results is always a JSON array, even for a single-object read like items get (a one-element array) — index it as results[0] or iterate with results[] uniformly. analytics is in this group too: every mode answers results rows carrying a count, so one jq works across --group-by, --type, and the no-flag resource breakdown. Purpose-built report-shaped diagnostics are explicit exceptions: items audit, journal show, doctor, which, schema drift, capabilities drift, workflow status, reading-list, items duplicates, items citekey-conflicts, items bibcheck, library health, library prisma, and creators audit expose named report fields instead of results. annotations export is also an explicit raw-export exception: its --format markdown and --format json outputs are consumable documents, not read-list envelopes.
A read command may also emit sibling top-level keys beside its own payload, for advisory data it did NOT match. Three commands do this, and they share one naming convention so the grammar is learned once: the rows arrive under near_<selector>_<thing>, the outcome under meta.<selector>_lookup, and the truncation count under meta.near_<selector>_total. items find --title reports near_title_matches (rows {key, title, score, year, item_type, trashed}) with meta.title_lookup; items find --citekey reports near_citekey_matches (rows {cite_key, item_key, title, score}) with meta.citekey_lookup; items duplicates --by title|all reports near_title_groups (rows {group, score, count, keys, titles, item_type, requires_review}, where group is title_near) with meta.title_lookup. Rows are strongest first and the score is rounded to two decimals.
What a perfect score means differs by command, so do not apply one rule to all three. On items find --title 1.00 is unreachable: an equal title is an exact hit, so a near row is always capped below it. On items find --citekey 1.00 is reachable and is the strongest suggestion there is — the library holds that key differing only in case or Unicode encoding. On items duplicates 1.00 is reachable and expected: a fold-equal pair is two stored titles that differ only in case, punctuation or Unicode form, which is exactly the duplicate the equality detector cannot see.
The lookup-state key also differs, and this is the one an agent gets wrong. On items find it describes a lookup a person asked for: exact_hit (a returned row matched on that selector, and .results holds it), near_matches, no_near_matches, or near_lookup_failed; it is ABSENT when the question stayed unanswered, because the selector was not given or another selector matched while this one did not — the selectors are OR-ed and one identifier hit suppresses the report. On items duplicates nothing is looked up, so meta.title_lookup describes ONLY the advisory near pass and can never be exact_hit. The exact answer is the report body in .groups, which is always complete: a run reporting no_near_matches can still hold exact duplicate groups, so read .groups and do not treat that state as a clean library. --by doi reports not_requested, because that detector never asks the title question at all.
items duplicates adds meta.near_title_scan, which says whether the comparison was complete: complete, or common_words_skipped when some title words were carried by too many titles to pair on, so two titles whose only shared words were those were never compared. Read no_near_matches with common_words_skipped as "not everything was compared", never as "nothing is close". The key is absent after near_lookup_failed and for --by doi. A meta.near_<selector>_total key reports how many candidates were scored when more were scored than listed — for citekeys, how many distinct keys, since one key held by three items is one suggestion — and is absent when the list is complete, so "not in these rows" is never mistaken for "not in the library".
A sibling key is never folded into the payload, so .results and .groups keep holding only what the command matched and every existing pipeline is unaffected; these rows are suggestions to confirm, never matches, and items duplicates resolve will not merge a near group. --select and --compact apply to the payload only and never drop a sibling key or meta. Machine formats with no column for a score (--plain, --csv) print a one-line stderr note naming the count and --json; on items duplicates a second note names the words too common to pair on, and unlike the rows note it prints even when no rows were found. --quiet is exempt on a terminal, where the exit code is the whole answer; piped, items find still emits its envelope including the near rows. Under --group all sibling keys are kept per library at libraries[].extra and never merged, because a similarity score only ranks candidates inside the library that produced it — and items duplicates --group all REQUIRES --json (or --agent), exiting 2 otherwise, because its near rows exist only in the JSON report and no other format could carry them into the aggregate.
Global flags
These flags are available on every command.
| Flag |
Type |
Default |
Description |
--agent |
bool |
false |
Set agent-friendly defaults (--json --compact --no-input --no-color); does NOT auto-apply writes — pass --yes to mutate |
--allow-destructive |
bool |
false |
Allow irreversible operations (merge, permanent delete, collection delete, creators rename/audit fix, vault resolve) to apply |
--allow-zotero-cloud |
bool |
false |
Allow stored attachment uploads into Zotero's cloud storage even when Zotero desktop keeps files elsewhere (for example WebDAV) |
--compact |
bool |
false |
Return only key fields (id, name, status, timestamps) for minimal token usage |
--config |
string |
|
Config file path |
--connector-target |
string |
|
Desktop connector save target ID (for example C78); overrides --collection target mapping |
--continue-on-error |
bool |
false |
On bulk mutations, continue past per-item failures/conflicts instead of stopping at the first |
--csv |
bool |
false |
Output as CSV (table and array responses) |
--data-source |
string |
auto |
Data source for read commands: auto (live with local fallback), live (API only), local (synced data only) |
--deliver |
string |
|
Also write output to a sink (output still goes to stdout): file: or webhook:; stdout is the default |
--dry-run |
bool |
false |
Preview writes without sending them; read requests still run |
--group |
string |
|
Operate on a Zotero group library by numeric group ID, or 'all' to fan one read/diagnostic out across the personal library and every accessible group (default: personal library) |
--human-friendly |
bool |
false |
Force colored output (auto-enabled on terminals; NO_COLOR and --no-color still win) |
--idempotent |
bool |
false |
Treat already-existing create results as a successful no-op |
--ignore-missing |
bool |
false |
Treat missing delete targets as a successful no-op |
--json |
bool |
false |
Output as JSON |
--max-changes |
int |
-1 |
Max write operations a single mutation may apply before refusing (-1 = default: 500, or 50 under --agent) |
--max-failures |
int |
0 |
With --continue-on-error, stop after this many failures (0 = unlimited) |
--no-cache |
bool |
false |
Bypass response cache |
--no-color |
bool |
false |
Disable colored output |
--no-input |
bool |
false |
Disable all interactive prompts (for CI/agents) |
--plain |
bool |
false |
Output as plain tab-separated text |
--profile |
string |
|
Apply values from a saved profile (see 'zotio profile list') |
--quiet |
bool |
false |
Print nothing; the exit code is the answer |
--rate-limit |
float64 |
0 |
Max requests per second (0 to disable) |
--select |
string |
|
Comma-separated fields to include in output (e.g. --select id,name,status) |
--timeout |
duration |
30s |
Request timeout |
--via |
string |
auto |
Item-creation route: auto (connector when local+reachable), connector (desktop), or web (api.zotero.org) |
--yes |
bool |
false |
Skip confirmation prompts (for agents and scripts) |
Configuration
zotio loads its configuration from the platform configuration directory, or from the path supplied with --config. zotio doctor reports the active path.
Update checks
Release discovery is off by default. To opt in to one anonymous GitHub releases request per day, add:
The request reads only the public zotio releases feed; it sends no identifying payload, never installs anything, and failures are ignored. zotio doctor reports whether a newer version is available.
zotio agent-context
Emit structured JSON describing this CLI for agents
Outputs a machine-readable description of commands, flags, and auth so
agents can introspect this CLI at runtime without parsing --help or
reading source. Schema is versioned via schema_version.
zotio agent-context [flags]
| Flag |
Type |
Default |
Description |
--pretty |
bool |
false |
indent JSON output for human reading |
zotio analytics
Analyze a synced Zotero library
Analyze locally synced Zotero data.
--type accepts a mirrored resource kind (such as items or collections) for
resource counts. --item-type accepts a Zotero item type (such as journalArticle
or book) to count only matching rows in the items mirror. When --group-by is
supplied, the item rows are grouped by year, itemType, collection, creator, or
tag. --limit caps the groups shown (0 = no limit).
Examples:
# Count all items and only journal articles
zotio analytics --type items
zotio analytics --item-type journalArticle --json
# Group items by publication year or collection
zotio analytics --type items --group-by year
zotio analytics --type items --group-by collection --limit 10 --json
| Flag |
Type |
Default |
Description |
--db |
string |
|
Database path |
--group-by |
string |
|
Group items by year, itemType, collection, creator, or tag |
--item-type |
string |
|
Zotero item type to count or group (journalArticle, book, ...) |
--limit |
int |
25 |
Maximum groups to show with --group-by (0 = no limit) |
--type |
string |
|
Mirrored resource kind to count (items, collections, tags, ...) |
zotio annotations
Search and export annotations from your Zotero library
zotio annotations export
Export annotations as markdown or JSON
Export annotations grouped by the top-level item that owns them. --limit caps
the number of annotations exported, not the number of items scanned.
--collection and --tag are shorthand for --scope collection:KEY and
--scope tag:NAME; together they intersect.
With --data-source auto (the default) the command reads live and serves the
local mirror when Zotero is unreachable. --format json returns the
{meta, results} envelope; under --json or --agent, --format markdown returns
{"format", "content"}. With --output the result goes to the file and stdout
reports what was written; under --dry-run nothing is written.
zotio annotations export [flags]
Examples:
zotio annotations export --collection ABCD2345 --output notes.md
zotio annotations export --scope "collection:ABCD2345 AND tag:to-read" --format json
zotio annotations export --limit 200 --output annotations.md --dry-run
| Flag |
Type |
Default |
Description |
--collection |
string |
|
Only annotations of items in this collection key (same as --scope collection:KEY) |
--format |
string |
markdown |
Output format (markdown or json) |
--limit |
int |
50 |
Maximum number of annotations to export (0 = no limit) |
--refresh |
bool |
false |
Read live only, with no local mirror fallback (same as --data-source live) |
--scope |
string |
|
Item cohort: library | collection:KEY | tag:NAME | item:KEY | query:TEXT | saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
--tag |
string |
|
Only annotations of items with this tag (same as --scope tag:NAME) |
-o, --output |
string |
|
Output file path (default: stdout) |
zotio annotations search
Search annotations by text
Search annotation text, comments and tags.
By default the search uses the local store's full-text index: word stems match
("trust" finds "trusted"), "quoted phrases" match exactly, AND, OR, NOT and
parentheses combine terms, and results are ranked by relevance. Live search has
none of that, which is why this command reads the local store first, unlike
annotations timeline and annotations export, which read live first. With no
synced local store the search runs live instead: each word or "quoted phrase"
must appear, in any letter case, in the annotation's text, comment or tags;
stems and operators do not apply. meta.source and meta.reason say which path
answered.
--data-source local searches the index only. --data-source live (or --refresh)
searches live only.
Results include the key and title of the top-level item each annotation
belongs to. --scope names top-level items, as it does for annotations export
and annotations timeline.
zotio annotations search <query> [flags]
Examples:
zotio annotations search "working memory"
zotio annotations search trust --scope collection:ABCD2345 --color yellow
zotio annotations search "effect size" --data-source local --limit 20
| Flag |
Type |
Default |
Description |
--color |
string |
|
Filter by annotation color (yellow, red, green, blue, purple, orange) |
--limit |
int |
50 |
Maximum number of annotations to return (0 = no limit) |
--refresh |
bool |
false |
Search live only, skipping the local index (same as --data-source live) |
--scope |
string |
library |
Item cohort: library | collection:KEY | tag:NAME | item:KEY | query:TEXT | saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
zotio annotations timeline
List annotations sorted by creation date
List annotations newest first. --item names the top-level item that owns the
annotations, never the attachment that holds them. --collection and --item are
shorthand for --scope collection:KEY and --scope item:KEY.
With --data-source auto (the default) the command reads live and serves the
local mirror when Zotero is unreachable.
zotio annotations timeline [flags]
Examples:
zotio annotations timeline --limit 50
zotio annotations timeline --since 2024-01-01
zotio annotations timeline --item ABCD2345 --json
zotio annotations timeline --scope "collection:ABCD2345 AND tag:to-read"
| Flag |
Type |
Default |
Description |
--collection |
string |
|
Only annotations of items in this collection key (same as --scope collection:KEY) |
--item |
string |
|
Only annotations of this top-level item key (same as --scope item:KEY) |
--limit |
int |
50 |
Maximum number of annotations to return (0 = no limit) |
--refresh |
bool |
false |
Read live only, with no local mirror fallback (same as --data-source live) |
--scope |
string |
|
Item cohort: library | collection:KEY | tag:NAME | item:KEY | query:TEXT | saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
--since |
string |
|
ISO date string; include annotations after this date |
zotio attachments
Attachment file operations
zotio attachments add
Attach a local file to an existing item
Attach a local file (typically a PDF) to an existing item. Mode "stored"
uploads an imported_file child that syncs to all devices. Mode "linked-file"
records the absolute local path without consuming Zotero storage quota; the
bytes remain local to this machine.
A stored upload through the Zotero Web API always lands in Zotero's OWN cloud
storage and is billed against that storage plan. When Zotero desktop keeps files
elsewhere (a personal WebDAV server, or file syncing turned off) that upload is
refused rather than silently misrouted.
--via connector is the local route for that case. Zotero's connector cannot
attach to an item that already exists, so this creates a temporary parent and
the file in one connector session — putting the bytes in whatever file store the
desktop actually uses — then moves the attachment onto your item and trashes the
temporary parent. Moving it relocates no bytes: every storage name derives from
the attachment's own key, not its parent's. Requires Zotero desktop running.
The route is opt-in and never chosen by --via auto, because it creates and
trashes a temporary item in your library. Pass --allow-zotero-cloud instead to
accept the cloud upload.
All routes are retry-safe. Stored files reconcile by filename and registered
MD5, and linked files by absolute path. The connector route reconciles on
content hash alone, because Zotero names the stored file after the parent item
rather than after your file: an identical retry no-ops, a run interrupted
before the move is resumed rather than duplicated, and an empty temporary parent
left by a run interrupted after the move is trashed by an identical retry once
it is more than seven minutes old. Two limits are worth knowing:
a retry issued before Zotero has registered the hash can still add a second
copy, and two simultaneous runs against one item can both add one.
By default this previews the planned attachment; apply with --yes.
zotio attachments add <parent-key> <file> [flags]
| Flag |
Type |
Default |
Description |
--attach-mode |
string |
stored |
Attachment handling: stored (synced) or linked-file (local path) |
--title |
string |
|
Attachment title (default: the file name) |
zotio auth
Manage authentication for Zotero
zotio auth logout
Clear stored credentials
Previews by default: shows what would be cleared and changes nothing. Pass --yes to clear the stored API key from the config file and remove the credentials file.
Examples:
zotio auth logout
zotio auth logout --yes
zotio auth set-token
Save an API token to the config file
zotio auth set-token --stdin [flags]
Examples:
printf %s "$ZOTERO_API_KEY" | zotio auth set-token --stdin
| Flag |
Type |
Default |
Description |
--stdin |
bool |
false |
Read the API token from stdin |
zotio auth status
Show authentication status
Examples:
zotio capabilities
Emit the machine-readable capability + preconditions registry
Outputs a typed registry describing each command's operation kind
(read/write/destructive), write target, supported data sources, and
preconditions (live_local_api, web_api_key, synced_store, better_bibtex,
desktop_connector, zotero_file_storage) so agents can select safe commands
and pre-flight requirements without parsing --help or guessing from names.
Commands with more than one write route carry a routes list where each route
names its own preconditions; the top-level requires and write_target describe
the default route (the first entry in routes).
A destructive command makes a change "journal undo" cannot reverse. A command
that writes local files you name (an export, a report, a baseline) keeps
operation read and reports write_target local_file.
zotio capabilities [flags]
| Flag |
Type |
Default |
Description |
--pretty |
bool |
false |
indent JSON output for human reading |
zotio capabilities drift
Probe core read endpoints for capability/API drift
Probes core Zotero read endpoints declared in the capability registry and
reports any endpoint that now returns an API error or cannot be reached. This is
read-only and treats every non-nil GET error as drift, including HTTP 4xx/5xx
and network failures.
zotio collections
Manage collections in your Zotero library
Manage collections in your Zotero library.
This group covers the collections themselves (create, update, move, delete,
list, export); it does not add items to a collection. To add an item, use
'zotio items move --to ' (by key, also --from and --keys-from
for many items at once) or 'zotio items add-to-collection
--collection-name ' (by name, creating the collection if it does not
exist yet).
zotio collections bundle
Write a local research package for a collection
Assemble a self-contained research package from the local store for a
collection: synthesis context, annotations, and compact bibliography. This command
never writes to Zotero; run sync first if local data is missing.
Re-running into the same --output-dir replaces the package; the result names every
existing file it replaced. zotio-bundle.json is written last and lists each
file's size and SHA-256: the package is complete only while that file is
present and matches. With --dry-run nothing is written; the result lists the
files that would be written and replaced.
zotio collections bundle <collectionKey> [flags]
Examples:
zotio collections bundle ABCD2345 --output-dir ./research/ethics
zotio collections bundle ABCD2345 --output-dir ./research/ethics --dry-run
| Flag |
Type |
Default |
Description |
--output-dir |
string |
|
Output directory for the research package |
zotio collections create
Create one or more collections
zotio collections create [flags]
Examples:
zotio collections create --name "Reading list" --parent-collection ABCD2345
| Flag |
Type |
Default |
Description |
--name |
string |
|
Collection name |
--parent-collection |
string |
|
Parent collection key (omit for top-level) |
--stdin |
bool |
false |
Read request body as JSON from stdin |
zotio collections delete
Delete a collection (does not delete items)
zotio collections delete <collectionKey>
Examples:
zotio collections delete ABCD2345
zotio collections export
Export a collection (and subcollections) as BibTeX, RIS, CSL-JSON, or item JSON
Recursively walks a collection and all its subcollections, then emits a
single combined export in the requested format. Use --flat to export only
the top-level collection without recursing into subcollections.
bibtex, ris, and csljson are rendered by Zotero's export translators, so they
need Zotero desktop running. --format json emits the structured item JSON the
synced mirror already holds, so it also works with Zotero closed
(--data-source local, or auto once the local API stops answering).
The collection must exist: an unknown key exits 3 before anything is written.
Under --json or --agent, bibtex and ris output is wrapped as
{"format": ..., "content": ...}. With --output the file is replaced atomically
and the result reports whether it replaced an existing file; with --dry-run
nothing is written, not even a lock file, and the result reports what would be.
zotio collections export <collectionKey> [flags]
Examples:
# Export collection as BibTeX (default)
zotio collections export ABCD2345
# Export as RIS to a file
zotio collections export ABCD2345 --format ris -o refs.ris
# Preview the file write without creating it
zotio collections export ABCD2345 -o refs.bib --dry-run
# Export the whole subtree's item JSON from the synced mirror, Zotero closed
zotio collections export ABCD2345 --format json --data-source local
# Export without descending into subcollections
zotio collections export ABCD2345 --flat
| Flag |
Type |
Default |
Description |
--flat |
bool |
false |
Export only the top-level collection, skip subcollections |
--format |
string |
bibtex |
Export format: bibtex, ris, csljson, json (json is the offline-capable format) |
--page-size |
int |
100 |
Items fetched per API request (max 100); the export always walks every page |
-o, --output |
string |
|
Write output to this file instead of stdout (replaced atomically) |
zotio collections gaps
Find highly cited DOI references missing from a collection's library
zotio collections gaps <collectionKey> [flags]
Examples:
zotio collections gaps ABCD2345 --limit 100 --top 10
| Flag |
Type |
Default |
Description |
--limit |
int |
50 |
Maximum DOI-bearing collection items to scan (0 = no limit) |
--top |
int |
20 |
Number of citation gaps to show (0 = no limit) |
zotio collections get
Get a specific collection
zotio collections get <collectionKey>
Examples:
zotio collections get ABCD2345
zotio collections items
List all items in a collection
zotio collections items <collectionKey> [flags]
Examples:
zotio collections items ABCD2345 --limit 25
| Flag |
Type |
Default |
Description |
--format |
string |
|
Response format (json, bibtex, ris, etc.) |
--limit |
int |
0 |
Maximum number of items to return (0 = no limit) |
--start |
int |
0 |
Pagination offset |
zotio collections list
List all collections
zotio collections list [flags]
Examples:
zotio collections list --limit 50
| Flag |
Type |
Default |
Description |
--limit |
int |
0 |
Maximum number of collections to return (0 = no limit) |
--start |
int |
0 |
Pagination offset |
zotio collections move
Move a collection under a new parent
zotio collections move <collectionKey> --to <parentKey> [flags]
Examples:
zotio collections move ABCD2345 --to EFGH6789
zotio collections move ABCD2345 --to root
| Flag |
Type |
Default |
Description |
--to |
string |
|
New parent collection key (use root or empty string for top-level) |
zotio collections stats
Show analytics for a collection (item count, PDF coverage, year range, top journals)
zotio collections stats <collectionKey> [flags]
Examples:
zotio collections stats ABCD2345 --top 10
| Flag |
Type |
Default |
Description |
--top |
int |
5 |
Number of top venues to show (0 = no limit) |
zotio collections subcollections
List subcollections of a collection
zotio collections subcollections <collectionKey>
Examples:
zotio collections subcollections ABCD2345
List tags used within a collection
zotio collections tags <collectionKey> [flags]
Examples:
zotio collections tags ABCD2345 --query ethics
| Flag |
Type |
Default |
Description |
--limit |
int |
0 |
Maximum number of tags to return (0 = no limit) |
--query |
string |
|
Filter tags by name substring |
--query-mode |
string |
contains |
Tag query mode: contains or startsWith |
--start |
int |
0 |
Pagination offset |
zotio collections top
List only top-level collections (no parents)
zotio collections top [flags]
Examples:
zotio collections top --limit 20
| Flag |
Type |
Default |
Description |
--limit |
int |
0 |
Maximum number of collections to return (0 = no limit) |
zotio collections update
Update a collection
zotio collections update <collectionKey> [flags]
Examples:
zotio collections update ABCD2345 --name "Reading list"
| Flag |
Type |
Default |
Description |
--name |
string |
|
New collection name |
--parent-collection |
string |
|
New parent collection key (false or empty for top-level) |
--stdin |
bool |
false |
Read request body as JSON from stdin |
zotio creators
Audit creator names across your Zotero library
zotio creators audit
Audit synced creator names for variant candidates
Audit synced creator names for variant candidates without mutating Zotero.
The audit reads item creators from the local synced store and groups likely name
variants into three confidence tiers: exact-after-normalization, initial/full-name
compatibility, and ambiguous same-surname diagnostics. Ambiguous groups are
hidden unless --include-ambiguous is supplied; when shown, they are review-only
and never include runnable rename commands. Use --orcid to fetch CrossRef author
ORCIDs for DOI-bearing tier-2/3 candidates and store them in the local
creator_orcids sidecar table as corroboration evidence. That sidecar is
local-only evidence; zotio never writes ORCIDs back to Zotero because Zotero has
no creator ORCID field.
zotio creators audit [flags]
Examples:
zotio creators audit
zotio creators audit --scope collection:ABCD1234
zotio creators audit --orcid --json
| Flag |
Type |
Default |
Description |
--include-ambiguous |
bool |
false |
Include unsafe same-surname groups as review-only evidence; no rename commands are emitted |
--orcid |
bool |
false |
Fetch CrossRef author ORCIDs into the local-only sidecar table; never writes ORCIDs to Zotero |
--scope |
string |
library |
Item cohort: library | collection:KEY | tag:NAME | item:KEY | query:TEXT | saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME |
zotio creators audit fix
Apply safe creator variant renames from creators audit
Apply safe creator variant renames from creators audit through the shared
mutation preview/apply envelope.
By default this previews item writes without sending network mutations. Pass --yes
to apply. Tier-1 exact-normalized groups are planned automatically. Tier-2
initial/full-name groups are planned only for explicit repeatable mappings such
as --map "J. Smith=John Smith". Tier-3 ambiguous groups are never planned.
Creator renames PATCH each affected item with its full creators array and a
version precondition, preserving creatorType, creator order, and unrelated
creators. Applied creator renames are journaled for audit history, but journal
undo does not reverse them because ordered creator-array inverses are not
supported.
zotio creators audit fix [flags]
Examples:
# Previews also need --allow-destructive to show ok:true
zotio creators audit fix --allow-destructive
zotio creators audit fix --map "J. Smith=John Smith" --allow-destructive
zotio creators audit fix --scope collection:ABCD2345 --allow-destructive --yes
| Flag |
Type |
Default |
Description |
--map |
stringArray |
[] |
Tier-2 alias mapping alias=canonical; repeatable |
--scope |
string |
library |
Item cohort: library | collection:KEY | tag:NAME | item:KEY | query:TEXT | saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME |
zotio creators rename
Rename a creator display name across matching items
Rename a creator's display name across every item in scope that carries it.
This is the fix half of creators audit: run creators audit first to find variant
groups, then apply the rename it suggests (or any alias/canonical pair of your
own choosing) with this command.
The rename PATCHes each affected item with its full creators array and a
version precondition, preserving creatorType, creator order, and unrelated
creators. It handles both creator shapes: firstName/lastName and a single name
field.
zotio creators rename --from <oldName> --to <newName> [flags]
Examples:
# Without --allow-destructive the preview is refused (ok:false, destructive_opt_in_required)
zotio creators rename --from "Adam J Rock" --to "Adam J. Rock" --allow-destructive
zotio creators rename --from "Adam J Rock" --to "Adam J. Rock" --allow-destructive --yes
zotio creators rename --from "Adam J Rock" --to "Adam J. Rock" --scope collection:ABCD2345 --allow-destructive
| Flag |
Type |
Default |
Description |
--from |
string |
|
Old creator display name |
--scope |
string |
library |
Item cohort: library | collection:KEY | tag:NAME | item:KEY | query:TEXT | saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME |
--to |
string |
|
New creator display name |
zotio demo
Seed a zero-setup sample library and print a guided tour
Seed a bundled sample library into a separate demo store and print a
short tour of commands to try.
The demo library is a separate SQLite database (demo.db) beside your real
store. Set ZOTIO_DEMO=1 and pass --data-source local to keep reads inside it.
Without that flag, the default auto source can query a running Zotero desktop.
No Zotero desktop and no API key are required for local demo reads.
| Flag |
Type |
Default |
Description |
--reset |
bool |
false |
Delete and re-seed the demo library (also removes demo.db) |
zotio desktop
Report whether Zotero desktop is running, or wait for it to start
zotio desktop status
Report whether Zotero desktop is running and its connector accepts requests
Report whether Zotero desktop is running and whether its connector accepts
requests. Cheap and local: it reads the profile lock of every discovered
Zotero profile and sends one ping to the local connector. It exits 0 whatever
it finds; read the fields, not the exit code.
Two signals, reported separately:
running Zotero's process is up: another process holds the
profile lock (.parentlock via fcntl on macOS and Linux,
parent.lock opened exclusively on Windows), or the
connector answered.
connector_reachable GET /ping answered 200 within 3s during this
check. Imports and every other connector write need
this.
state:
ready the connector answers.
starting the lock is held, the connector does not answer yet, and the
lock is younger than the 2-minute startup window. Zotero
takes the lock about 3s after launch and its connector
listens a few seconds later.
busy the lock is older than the startup window and the connector
port accepted the connection but did not answer in this
check (or answered with an error). One silent check is not
a hang: a large sync can hold Zotero's main thread for
seconds.
unresponsive desktop wait only: the connector stayed silent for at least
60s across 3 or more checks (each allowed 10s): Zotero is
open but not responding.
connector_off the lock is older than the startup window and nothing
listens on the connector port (every address refused
repeated connects): the connector is disabled (Settings ->
Advanced -> "Allow other applications to communicate with
Zotero") or on another port.
stopped neither signal holds.
The lock age comes from the lock file's modification time, which Zotero
resets when it takes the lock (profiles[].lock_since). evidence names the
strongest signal: connector, profile_lock, none.
Profiles are discovered from profiles.ini in the platform's Zotero directory
(ZOTERO_PROFILE_DIR pins one); data_dir comes from the profile's prefs.js.
Examples:
zotio desktop status
zotio desktop status --agent
zotio desktop wait
Block until Zotero desktop's connector accepts requests
Block until Zotero desktop's connector accepts requests, then print the
status that proved it. If the connector already answers, it returns at once.
While Zotero is closed nothing runs on a timer: the command sleeps on
filesystem notifications for the Zotero profile and data directories and
probes only after a change. While Zotero is starting, the connector is
re-checked on a capped backoff (250ms up to 2s), because it listens a few
seconds after the lock and its start writes no file. When the startup window
passes, a connector that cannot take a request is re-checked every 20s with a
10s ping. Any answer ends the wait as ready. 60s without one, across 3 or more
checks, ends it with exit 15: unresponsive if any check found a listener on
the connector port, connector_off if none did (a hung Zotero's listener also
refuses some connects, so one refusal proves nothing). Exit 15 means Zotero is open
but stuck: nothing on disk announces a recovery, so the caller tells the user
and decides when to wait again. While Zotero is starting or busy, filesystem
events cause no extra checks.
Two signals, reported separately:
running Zotero's process is up: another process holds the
profile lock (.parentlock via fcntl on macOS and Linux,
parent.lock opened exclusively on Windows), or the
connector answered.
connector_reachable GET /ping answered 200 within 3s during this
check. Imports and every other connector write need
this.
state:
ready the connector answers.
starting the lock is held, the connector does not answer yet, and the
lock is younger than the 2-minute startup window. Zotero
takes the lock about 3s after launch and its connector
listens a few seconds later.
busy the lock is older than the startup window and the connector
port accepted the connection but did not answer in this
check (or answered with an error). One silent check is not
a hang: a large sync can hold Zotero's main thread for
seconds.
unresponsive desktop wait only: the connector stayed silent for at least
60s across 3 or more checks (each allowed 10s): Zotero is
open but not responding.
connector_off the lock is older than the startup window and nothing
listens on the connector port (every address refused
repeated connects): the connector is disabled (Settings ->
Advanced -> "Allow other applications to communicate with
Zotero") or on another port.
stopped neither signal holds.
The lock age comes from the lock file's modification time, which Zotero
resets when it takes the lock (profiles[].lock_since). evidence names the
strongest signal: connector, profile_lock, none.
Exit codes and the JSON outcome field:
0 outcome "ready": the connector answers.
15 outcome "unresponsive" or "connector_off" (the state): Zotero runs past
its startup window and its connector cannot take requests.
14 outcome "timeout": --timeout passed first; wait again.
9 outcome "no_profile" (no Zotero profile found and the connector does not
answer) or "watch_failed" (the directories could not be watched).
10 the configured base URL is not a local Zotero, so there is no connector
to wait for.
1 interrupted (SIGINT/SIGTERM) or stdin closed under --watch-stdin; nothing
is printed on stdout.
--timeout replaces the global request timeout for this command; a connector
ping is bounded to 3s, or 10s for the stall re-checks past the startup window.
zotio desktop wait [flags]
Examples:
zotio desktop wait
zotio desktop wait --agent --timeout 6h
# Supervised: exit when the supervisor's pipe closes
zotio desktop wait --agent --watch-stdin --timeout 1h
| Flag |
Type |
Default |
Description |
--timeout |
duration |
0s |
Give up after this long and exit 14, e.g. 30m or 6h (0 = wait indefinitely) |
--watch-stdin |
bool |
false |
Exit when stdin reaches end of file (for supervisors that hold a pipe open); input is discarded |
zotio doctor
Check CLI health
Check CLI health.
When [updates].check is enabled, doctor also checks zotio's public GitHub
releases feed at most once a day.
Examples:
zotio doctor
zotio doctor --json
zotio doctor --fail-on stale
| Flag |
Type |
Default |
Description |
--ensure-live |
bool |
false |
Check the Zotero desktop local API is reachable; exit 9 if not. Prints {"status":"reachable"} under --json instead of the full report |
--fail-on |
string |
|
Exit non-zero when a health level is reached: stale (exit 12 when the cache is stale, 11 on an error) or error (exit 11). Default is never. |
--launch |
bool |
false |
With --ensure-live, launch Zotero and wait up to 15s for the local API |
zotio export
Export data to JSONL or JSON for backup, migration, or analysis
Export paginated API data to a local file. Output defaults to JSONL
(one JSON object per line, streaming-friendly); JSON output is available for
backwards-compatible resource exports.
zotio export <resource> [id] [flags]
Examples:
# Export all items as JSONL (streaming, recommended for large datasets)
zotio export items --output data.jsonl
# Export with limit
zotio export items --limit 1000
# Pipe to another tool
zotio export items | jq '.key'
| Flag |
Type |
Default |
Description |
--format |
string |
jsonl |
Output format: jsonl or json |
--limit |
int |
0 |
Maximum records to export (0 = no limit) |
-o, --output |
string |
|
Output file path (default: stdout) |
zotio export snapshot
Reproducible, resumable paginated export with a content manifest
Export every page of a library, collection, or tag scope as JSONL, BibTeX,
RIS, or CSL-JSON. A sidecar manifest (
Scope uses the shared --scope grammar: library (default), collection:KEY,
tag:NAME, or collection:KEY AND tag:NAME. The data file and the manifest are
each published atomically (a temporary file renamed into place), so a failed
run keeps the previous snapshot. A JSONL run keeps its in-progress data in
.partial until it finishes.
zotio export snapshot [scope] [flags]
Examples:
zotio export snapshot --output backup.jsonl
zotio export snapshot --format bibtex --output library.bib
zotio export snapshot collection:ABCD2345 --format ris --output coll.ris
zotio export snapshot "collection:ABCD2345 AND tag:to-read" --output subset.jsonl
zotio export snapshot --format csljson --output library.json --resume
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--format` | `string` | `jsonl` | Snapshot format: jsonl, bibtex, ris, or csljson |
| `--limit` | `int` | `0` | Maximum items to export (0 = no limit) |
| `--page-size` | `int` | `100` | Items per API page (1-100) |
| `--resume` | `bool` | `false` | Resume an interrupted snapshot from its checkpoint sidecar |
| `-o, --output` | `string` | | Output data file (required); the manifest is written to .manifest.json |
#### `zotio export snapshot verify`
Verify a snapshot manifest against the current library
Verify a snapshot manifest against the current Zotero library.
The verifier re-resolves the manifest scope when possible, recomputes the same
content hash as export snapshot, and separates semantic drift (added, removed,
changed) from Zotero version churn (touched). Touched items have a newer version
but identical normalized content and never fail --fail-on-drift.
zotio export snapshot verify <manifest.json> [flags]
Examples:
zotio export snapshot verify backup.jsonl.manifest.json
zotio export snapshot verify library.bib.manifest.json --fail-on-drift
zotio export snapshot verify backup.jsonl.manifest.json --json
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--fail-on-drift` | `bool` | `false` | Exit 11 when added, removed, or changed items are detected (touched never fails) |
## `zotio feedback`
Record feedback about this CLI (local by default; upstream opt-in)
Feedback is captured locally first at ~/.zotio/feedback.jsonl.
When `ZOTERO_FEEDBACK_ENDPOINT` is set and either --send is
passed or `ZOTERO_FEEDBACK_AUTO_SEND=true`, the entry is
POSTed as JSON after the local write.
The entry, stored locally and POSTed unchanged, holds exactly these
fields: text, cli ("zotio"), version, timestamp (UTC), and agent_id
only when --agent-id is passed. No environment variables, config,
credentials, or library data are added.
--send makes delivery required. If the endpoint is unset or unusable
the command exits 10; if the POST fails it exits 5. The local entry is
kept in both cases. An automatic send (ZOTERO_FEEDBACK_AUTO_SEND) stays
best-effort: a failure is a warning and the command exits 0. Results
and errors name only the endpoint's scheme and host.
Write what surprised you or tripped you up, not a bug report. The
loop is: agent notices friction -> one invocation -> captured -> the
maintainer sees it.
zotio feedback [text] [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--agent-id` | `string` | | Identifier to record (and send) as agent_id; omitted unless passed |
| `--send` | `bool` | `false` | POST to the configured feedback endpoint in addition to local write; exits non-zero if the POST does not happen |
| `--stdin` | `bool` | `false` | Read feedback body from stdin rather than arguments |
### `zotio feedback list`
List recent feedback entries
zotio feedback list [flags]
Examples:
zotio feedback list
zotio feedback list --limit 5
zotio feedback list --json
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `20` | Maximum number of recent entries to return (0 = no limit) |
## `zotio groups`
Discover Zotero group libraries you can access
### `zotio groups inspect`
Check whether a Zotero group library is accessible and writable
Probes one group library. Without --quiet the command exits 0 when the probe
itself works: "found": false means the group is not accessible (not a member,
or the API key lacks access), not that the command failed. Read "found",
"ready_for_read" and "ready_for_write" in the output. With --quiet nothing is
printed; the exit code is 3 when the group is not found, 9 when it is not
ready for write, and 0 otherwise.
zotio groups inspect <group-id>
Examples:
zotio groups inspect 1234567 --json
### `zotio groups list`
List the group libraries the configured user belongs to
Examples:
zotio groups list
zotio groups list --json
# Then sync and search a specific group:
zotio sync --group <id>
zotio search --group <id> <query>
## `zotio import`
Import data from JSONL file via API create/upsert calls
Import data from a JSONL file by issuing one POST request per record.
Each line must be a valid JSON object. Failed records are logged to stderr
but do not stop processing the import.
The import previews by default and writes only under --yes; --dry-run always
wins over --yes. Every parsed record counts against --max-changes.
zotio import <resource> [flags]
Examples:
# Preview an import without sending anything (the default)
zotio import <resource> --input data.jsonl
# Apply the import
zotio import <resource> --input data.jsonl --yes
# Import from stdin
cat data.jsonl | zotio import <resource> --input - --yes
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `-i, --input` | `string` | | Input JSONL file path (use - for stdin) |
### `zotio import apply`
Apply a reviewed import manifest
Apply a reviewed import manifest, optionally attaching each entry's file.
--attach-mode stored routes two different ways. For an entry that CREATES its
item, --via connector hands the item and its file to Zotero desktop in one
session, so Zotero files the bytes wherever it is configured to — including a
personal WebDAV server. Every other stored case (attaching to an item that
already exists, or a create that resolves to the Web route) uploads through the
Zotero Web API, which always lands in Zotero's own cloud storage; that upload is
refused when the desktop keeps files elsewhere, unless --allow-zotero-cloud.
An entry with no DOI or source URL records the file's own file:// URI as the
attachment's provenance, so the local path becomes item metadata and syncs.
By default this previews the planned changes; apply with --yes.
--via connector hands work to the running Zotero desktop, which surfaces its
own progress UI; zotio cannot dismiss it and the connector protocol exposes no
endpoint that closes or completes a save session. Observed 2026-08-22: roughly
78 consecutive one-per-item invocations left Zotero unresponsive with progress
windows accumulating; no proven mechanism has been established. One invocation
therefore saves every connector-bound stored create in one session, as import
file --via connector and items create do; in a run with no destination, an
entry whose item names its own collection still gets its own session. Because
the batch commits together, a failed entry does not stop the entries after it,
a canceled run still reports every entry the session already saved, and
--max-failures or an explicit --continue-on-error=false is refused. Prefer one
invocation with many records. --rate-limit governs only Web API requests and
does not pace connector calls; pace your own invocations.
--to KEY files every created item into that collection. It overrides
the manifest's "collection" default for this run; an entry's own "collection"
still wins over both. The destination applies to created items, including the
item Zotero's recognizer makes from a "recognize" entry, which its own save
session files once recognition finishes. Attach entries add a file to an item
that already exists and do not re-file it. The key must exist in the synced
local store, or the run is refused before any write. A connector save session
has one target, so on the connector route an entry whose own collection differs
from the run's destination, or whose item already lists another collection, is
refused before any item is created; apply it in a separate run.
zotio import apply <manifest> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--attach-mode` | `string` | `none` | Attachment handling: none, linked-file, or stored |
| `--fetch-pdf` | `bool` | `false` | Attach an open-access PDF via Zotero's desktop resolver (requires --via connector) |
| `--to` | `string` | | Destination collection key to file created items into (overrides the manifest default; attach entries are not re-filed) |
### `zotio import arxiv`
Import a preprint from arXiv metadata
Import a preprint from arXiv metadata.
The item previews by default and is created only under --yes; --dry-run always
wins over --yes.
zotio import arxiv <id> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--fetch-pdf` | `bool` | `false` | Attach an open-access PDF via Zotero's desktop resolver (requires --via connector) |
| `--to` | `string` | | Destination collection key to add the item to |
### `zotio import discover`
Discover missing references and write a reviewable import manifest
zotio import discover [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--direction` | `string` | `backward` | Citation chase direction: backward, forward, or both |
| `--limit` | `int` | `25` | Maximum manifest entries to emit (0 = no limit) |
| `--min-count` | `int` | `2` | Minimum number of source items citing a DOI |
| `--overwrite` | `bool` | `false` | Replace an existing manifest at --output; review edits in it are lost |
| `--scope` | `string` | | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (required) |
| `--to` | `string` | | Default destination collection key recorded in the manifest for created items |
| `-o, --output` | `string` | | Path to write the reviewable import manifest |
### `zotio import doi`
Import an item from DOI metadata (CrossRef, then DataCite)
Import an item from DOI metadata.
The DOI's registration agency is resolved automatically. CrossRef is asked
first; if it reports no such record, DataCite is asked. That covers registrants
CrossRef does not hold, including arXiv's own 10.48550/arXiv.* preprint DOIs,
Zenodo, Dryad, and figshare. A CrossRef outage is not treated as "no record",
so a transient failure is reported rather than being retried at a registry that
does not own the DOI.
The item previews by default and is created only under --yes; --dry-run always
wins over --yes.
zotio import doi <doi> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--fetch-pdf` | `bool` | `false` | Attach an open-access PDF via Zotero's desktop resolver (requires --via connector) |
| `--to` | `string` | | Destination collection key to add the item to |
### `zotio import file`
Import items from BibTeX, RIS, or CSL JSON
Import items from a BibTeX, RIS, or CSL JSON file.
The import previews by default and writes only under --yes; --dry-run always
wins over --yes. Every parsed record counts against --max-changes.
Records are posted in batches, so a record Zotero rejects cannot un-submit the
records sent alongside it. Every record therefore reports its own outcome
instead of the run stopping at the first rejection.
zotio import file <path> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--format` | `string` | | Input format (bibtex, ris, csljson; csljson requires --via connector) |
| `--to` | `string` | | Destination collection key to add imported items to |
### `zotio import isbn`
Import a book from Open Library ISBN metadata
Import a book from Open Library ISBN metadata.
The item previews by default and is created only under --yes; --dry-run always
wins over --yes.
zotio import isbn <isbn> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--fetch-pdf` | `bool` | `false` | Attach an open-access PDF via Zotero's desktop resolver (requires --via connector) |
| `--to` | `string` | | Destination collection key to add the item to |
### `zotio import monitor`
Find new author or topic works and write a reviewable import manifest
Search OpenAlex for works published since a date, then exclude works already in the synced library (by DOI, then by normalized title). The result is a reviewable import manifest; nothing is written to Zotero.
There is no saved state. Run this command on a schedule with an OS scheduler or a workflow spec. Set --since a few weeks before the last run to overlap search windows. Library de-duplication removes works you already imported. OpenAlex publication-date filters cannot reliably find newly indexed or backdated works, so this is not a complete feed of newly indexed records. Review the manifest with import resolve, then write it with import apply. Works without a DOI are counted but omitted because import resolve needs a DOI to fetch their metadata.
zotio import monitor [flags]
Examples:
zotio import monitor --author 0000-0002-1825-0097 --since 2026-01-01 --output new-works.json
zotio import monitor --query "bayesian inference" --author A5023888391 --since 2026-09-01 --output feed.json
zotio import resolve new-works.json > reviewed.json && zotio import apply reviewed.json
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--author` | `stringArray` | `[]` | Author ORCID or OpenAlex author ID (or their https URL); repeat to match any author |
| `--limit` | `int` | `25` | Maximum manifest entries to write (0 = no limit) |
| `--overwrite` | `bool` | `false` | Replace an existing manifest at --output; review edits in it are lost |
| `--query` | `string` | | Free-text topic search; with --author, match both |
| `--since` | `string` | | Required publication date lower bound (YYYY-MM-DD, inclusive) |
| `--until` | `string` | | Publication date upper bound (YYYY-MM-DD, inclusive) |
| `-o, --output` | `string` | | Required reviewable import manifest output path |
### `zotio import pdf`
Create items from PDFs using Zotero desktop recognition
Create Zotero items from local PDF files by asking the Zotero desktop Connector
API to save each PDF as a standalone attachment and run Zotero's metadata recognizer.
This command requires a local Zotero base URL, Zotero running, and Zotero's local API
preference enabled. Recognition failures are reported as unrecognized standalone PDF
attachments instead of hard errors. The plan phase probes the connector before
previewing or applying anything, so a missing desktop fails loudly instead of a
--dry-run silently planning an import that cannot run.
Before importing, each PDF is classified against your synced library the same way
'import scan' does (DOI match). A DOI that already has a copy on file is handled per
--on-duplicate (skip, attach, or create) instead of always minting a second item; a
synced library is required for that check, and its absence only disables the check,
never the import.
--to files the created item into the collection with that key after import.
--collection-name does the same by exact name, creating the collection
when it does not exist (like 'items add-to-collection'). Pass at most one. This
is a separate step, not part of the connector call: the saveStandaloneAttachment
endpoint saves into whatever the desktop pane currently targets and accepts no
collection parameter.
--via connector hands work to the running Zotero desktop, which surfaces its
own progress UI; zotio cannot dismiss it and the connector protocol exposes no
endpoint that closes or completes a save session. Observed 2026-08-22: roughly
78 consecutive one-per-item invocations left Zotero unresponsive with progress
windows accumulating; no proven mechanism has been established. Prefer one
invocation with many PDFs: import file --via connector and items create share
one session, and import apply saves all of its stored creates in one session
per invocation; its recognize and --fetch-pdf entries, like each PDF here,
still use one session each. --rate-limit governs only Web API requests and
does not pace connector calls.
zotio import pdf <path...> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--collection-name` | `string` | | Exact collection name to file the created item into (created if absent) |
| `--on-duplicate` | `string` | `skip` | How to handle a PDF whose DOI already has a copy in the library: skip, attach, or create |
| `--to` | `string` | | Destination collection key to file the created item into |
### `zotio import pmid`
Import a journal article from PubMed metadata
Import a journal article from PubMed metadata.
The item previews by default and is created only under --yes; --dry-run always
wins over --yes.
zotio import pmid <pmid> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--fetch-pdf` | `bool` | `false` | Attach an open-access PDF via Zotero's desktop resolver (requires --via connector) |
| `--to` | `string` | | Destination collection key to add the item to |
### `zotio import resolve`
Resolve PDFs into an editable import manifest
zotio import resolve <dir-or-manifest> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `0` | Scan at most N PDFs when resolving a directory (0 = no limit) |
### `zotio import scan`
Triage a folder of PDFs against your library (read-only): new vs duplicate vs attach-candidate
Scan a directory of PDFs and classify each against your synced library WITHOUT
importing anything. For each PDF it extracts a DOI (from the filename, then the
file's embedded metadata) and reports:
duplicate the DOI already belongs to an item in your library
attach_candidate matches an item you have that is missing its PDF
new not in your library (use 'import doi ' to add it)
unidentified no DOI found (rename the file with its DOI, or add it in Zotero)
This complements Zotero's "Retrieve Metadata for PDF": it makes the library-aware
decision the GUI does not — which PDFs to import, which are duplicates, and which
complete items you already have. It never writes; hand the actual import and file
attach to Zotero.
DOI extraction is dependency-free: it reads the filename and the PDF's uncompressed
embedded metadata (Info/XMP). It does NOT decode compressed page text, so scanned
or text-only PDFs may report "unidentified".
zotio import scan <dir> [flags]
Examples:
zotio import scan ~/Downloads/papers
zotio import scan ~/Downloads/papers --resolve --json
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `0` | Scan at most N PDFs (0 = no limit) |
| `--resolve` | `bool` | `false` | Fetch titles for 'new' PDFs from CrossRef, then DataCite (network) |
### `zotio import targets`
List Zotero desktop Connector save targets
List Zotero desktop Connector save targets for connector-backed imports.
Use the id column (for example C78) with --connector-target when --to is
ambiguous or the local Web API collection key cannot be mapped automatically.
### `zotio import translators`
Detect Zotero desktop translators for a URL
Fetch a page and ask Zotero desktop which web translators match it.
This is diagnostic-only: Zotero's desktop connector can detect matching web
translators, but browser-side web translation is not run by the desktop server.
zotio import translators <url>
### `zotio import url`
Import a URL as a metadata-enriched item
Import a URL, resolving real metadata where possible.
If the URL embeds a DOI, full metadata is fetched from CrossRef. Otherwise the
page's embedded metadata (citation_*, Open Graph, Dublin Core meta tags) is
mapped into a typed item with title, creators, abstract, and publication venue.
A bare webpage item is used only when no metadata is available.
The item previews by default and is created only under --yes; --dry-run always
wins over --yes.
zotio import url <url> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--fetch-pdf` | `bool` | `false` | Attach an open-access PDF via Zotero's desktop resolver (requires --via connector) |
| `--to` | `string` | | Destination collection key to add the item to |
## `zotio init`
Guided first-run setup using doctor, auth, sync, and health checks
Guided first-run setup for Zotero automation.
Checks the local Zotero API, stores an API key when one is provided interactively,
offers the first local sync when the store is missing or empty, finishes with the quick
library-health preset, and offers an opt-in daily check for public zotio releases.
The check sends only a GitHub releases request and no identifying payload.
Without a terminal prompt (--no-input or --agent), the first sync runs only with --yes.
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--launch` | `bool` | `false` | Launch Zotero and wait for the local API when it is not reachable |
## `zotio items`
Manage items in your Zotero library
### `zotio items add-to-collection`
Add an item to a named collection, creating it when needed
Add an item to a top-level collection identified by name, creating the
collection when no collection of that name exists.
Prefer this when you know the collection by name. When you already have the
collection key, or need to move many items at once or remove a membership, use
'zotio items move --to ' (also --from, --keys-from); this command
resolves the name and then applies the same membership change.
The plan holds two operations when the collection has to be created -- a
collection create plus an item membership change -- because two writes happen.
zotio items add-to-collection <itemKey> [flags]
Examples:
zotio items add-to-collection ABCD2345 --collection-name "Reading list"
zotio items add-to-collection ABCD2345 --collection-name "Reading list" --yes
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--collection-name` | `string` | | Exact collection name; create it when absent |
### `zotio items annotations`
List annotation children for an item
zotio items annotations <itemKey> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--annotation-type` | `string` | | Filter by annotation type (highlight, note, image) |
| `--color` | `string` | | Filter by annotation color (yellow, red, green, blue, purple, orange) |
### `zotio items audit`
Audit locally synced items for missing metadata and PDFs
zotio items audit [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--duplicate-attachment-bytes` | `bool` | `false` | List groups of stored attachments that share a Zotero-registered MD5 (candidate duplicates; never deletes) |
| `--limit` | `int` | `0` | Maximum number of items per category (0 = no limit) |
| `--missing-abstract` | `bool` | `false` | List items with no abstract |
| `--missing-citation` | `bool` | `false` | List citeable items missing core citation fields (creators, title, date, venue) |
| `--missing-doi` | `bool` | `false` | List journal articles, conference papers, and preprints with no DOI |
| `--missing-pdf` | `bool` | `false` | List items that should have an attached PDF but do not |
| `--missing-tags` | `bool` | `false` | List items with no tags |
| `--scope` | `string` | | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
| `--verify-files` | `bool` | `false` | Verify each PDF attachment's file exists on disk (one local-API lookup per attachment) |
### `zotio items authors`
Count synced items per creator
zotio items authors [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--collection` | `string` | | Filter to items in this collection key (same as --scope collection:KEY) |
| `--creator-type` | `string` | | Filter by creatorType (for example author or editor) |
| `--limit` | `int` | `20` | Maximum number of authors to return (0 = no limit) |
| `--scope` | `string` | | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
### `zotio items bibcheck`
Check manuscript citation keys against the synced Better BibTeX library
zotio items bibcheck <manuscript...> [flags]
Examples:
zotio items bibcheck paper.tex
zotio items bibcheck manuscript.md --fail-on-unknown
zotio items bibcheck paper.tex chapter.md --json
zotio items bibcheck chapter.qmd --fail-on high
zotio items bibcheck paper.tex --check-retractions --fail-on high
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--check-retractions` | `bool` | `false` | Also look up the DOI of every cited library item at CrossRef (network) and report retracted works (critical) and expressions of concern (high) at the manuscript line that cites them. Off by default; a lookup that fails is reported as skipped and, under --fail-on, exits 9 instead of passing |
| `--fail-on` | `string` | | Exit 11 when findings reach this severity: critical, high, info, any, or none |
| `--fail-on-unknown` | `bool` | `false` | Exit 11 when any cited key is unknown or ambiguous |
| `--follow-includes` | `bool` | `false` | Also check .tex files pulled in by \input{PATH} and \include{PATH}, resolved from the named file's directory; a missing include or a cycle exits 11. Only the braced forms are followed: not \input PATH, \subfile, or \import |
### `zotio items bibliography`
Render or export a bibliography for a scoped item selection
Render or export a bibliography for items selected with the shared scope grammar.
The default bib format uses Zotero's Web API CSL renderer, so named styles such
as apa, chicago, mla, nature, and journal-specific CSL IDs are honored.
Machine-readable csljson, bibtex, biblatex, and ris formats use Zotero's export
translators. CSL-JSON ids are rewritten to unique Better BibTeX citation keys
so Pandoc and Quarto citations resolve against the output.
The Web API limits itemKey batches, so large scopes are fetched in stable
50-key chunks and merged in scope order.
--manuscript selects the items a manuscript cites instead of a scope. It reads
.tex, .md, .markdown, and .qmd files with the items bibcheck parser and
resolves each citation key against the synced Better BibTeX keys. It also reads
Zotero Word (.docx) and LibreOffice (.odt) files: each Zotero citation field
names its items by Zotero URI, and an item that is not in the targeted library
(missing, deleted, trashed, or from another library) counts as unknown. Items appear
once, in first-citation order across the files. --follow-includes also reads
.tex files pulled in by \input{PATH} and \include{PATH}, at each include's
position in the text. BibTeX, BibLaTeX, RIS, and CSL-JSON exports preserve
that order. The bib format follows the selected CSL style's sorting rules.
When any citation key is unknown or ambiguous, or an include is missing or
cyclic, nothing is rendered: the command names each problem with its file
and line and exits 11.
zotio items bibliography [flags]
Examples:
zotio items bibliography --scope collection:ABCD2345 --style apa
zotio items bibliography --scope tag:to-submit --format csljson
zotio items bibliography --scope collection:ABCD2345 --format bibtex
zotio items bibliography --scope item:ABCD2345 --json
zotio items bibliography --manuscript paper.tex --format biblatex
zotio items bibliography --manuscript thesis.tex --follow-includes --format bibtex
zotio items bibliography --manuscript intro.md --manuscript methods.md --format csljson
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--follow-includes` | `bool` | `false` | With --manuscript, also read .tex files pulled in by \input{PATH} and \include{PATH}, resolved from the named file's directory; a missing include or a cycle exits 11 |
| `--format` | `string` | `bib` | Output format: bib, csljson, bibtex, biblatex, or ris |
| `--manuscript` | `stringArray` | `[]` | Select the items cited in this .tex, .md, .markdown, .qmd, .docx, or .odt file instead of --scope (repeatable); an unknown or ambiguous citation key exits 11 |
| `--scope` | `string` | `library` | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME |
| `--style` | `string` | | CSL style ID for --format bib (default uses Zotero's default bibliography style) |
### `zotio items children`
Get child items (attachments and notes) for an item
zotio items children <itemKey> [flags]
Examples:
zotio items children ABCD2345
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--item-type` | `string` | | Filter by type (attachment or note) |
### `zotio items cite`
Get a formatted citation for an item
Retrieve a formatted bibliographic citation for an item. The Zotero local
API renders citations using your installed CSL styles. For the most common
styles (apa, chicago, mla), use the --style flag. For others, pass the
CSL style ID directly (e.g. "nature", "harvard-cite-them-right").
zotio items cite <itemKey> [flags]
Examples:
zotio items cite ABCD2345
zotio items cite ABCD2345 --style apa
zotio items cite ABCD2345 --style bibtex --json
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--style` | `string` | | Citation style (apa, mla, chicago, bibtex, ris, csljson) — default uses Zotero's current default style |
### `zotio items citekey-conflicts`
Find missing and duplicate Better BibTeX citation keys
zotio items citekey-conflicts [flags]
Examples:
zotio items citekey-conflicts
zotio items citekey-conflicts --missing
zotio items citekey-conflicts --conflicts --json
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--conflicts` | `bool` | `false` | Show only items with duplicate citation keys |
| `--missing` | `bool` | `false` | Show only items without a citation key |
### `zotio items collections-of`
Show collections containing an item
zotio items collections-of <itemKey>
### `zotio items create`
Create one or more items
Create one or more items from JSON supplied with --items or --stdin.
This command shares one connector session across the invocation when routed
through --via connector; the Zotero desktop surfaces its own progress UI and
zotio cannot dismiss it — no connector endpoint closes or completes a save
session. Observed 2026-08-22: roughly 78 consecutive one-per-item
invocations left Zotero unresponsive with progress windows accumulating; no
proven mechanism has been established. Prefer one invocation with many items:
import file --via connector and this command share one session, while import
apply currently opens one per manifest entry. --rate-limit governs only Web
API requests and does not pace connector calls.
zotio items create [flags]
Examples:
zotio items create --items '[{"itemType":"journalArticle","title":"A title"}]'
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--items` | `string` | | Array of item objects to create, or a single item object (sent as a one-element array; use item-types and item-type-fields for schema) |
| `--stdin` | `bool` | `false` | Read request body as JSON from stdin (array or single object, as --items) |
### `zotio items delete`
Move an item to the trash
Move an item to the trash, where 'zotio items restore' can bring it back.
This is a reversible trash operation (the item's "deleted" flag is set), matching
what 'zotio items trash' lists and 'zotio items restore' reverses.
--permanent instead destroys the item and its child attachments outright. That
cannot be undone by 'items restore' and requires --allow-destructive.
zotio items delete <itemKey> [flags]
Examples:
zotio items delete ABC12345
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--permanent` | `bool` | `false` | Destroy the item and its attachments outright instead of trashing it (irreversible; requires --allow-destructive) |
### `zotio items duplicates`
Find likely duplicate items in the local store
zotio items duplicates [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--by` | `string` | `all` | Duplicate detector to run (doi, title, all); title and all also report titles that are close but not equal, scored and separately (near_title_groups in JSON, with meta.title_lookup naming the state of that advisory pass and meta.near_title_scan saying whether blocking compared everything), never as duplicate groups and never as something 'duplicates resolve' will merge |
#### `zotio items duplicates resolve`
Resolve duplicate items by merging metadata onto a master and trashing duplicates
Resolve duplicate items by merging metadata onto a master and trashing duplicates.
By default, resolve only matches duplicate DOI groups. Title matching can group
distinct items that share generic titles; pass --title only when you are ready to
review the preview carefully before applying.
zotio items duplicates resolve [flags]
Examples:
zotio items duplicates resolve
zotio items duplicates resolve --doi
zotio items duplicates resolve --title
zotio items duplicates resolve --doi --title --yes --allow-destructive
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--doi` | `bool` | `false` | Resolve duplicate DOI groups |
| `--strategy` | `string` | `keep-most-complete` | Resolution strategy (keep-most-complete) |
| `--title` | `bool` | `false` | Opt into riskier duplicate title groups |
### `zotio items enrich`
Fill or validate item metadata (DOI, abstract, open-access PDF attachment) from CrossRef, OpenAlex, Semantic Scholar, Unpaywall, and OpenCitations
Resolve missing metadata for locally synced items and apply it back to Zotero.
Work queues come from the same checks as 'items audit':
--missing-doi resolve a DOI by title from CrossRef, then OpenAlex/Semantic Scholar (exact title match)
--missing-abstract fill the abstract from CrossRef, then OpenAlex/Semantic Scholar (requires the item's DOI)
--missing-pdf attach an open-access PDF from Unpaywall (requires DOI)
--repair-pdf re-attach an open-access PDF for items whose EXISTING attachment
file is missing or broken on disk (requires DOI and an exact key
set from --keys/--keys-from, since "broken" is a live fact this
command cannot observe locally). Feed it the broken_attachment_file
findings from 'zotio library health --verify-files --json'.
--missing-citation fill the core citation fields from CrossRef (requires DOI):
creators, title, date and the venue field of the item's
own type, plus volume/issue/pages when CrossRef carries
them and the item's copy is blank. A blank volume is only
a defect if the venue actually has one, so those fields
are reported by --validate, not by 'library health'.
--missing-subjects propose OpenAlex subject terms as automatic "concept/"
tags for untagged items (requires DOI). Only concepts
OpenAlex scores at or above 0.50 are proposed, the
strongest six at most, so an untagged library becomes
facetable without burying the operator's own tags.
--missing-mesh propose PubMed MeSH descriptors as automatic "mesh/"
tags for items with a PMID (the PMID field or an Extra
"PMID:" line) or a DOI that PubMed knows. The PMID is read
from the item; a DOI is resolved through PubMed ESearch.
Every MeshHeadingList descriptor is proposed in PubMed's
order; qualifiers and the major-topic marker are not.
Items that already carry a mesh/ tag are left alone, and
a term the item already has is never proposed twice. A
record PubMed has not indexed yet is skipped, so a later
run picks it up. PubMed calls send tool=zotio and the
--email contact, paced to 3 requests per second.
PDF attachment modes:
linked-url create a linked_url attachment (default; no download)
linked-file download the PDF to --pdf-dir and create a linked_file child item; linked files do not sync to other devices
Downloaded PDFs accept application/pdf, application/octet-stream, or an absent
Content-Type, then must pass a %PDF- magic check. Stored (imported-file)
retro-attachment is handled by 'zotio attachments add ', which
uploads through the Zotero Web API file-upload protocol.
By default this previews the proposed changes (a patch plan) and never downloads
PDF bytes. Pass --scope to select the cohort through the shared grammar
(collection:KEY, tag:NAME, item:KEY, query:TEXT, saved-search:KEY); --collection
is the older spelling of --scope collection:KEY and still works. Pass
--keys/--keys-from to scope to exact item keys. Pass --yes to apply via the
Zotero API; --dry-run always previews.
Applied field changes record provenance in the item's Extra field.
zotio items enrich [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--attach-mode` | `string` | `linked-url` | PDF attachment handling: linked-url or linked-file; stored retro-attachment is handled by `zotio attachments add` |
| `--collection` | `string` | | Scope the work queue to items in a collection key |
| `--email` | `string` | | Contact email for Unpaywall (required for --missing-pdf), the OpenAlex polite pool and NCBI PubMed for --missing-mesh (both optional); or set UNPAYWALL_EMAIL |
| `--keys` | `string` | | Comma-separated exact item keys to enrich |
| `--keys-from` | `string` | | Read exact item keys from a file or '-' for stdin, then enrich only matching queued items |
| `--limit` | `int` | `25` | Maximum items to process per category (0 = no limit) |
| `--missing-abstract` | `bool` | `false` | Fill the abstract from CrossRef, OpenAlex, or Semantic Scholar (uses the item's DOI) |
| `--missing-citation` | `bool` | `false` | Fill the core citation fields from CrossRef: creators, title, date, venue, plus volume/issue/pages when the provider has them and the item does not (uses the item's DOI) |
| `--missing-doi` | `bool` | `false` | Resolve and add a DOI from CrossRef, OpenAlex, or Semantic Scholar |
| `--missing-mesh` | `bool` | `false` | Propose PubMed MeSH descriptors as automatic mesh/ tags for items with a PMID or DOI that carry no mesh/ tag yet |
| `--missing-pdf` | `bool` | `false` | Attach an open-access PDF from Unpaywall as a link or download (uses the item's DOI) |
| `--missing-subjects` | `bool` | `false` | Propose OpenAlex subject terms as automatic concept/ tags for untagged items (uses the item's DOI) |
| `--no-openalex` | `bool` | `false` | Disable the OpenAlex fallback for --missing-doi/--missing-abstract |
| `--no-semantic-scholar` | `bool` | `false` | Disable the Semantic Scholar fallback for --missing-doi/--missing-abstract |
| `--pdf-dir` | `string` | | Directory for linked-file PDF downloads; responses must be PDF/octet-stream/unspecified Content-Type plus %PDF- magic |
| `--repair-pdf` | `bool` | `false` | Re-attach an open-access PDF for named items whose existing attachment file is broken or missing on disk (uses the item's DOI; requires --keys/--keys-from/--scope/--collection) |
| `--scope` | `string` | | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
| `--validate` | `bool` | `false` | Read-only DOI discrepancy report against CrossRef and OpenCitations |
### `zotio items file`
Resolve the on-disk path (file:// URL) of an item's attachment
Return the local filesystem location of an item's attachment via the Zotero
local API (/items//file/view/url). Accepts an attachment key directly, or a
regular item key whose PDF attachment is resolved automatically through its child
items.
This complements 'items open' (which launches the Zotero desktop app): 'items file'
hands you the actual file so an agent or script can read or process it. The local
API must be enabled (Settings → Advanced → "Allow other applications…").
zotio items file <itemKey>
Examples:
# Path of an item's PDF
zotio items file ABCD2345
# JSON envelope (item key, resolved attachment key, url, path)
zotio items file ABCD2345 --json
### `zotio items find`
Find locally synced items by identifier, URL, or exact title
Examples:
zotio items find --doi 10.1145/3290605.3300709
zotio items find --isbn 978-0-262-03384-8
zotio items find --url https://example.org/paper
zotio items find --openalex W2741809807
zotio items find --title "Attention Is All You Need"
zotio items find --citekey smith2023 --json
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--arxiv` | `string` | | Find items with this arXiv ID or URL |
| `--citekey` | `string` | | Find items with this Better BibTeX citation key; when the lookup as a whole matches nothing, the closest citation keys are reported separately (near_citekey_matches in JSON) and never as results |
| `--doi` | `string` | | Find items with this DOI |
| `--isbn` | `string` | | Find items with this ISBN |
| `--openalex` | `string` | | Find items with this OpenAlex work ID or URL |
| `--pmid` | `string` | | Find items with this PMID (Zotero's PMID field, or a legacy "PMID:" line in Extra) |
| `--title` | `string` | | Find items with this exact title, ignoring case, whitespace, quote and dash styling, and a trailing full stop; when the lookup as a whole matches nothing, the closest titles are reported separately (near_title_matches in JSON) and never as results |
| `--url` | `string` | | Find items with this normalized URL |
### `zotio items fulltext`
Get full text from an item's PDF attachment
zotio items fulltext <itemKey> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--refresh` | `bool` | `false` | Fetch live from the API instead of the local store |
| `--search` | `string` | | Return only full-text lines containing this string |
### `zotio items get`
Get a single item by key
zotio items get <itemKey> [flags]
Examples:
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--format` | `string` | | Response format (json, bib, bibtex, csljson, ris, etc.) |
### `zotio items list`
List all items in the library
Examples:
zotio items list --limit 20 --item-type journalArticle
zotio items list --query "deep learning" --sort dateModified --direction desc
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--direction` | `string` | | Sort direction (asc or desc) |
| `--format` | `string` | | Response format (json, atom, bib, bibtex, bookmarks, coins, csljson, mods, refer, rdf_bibliontology, rdf_dc,... |
| `--item-type` | `string` | | Filter by item type, for example journalArticle or book (not validated client-side; see 'zotio schema item-types') |
| `--limit` | `int` | `0` | Maximum number of items to return (0 = no limit) |
| `--query` | `string` | | Quick search across title, creator, and year; quote multi-word queries |
| `--since-version` | `int` | `0` | Return only items modified after this library version number |
| `--sort` | `string` | | Sort field (dateAdded, dateModified, title, creator, type, date, publisher, publicationTitle, journalAbbreviation,... |
| `--start` | `int` | `0` | Pagination offset (zero-based) |
| `--tag` | `string` | | Filter by tag name |
### `zotio items missing-pdf`
List items that should have an attached PDF but do not
zotio items missing-pdf [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--collection` | `string` | | Filter to an exact Zotero collection key (same as --scope collection:KEY) |
| `--item-type` | `string` | | Filter to a specific Zotero item type |
| `--keys` | `string` | | Comma-separated exact parent item keys |
| `--limit` | `int` | `0` | Maximum number of items to return (0 = no limit) |
| `--scope` | `string` | | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
### `zotio items move`
Add, remove, or move item collection memberships
Add, remove, or move item collection memberships by collection key.
Use --to to add, --from to remove, and both together to move between collections.
Accepts many item keys, or --keys-from to read them from a file or stdin.
To file an item into a collection you know by name rather than by key (creating
the collection when it does not exist), use 'zotio items add-to-collection
--collection-name '. That command resolves the name to a key and
then delegates the membership change here, so guards and idempotency are shared.
zotio items move [itemKey...] [--to <collectionKey>] [--from <collectionKey>] [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--from` | `string` | | Collection key to remove item from |
| `--keys-from` | `string` | | Read item keys from a file, '-' for stdin, or positional args when omitted |
| `--to` | `string` | | Collection key to add item into |
### `zotio items new`
Create a schema-validated item from Zotero's blank item template
Create a schema-validated item from Zotero's blank item template.
Fields are checked against the item type's template before anything is sent.
The item previews by default and is created only under --yes; --dry-run always
wins over --yes.
zotio items new --item-type <type> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--field` | `stringArray` | `[]` | Item field assignment name=value (repeatable) |
| `--item-type` | `string` | | Zotero item type to create (e.g. journalArticle) |
| `--stdin` | `bool` | `false` | Read a JSON object of item fields from stdin |
| `--to` | `string` | | Collection key to add the item to (e.g. ABCD2345) |
### `zotio items note-template`
Generate a markdown reading-note template for an item
zotio items note-template <itemKey> [flags]
Examples:
zotio items note-template ABCD2345
zotio items note-template ABCD2345 --format obsidian
zotio items note-template ABCD2345 --format logseq --json
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--format` | `string` | `standard` | Template format: standard, obsidian, or logseq |
### `zotio items open`
Print or launch a Zotero desktop deep link (item, collection, or PDF)
Build a zotero:// desktop deep link for an item (default), a collection
(--target collection), or a PDF attachment (--target attachment), then print it or
launch it (--launch) via the OS handler (open on macOS, xdg-open on Linux,
rundll32 on Windows).
The link targets the personal library by default, or a group library when the
global --group flag (or ZOTERO_GROUP) is set. With --agent/--json the
command emits {uri, target_type, library_scope, launched}.
zotio items open <key> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--launch` | `bool` | `false` | Launch the deep link via the OS handler (open/xdg-open/rundll32) |
| `--target` | `string` | `item` | Target type: item, collection, or attachment (PDF) |
### `zotio items preprint-check`
Check arXiv preprints for published CrossRef records
zotio items preprint-check [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `20` | Maximum number of preprints to check |
#### `zotio items preprint-check fix`
Apply published DOIs to preprints that CrossRef reports as published
zotio items preprint-check fix [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `20` | Maximum number of preprints to check |
### `zotio items recent`
List recently added items
zotio items recent [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--days` | `int` | `0` | Return only items added in the last N days (0 = no date filter) |
| `--item-type` | `string` | | Filter by Zotero item type |
| `--limit` | `int` | `20` | Maximum number of items to return (0 = no limit) |
### `zotio items related`
Show outgoing and incoming Zotero related-item edges
zotio items related <itemKey>
Examples:
zotio items related ABCD2345
zotio items related ABCD2345 --json
### `zotio items restore`
Restore a trashed item
zotio items restore <itemKey>
### `zotio items retract-check`
Check DOI-bearing local items for CrossRef retraction notices
Check DOI-bearing items from the locally synced store against CrossRef's
updated-by metadata. The command reports retractions, expressions of concern,
and corrections without writing to Zotero. Run 'zotio sync' first so the local
store is current.
zotio items retract-check [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--collection` | `string` | | Scope checks to items in a collection key (same as --scope collection:KEY) |
| `--limit` | `int` | `0` | Maximum DOI-bearing items to check (0 = no limit) |
| `--scope` | `string` | | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
### `zotio items similar`
Rank locally similar items with explainable signals
Rank locally similar items using five weighted signals: collections (0.30), tags (0.25), fulltext rare-word overlap (0.25), creators (0.10), and an exact, binary venue match (0.10). Run 'zotio sync' first to populate the local mirror and 'zotio sync --fulltext' to include the fulltext signal. --min-score filters the resulting weighted composite score.
zotio items similar <itemKey> [flags]
Examples:
zotio items similar ABCD2345
zotio items similar ABCD2345 --limit 5 --min-score 0.2 --json
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `10` | Maximum number of similar items to return (0 = no limit) |
| `--min-score` | `float64` | `0` | Minimum composite similarity score to include |
### `zotio items stale`
Find old items without PDFs or annotations
zotio items stale [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `50` | Maximum number of items to return (0 = no limit) |
| `--no-annotations` | `bool` | `false` | Include only items without annotation children |
| `--no-pdf` | `bool` | `false` | Include only items without a PDF attachment |
| `--older-than-days` | `int` | `365` | Items added more than this many days ago |
### `zotio items summarize`
Assemble a bounded, synthesis-ready context bundle for an item or collection
Gather the highest-signal local context for an item (or every item in a
collection) into one bounded bundle — citation, abstract, your annotations, a
capped fulltext excerpt, and known metadata gaps — plus a synthesis prompt.
With --focus TERMS, each item instead carries a "focus" section: the passages
that match TERMS in that item's indexed PDF text (one ranked snippet per
attachment, within --max-chars) and in its annotations (within
--max-annotations), each with its item and attachment key. Only the selected
items are searched, and no full PDF text is loaded. An item with no synced PDF
text says so (fulltext: not_indexed) rather than reporting "no match".
This command never calls a model: it does the assembly and budgeting the host LLM
is bad at, then hands off. Reads are local only. With --agent/--json it emits the
structured bundle; otherwise a readable Markdown brief you can paste into any LLM.
zotio items summarize [<itemKey>] [flags]
Examples:
zotio items summarize 9UXV5R7L
zotio items summarize 9UXV5R7L --agent --max-chars 6000
zotio items summarize --collection MAR7RFQN --no-fulltext
zotio items summarize --scope tag:to-read --agent
zotio items summarize --scope collection:MAR7RFQN --focus "transfer learning" --max-chars 2000
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--collection` | `string` | | Summarize every item in this collection key |
| `--focus` | `string` | | Select ranked passages matching these terms from each item's indexed PDF text and annotations, instead of the first-pages excerpt |
| `--max-annotations` | `int` | `40` | Max annotations included per item |
| `--max-chars` | `int` | `8000` | Max characters of fulltext per item: the excerpt, or with --focus the matched PDF-text passages |
| `--no-fulltext` | `bool` | `false` | Omit the fulltext excerpt (abstract + annotations only) |
| `--scope` | `string` | | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
### `zotio items tags`
Get or manage tags for a specific item
zotio items tags [itemKey]
Examples:
zotio items tags list ABC12345
#### `zotio items tags add`
Add one or more tags to items
zotio items tags add --tag <tag> [itemKeys...] [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--automatic` | `bool` | `false` | Write added tags as Zotero automatic tags (type 1) |
| `--batch` | `bool` | `false` | Write up to 50 items per request instead of one request per item. Much faster over large key sets, but every item in a request reaches Zotero together, so the run always continues past a failure: --max-failures and --continue-on-error=false are rejected rather than ignored. Each item still reports its own status and its own conflict. |
| `--keys-from` | `string` | | Read item keys from a file, '-' for stdin, or positional args when omitted |
| `--tag` | `stringArray` | `[]` | Tag to add (repeatable) |
#### `zotio items tags list`
Get tags for a specific item
zotio items tags list <itemKey>
Examples:
zotio items tags list ABC12345
#### `zotio items tags remove`
Remove one or more tags from items
zotio items tags remove --tag <tag> [itemKeys...] [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--automatic-only` | `bool` | `false` | Remove only matching Zotero automatic tags (type 1) |
| `--batch` | `bool` | `false` | Write up to 50 items per request instead of one request per item. Much faster over large key sets, but every item in a request reaches Zotero together, so the run always continues past a failure: --max-failures and --continue-on-error=false are rejected rather than ignored. Each item still reports its own status and its own conflict. |
| `--keys-from` | `string` | | Read item keys from a file, '-' for stdin, or positional args when omitted |
| `--tag` | `stringArray` | `[]` | Tag to remove (repeatable) |
### `zotio items top`
List top-level items only (excludes attachments and notes)
Examples:
zotio items top --limit 20 --item-type journalArticle
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--item-type` | `string` | | Filter by item type |
| `--limit` | `int` | `0` | Maximum number of items to return (0 = no limit) |
| `--query` | `string` | | Quick search query |
| `--sort` | `string` | | Sort field |
| `--start` | `int` | `0` | Pagination offset |
| `--tag` | `string` | | Filter by tag |
### `zotio items trash`
List items in the trash
zotio items trash [flags]
Examples:
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `0` | Maximum number of items to return (0 = no limit) |
| `--start` | `int` | `0` | Pagination offset (zero-based) |
### `zotio items unfiled`
List top-level items not assigned to any collection
List top-level items without collections from the local mirror. With --suggest, rank existing collections using shared tags, creators, and venue against filed items. Each collection scores the mean of its three best member matches (or all members when fewer than three). Each shared tag, creator, or venue splits its vote across the collections its filed items occupy, so a status tag such as "/unread" or a broad journal that spans many collections is weak evidence, and suggestions scoring below --suggest-min-score are dropped. Suggestions are read-only and never move items.
zotio items unfiled [flags]
Examples:
zotio items unfiled --suggest --json
zotio items unfiled --suggest --suggest-limit 5 --item-type book
# File every item whose best suggestion is collection ABCD2345 (preview; add --yes to apply)
zotio items unfiled --suggest --json | jq '[.results[] | select(.suggestions[0].collection == "ABCD2345")]' | zotio items move --keys-from - --to ABCD2345
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--item-type` | `string` | | Filter by Zotero item type |
| `--limit` | `int` | `0` | Maximum number of items to return (0 = no limit) |
| `--suggest` | `bool` | `false` | Suggest collections from local tags, creators, and venue (read-only) |
| `--suggest-limit` | `int` | `3` | Maximum collection suggestions per item (requires --suggest) |
| `--suggest-min-score` | `float64` | `0.05` | Drop collection suggestions scoring below this (requires --suggest) |
### `zotio items update`
Update a specific item
zotio items update <itemKey> [flags]
Examples:
zotio items update ABCD1234 --title "Updated title"
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--abstract-note` | `string` | | Abstract or summary |
| `--collections` | `string` | | Collection keys to assign this item to |
| `--extra` | `string` | | Extra notes field (also stores Better BibTeX citation key) |
| `--stdin` | `bool` | `false` | Read request body as JSON from stdin |
| `--tags` | `string` | | Tags to apply (array of {tag: string} objects) |
| `--title` | `string` | | Item title |
### `zotio items venues`
Count synced items by publication venue
zotio items venues [flags]
Examples:
zotio items venues
zotio items venues --limit 10
zotio items venues --item-type journalArticle --json
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--item-type` | `string` | | Filter by Zotero item type |
| `--limit` | `int` | `20` | Maximum number of venues to return (0 = no limit) |
## `zotio journal`
Inspect the mutation run journal; workflow run --yes steps share an ID filterable with list --workflow
### `zotio journal list`
List recorded mutation runs, newest first
zotio journal list [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `50` | Maximum number of runs to return, newest first (0 = no limit) |
| `--workflow` | `string` | | Filter entries by workflow run ID |
### `zotio journal show`
Show the operations recorded for one mutation run
zotio journal show <run-id>
### `zotio journal undo`
Reverse a recorded run's reversible (tag/collection) changes
zotio journal undo <run-id>
## `zotio library`
Library-wide analytics and reporting
### `zotio library health`
Composite read-only library health report with a CI gate
Run a ranked, finding-typed health report over the locally synced library.
Composes the existing audit primitives (citekey conflicts, duplicates, missing
metadata, tag drift, broken attachments) into one report. Pick what "ready" means
with --for:
--for citation manuscript/bibliography readiness (citekeys, core fields, duplicates)
--for systematic-review PRISMA screening corpus (duplicates, screenable metadata, full text)
--for vault PKM vault sync readiness (stable note filenames from citekeys)
--for quick (default) anything obviously broken (citekey conflicts, duplicates, attachments)
--for all every check
Gate CI with --fail-on critical|high|info (or legacy any) (exit 11 when the bar is not met).
The broken-attachment and retraction checks are live checks that need Zotero
desktop or CrossRef network access respectively; pass --verify-files and/or
--check-retractions to run them. When a gate-relevant check can't run because
its precondition is unmet, the command refuses loudly (exit 9) rather than passing.
zotio library health [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--badge` | `bool` | `false` | Emit a shields.io endpoint JSON badge instead of the report |
| `--badge-label` | `string` | `bibliography` | Label for the shields.io endpoint badge |
| `--baseline` | `string` | | Read health finding identities from this baseline JSON. A missing file is reported as 'baseline: not found' and nothing is diffed; with --fail-on-new it exits 9. Create one with --write-baseline |
| `--check-retractions` | `bool` | `false` | Run the live CrossRef retraction check (network; DOI-bearing items) |
| `--fail-on` | `string` | | Exit 11 if findings reach this severity: critical, high, info/any; none disables the gate (default: the preset's) |
| `--fail-on-new` | `string` | | Exit 11 if new baseline-diff findings reach this severity: critical, high, info/any; none disables (requires --baseline, which must exist: a missing baseline exits 9) |
| `--for` | `string` | `quick` | Check preset: quick, citation, systematic-review, vault, all |
| `--limit` | `int` | `0` | Max findings listed per kind (0 = all); also caps the live attachment scan |
| `--report` | `string` | | Write the full JSON health report to this file in addition to stdout/badge output |
| `--require-fresh` | `duration` | `0s` | Refuse (exit 12) when the local store is staler than this (e.g. 24h); 0 = disabled |
| `--scope` | `string` | | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
| `--verify-files` | `bool` | `false` | Run the live broken-attachment check (needs Zotero desktop running) |
| `--write-baseline` | `string` | | Write current health finding identities to this baseline JSON after checks |
### `zotio library prisma`
Report PRISMA 2020 identification-stage counts from the local store
Report the PRISMA 2020 identification-stage counts for a locally synced screening corpus.
The report counts records identified (including their source databases), duplicate
records removed, and the records-after-deduplication input to screening. Screening
itself is deliberately out of scope: use Rayyan, ASReview, or another screening tool
for that stage. Run 'zotio items duplicates resolve' to actually remove duplicates
before exporting the certified corpus.
zotio library prisma [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--by` | `string` | `all` | Duplicate detector to run (doi, title, all) |
| `--scope` | `string` | | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
### `zotio library stats`
Show library-wide local statistics
zotio library stats [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--added-by` | `string` | | Group item intake by month or year |
| `--top` | `int` | `10` | Number of venues to show (0 = no limit) |
| `--years` | `int` | `20` | Number of years to show (0 = all years) |
### `zotio library wrapped`
Show a local year-in-review for your Zotero library
zotio library wrapped [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--card` | `string` | | Write an 800x418 SVG share card to this path (with --dry-run, report the path and write nothing) |
| `--card-style` | `string` | `overview` | Card layout: overview, rhythm, picks, or cycle (animated crossfade through all three) |
| `--year` | `int` | `2026` | Year to summarize |
## `zotio profile`
Named sets of flags saved for reuse
Profiles capture a set of flag values under a name so a scheduled
agent can invoke the same command with the same configuration each run.
profile save captures the current invocation's set flags
profile use prints the values (for inspection)
profile list lists all saved profiles
profile show shows the values of one profile
profile delete removes a profile
Use --profile on any command to apply that profile's values.
Explicit flags override profile values.
### `zotio profile delete`
Remove a profile
Previews by default: shows which profile would be removed and writes
nothing. Pass --yes to remove it.
zotio profile delete <name>
Examples:
zotio profile delete my-defaults --yes
zotio profile delete old-profile --yes --json
### `zotio profile list`
List saved profiles
Examples:
zotio profile list
zotio profile list --json
### `zotio profile save`
Save the current invocation's non-default flags as a named profile
Captures every flag explicitly set on the invocation and stores
them under . To update an existing profile, run save again; the
entry is replaced, and the output says so ("replaced": true in JSON).
Approval flags (--yes, --allow-destructive, --allow-zotero-cloud) are never
saved: pass them on the command line of each command that writes. A saved
--max-changes applies only when it is stricter than the default cap (500, or
50 under --agent). --dry-run previews the save and writes nothing.
To avoid creating empty profiles, at least one non-default flag must be
present (other than --profile and --config).
zotio profile save <name> [--<flag> <value> ...] [flags]
Examples:
zotio profile save my-defaults --json --compact
zotio profile save tonight-defaults --data-source local
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--description` | `string` | | Short description shown in 'profile list' |
### `zotio profile show`
Show a profile's values as JSON
zotio profile show <name>
Examples:
zotio profile show my-defaults
zotio profile show tonight-defaults --json
### `zotio profile use`
Print the flag values a profile holds (does not apply or execute anything)
Examples:
zotio profile use my-defaults
zotio profile use tonight-defaults --json
## `zotio reading-list`
Show the Zotero reading queue
zotio reading-list [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `20` | Maximum number of items to show (0 = no limit) |
| `--tag` | `string` | `to-read` | Override the reading queue tag |
### `zotio reading-list add`
Add items to the reading queue
zotio reading-list add <itemKeys...|--keys-from F> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--keys-from` | `string` | | Read item keys from a file, '-' for stdin, or positional args when omitted |
### `zotio reading-list done`
Mark reading items as read
zotio reading-list done <itemKeys...|--keys-from F> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--keys-from` | `string` | | Read item keys from a file, '-' for stdin, or positional args when omitted |
### `zotio reading-list start`
Move queued items into the reading state
zotio reading-list start <itemKeys...|--keys-from F> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--keys-from` | `string` | | Read item keys from a file, '-' for stdin, or positional args when omitted |
## `zotio schema`
Zotero item type and field schema
### `zotio schema creator-fields`
List all creator fields (firstName, lastName, name)
zotio schema creator-fields
Examples:
zotio schema creator-fields
### `zotio schema drift`
Detect Zotero schema changes (new/removed item types and fields) vs a saved baseline
Capture a baseline fingerprint of the running Zotero's item-type and field
schema, then on later runs report what changed. Use this after upgrading Zotero
to see which item types, fields, or creator fields a new version added or removed
— the deltas the CLI may not yet model.
The first run captures the baseline. Re-run after an upgrade to see drift. Pass
--update to adopt the current live schema as the new baseline. Deep checks fetch
per-item-type fields and creator types from Zotero in parallel by default. Pass
--db to read those values from an explicitly synced local snapshot instead. The
snapshot must be complete and match Zotero's live schema version. Refresh it in
two steps: 'zotio sync --resources schema', then 'zotio sync --resources
schema-item-type-fields,schema-item-type-creator-types'. The baseline is stored
as schema-baseline.json in the zotio data directory (override with --baseline) and is
shared across libraries because the schema is global to the Zotero install.
zotio schema drift [flags]
Examples:
# Capture a baseline on the current Zotero version
zotio schema drift
# After upgrading Zotero, see what changed
zotio schema drift
# Include live per-item-type field/creator validity
zotio schema drift --deep
# Use an explicitly synced deep snapshot instead
zotio schema drift --deep --db ./data.db
# Re-baseline to the current schema
zotio schema drift --update
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--baseline` | `string` | | Baseline file path (default: schema-baseline.json in the zotio data directory) |
| `--db` | `string` | | Read the deep schema from this explicitly synced database instead of Zotero |
| `--deep` | `bool` | `false` | Also diff per-item-type field and creator-type validity (live by default; use --db for an explicit cache) |
| `--update` | `bool` | `false` | Adopt the current live schema as the new baseline after reporting |
### `zotio schema item-fields`
List all available item fields
Examples:
### `zotio schema item-type-creator-types`
List valid creator types for an item type
zotio schema item-type-creator-types [flags]
Examples:
zotio schema item-type-creator-types --item-type journalArticle
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--item-type` | `string` | | Item type (e.g. journalArticle) |
### `zotio schema item-type-fields`
List valid fields for a specific item type
zotio schema item-type-fields [flags]
Examples:
zotio schema item-type-fields --item-type journalArticle
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--item-type` | `string` | | Item type (e.g. journalArticle) |
### `zotio schema item-types`
List all available Zotero item types
Examples:
### `zotio schema new-item-template`
Get a blank template for creating a new item of a given type
zotio schema new-item-template [flags]
Examples:
zotio schema new-item-template --item-type journalArticle
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--item-type` | `string` | | Item type to get template for (e.g. journalArticle) |
## `zotio search`
Full-text search across synced data or live API
Search data using FTS5 full-text search on locally synced data,
or hit the API's search endpoint when available.
In auto mode (default): uses the API search endpoint if the API has one,
otherwise searches local data. Falls back to local on network failure.
In live mode: uses the API search endpoint only.
In local mode: searches locally synced data only.
Use --fulltext to search synced PDF text and resolve hits to parent items.
With --fulltext, --scope limits hits to one item cohort before ranking and
--limit apply, so a hit outside the cohort never takes a place in the limit.
zotio search <query> [flags]
Examples:
# Search (uses API endpoint if available, local FTS otherwise)
zotio search "error timeout"
# Force local search only
zotio search "payment failed" --data-source local
# Resolve synced PDF full-text matches to their parent items
zotio search "calibration feedback" --fulltext --data-source local
# Search PDF text only inside one collection
zotio search "calibration feedback" --fulltext --scope collection:ABCD1234 --limit 5
# JSON output for piping
zotio search "critical" --json --limit 20
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--db` | `string` | | Database path (default: ~/.local/share/zotio/data.db) |
| `--fulltext` | `bool` | `false` | Search synced PDF full text and return parent item context |
| `--limit` | `int` | `50` | Maximum results to return (0 = no limit) |
| `--scope` | `string` | | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library); requires --fulltext |
## `zotio searches`
Manage saved searches in your Zotero library
### `zotio searches get`
Get a specific saved search
zotio searches get <searchKey>
Examples:
zotio searches get ABCD2345
### `zotio searches list`
List all saved searches
zotio searches list [flags]
Examples:
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `0` | Maximum number of searches to return (0 = no limit) |
### `zotio searches materialize`
Refresh a collection from a saved search, optionally removing stale members
Refresh a collection from a saved search. Add only missing items; report
unchanged and stale members. By default, leave stale members in the collection.
Use --prune to remove stale members; writes still require --yes. Refuse to prune
when no fileable search items would empty a non-empty collection. Child items
cannot be filed; add their parents to the saved search instead.
No search-to-collection binding is stored. For a scheduled refresh, put this
command in refresh.json, then run 'zotio watch --workflow refresh.json --yes'
after each sync (or run 'zotio workflow run refresh.json --yes').
zotio searches materialize <searchKey> --to <collectionKey> [--prune] [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--prune` | `bool` | `false` | Remove collection members absent from the saved search (requires --yes to apply) |
| `--to` | `string` | | Collection key to refresh from saved-search items |
### `zotio searches run`
Run a saved Zotero search when the API exposes results
Executes a saved search and returns the matching items.
Zotero desktop evaluates the search through its local API; that read is
authoritative. Sync mirrors the saved-search definition, so with
--data-source local, or when the desktop cannot answer (Zotero closed, or a
plane without /searches/{key}/items), the definition is evaluated against the
synced items instead. That local evaluation supports title and other text
fields, creator, tag, collection, item type, and date conditions with any/all
join and the noChildren/includeParents/includeChildren flags. A search that
uses any other condition (full text, notes, nested searches, groups, ...) is
never evaluated partially: the command refuses with a precondition_unmet
envelope (exit 9) naming the condition, instead of an empty-looking answer.
Local results come from the last sync and include child items exactly as
Zotero's search does.
zotio searches run <searchKey>
Examples:
zotio searches run ABCD2345
## `zotio sync`
Mirror the Zotero read API into local SQLite for offline search and analysis
Mirror data from the Zotero read API into a local SQLite database. One
direction only: it pulls into the mirror and never writes to Zotero.
The read API is normally the Zotero desktop local API (http://localhost:23119),
NOT api.zotero.org — so this mirrors what the desktop app currently holds, which
lags your cloud library until Zotero itself syncs. 'zotio doctor' names the plane
each cursor came from under cache.resources[].cursor_source. Version numbers are
per-plane, so changing the configured base URL discards the cursor and resyncs.
Supports resumable incremental sync (only fetches new data since the last sync)
and full resync (--full, which also discards stored per-row versions).
Once synced, use the 'search' command for instant full-text search.
Per-item-type schema resources are excluded from the default sync because each
selected resource makes one request per cached item type. Select
schema-item-type-fields or schema-item-type-creator-types explicitly after the
schema resource has populated the item-type list. Each dependent run probes the
live /itemTypes endpoint and exits 9 with remediation when that cached
prerequisite is stale.
Exit codes & warnings:
Resources the API denies access to (HTTP 403, or HTTP 400 with an
access-policy body) are reported as warnings rather than failing the
run. In --json mode each is emitted as a {"event":"sync_warning",...}
line carrying status, reason, and message fields, and a final
{"event":"sync_summary",...} aggregates the run.
A dependent per-item-type schema resource exits 13 when any fan-out request
fails after another request succeeds. A full sync exits non-zero when any
selected resource does not complete. Exit 0 otherwise when at least one
resource synced and no resource flagged in the spec as critical failed. Pass
--strict to exit non-zero on any other per-resource failure. Exit is always
non-zero when every selected resource failed, regardless of --strict.
Examples:
# Sync all resources
zotio sync
# Sync specific resources only
zotio sync --resources collections,items
# Cache per-item-type field validity (one request per cached item type)
zotio sync --resources schema-item-type-fields
# Full resync (ignore previous checkpoint)
zotio sync --full
# Incremental sync: only objects modified since a Zotero library version
zotio sync --since-version 4521
# Parallel sync with 8 workers
zotio sync --concurrency 8
# Latest-only: refresh head of each resource, no historical backfill
zotio sync --latest-only
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--concurrency` | `int` | `4` | Number of parallel sync workers |
| `--db` | `string` | | Database path (default: ~/.local/share/zotio/data.db) |
| `--full` | `bool` | `false` | Full resync (ignore previous checkpoint) |
| `--fulltext` | `bool` | `false` | Also sync PDF full-text content (slower; one request per attachment) |
| `--latest-only` | `bool` | `false` | Refresh head of each resource only; clears resume cursor and caps pages at 1. Mutually exclusive with --since-version (--since-version wins). |
| `--max-pages` | `int` | `100` | Maximum pages to fetch per resource (0 = unlimited; cap-hit emits a sync_warning event) |
| `--resources` | `stringSlice` | `[]` | Comma-separated resource types to sync (items also syncs items-trash; per-item-type schema resources are explicit and cost one request per item type) |
| `--since-version` | `int` | `0` | Only sync objects modified since this Zotero library version (0 = use stored checkpoint). Get versions from a prior sync or 'items list --since-version'. |
| `--strict` | `bool` | `false` | Exit non-zero on any per-resource failure (partial per-item-type schema fan-out always exits 13). |
## `zotio tags`
Manage tags across your Zotero library
### `zotio tags audit`
Audit tags for case and spacing drift
Examples:
zotio tags audit
zotio tags audit --json
zotio tags audit --prefer title
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--prefer` | `string` | `frequency` | Canonical spelling policy for duplicate groups: frequency (default, most-used spelling wins), sentence, title, or lower. Automatic (type 1) tags always keep the frequency canonical to avoid mangling MeSH-style imports. |
#### `zotio tags audit fix`
Apply the tag rename plan produced by tags audit
zotio tags audit fix [flags]
Examples:
zotio tags audit fix --yes
zotio tags audit fix --prefer title --yes
zotio tags audit --json | zotio tags audit fix --keys-from - --yes
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--keys-from` | `string` | | Read item keys from a file or '-' for stdin (the `tags audit --json` finding envelope is accepted directly), then rename tags only on those items |
| `--prefer` | `string` | `frequency` | Canonical spelling policy for duplicate groups: frequency, sentence, title, or lower. Must match the --prefer used for `tags audit` to apply the same targets shown there. |
### `zotio tags get`
Get a specific tag by name
Examples:
zotio tags get "machine learning"
### `zotio tags inventory`
Show tag usage within collections and across the library
zotio tags inventory [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--collection` | `string` | | Show only tags within this collection key |
### `zotio tags list`
List all tags in the library
Examples:
zotio tags list --query machine --limit 20
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `0` | Maximum number of tags to return (0 = no limit) |
| `--query` | `string` | | Filter tags by name substring |
| `--query-mode` | `string` | `contains` | Tag query mode: contains or startsWith |
| `--start` | `int` | `0` | Pagination offset |
### `zotio tags rename`
Rename a tag across matching items
zotio tags rename --from <oldTag> --to <newTag> [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--from` | `string` | | Old tag name |
| `--page-size` | `int` | `100` | Items to fetch per page while selecting carriers of the tag (1-100) |
| `--to` | `string` | | New tag name |
## `zotio tail`
Stream live changes by polling the API at regular intervals
Tail streams live data changes by polling the API at configurable intervals.
Events are emitted as NDJSON to stdout for piping to other tools.
Gracefully shuts down on SIGTERM/SIGINT.
When --workflow is set, tail runs the workflow once after a poll
cycle that emits events. It previews unless this tail invocation carries --yes.
A failed applied run leaves its checkpoint: subsequent applied triggers refuse
until it is resumed or deleted with zotio workflow run --yes --resume.
The cursor advances only after the triggered invocation returns, so a crash
before or during the trigger replays the same batch on the next poll.
A workflow that reads trigger values (${trigger.json:PATH} or "stdin_trigger")
receives the cycle's events, with upserted and deleted keys in separate lists.
It is skipped, with a notice, on the first poll (no stored cursor), which lists
the current records rather than changes, and on a cycle with more than 500
events.
Deletions are reported only when the configured API serves /deleted. The Zotero
desktop local API does not, so against the default local base this feed emits
upserts only, and says so once on the first poll that checks; point base_url
(or ZOTERO_BASE_URL) at the Web API if you need delete events. If a poll cannot
read deletions for any other reason, the cursor is held and that window is
retried, so upserts may repeat.
Note: For APIs with WebSocket or SSE support, a future version will use
native streaming instead of polling.
zotio tail [resource] [flags]
Examples:
# Tail item changes every 10 seconds
zotio tail items --interval 10s
# Tail a specific resource
zotio tail collections --interval 5s
# Pipe to jq for filtering
zotio tail items --interval 30s | jq 'select(.event == "upsert")'
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--db` | `string` | | Database path (default: ~/.local/share/zotio/data.db) |
| `--follow` | `bool` | `true` | Keep running (set --follow=false for single poll) |
| `--interval` | `duration` | `10s` | Poll interval |
| `--resource` | `string` | | Resource type to tail |
| `--workflow` | `string` | | Run this workflow once after an event-bearing poll; previews unless --yes, and failed applied runs require zotio workflow run --yes --resume |
## `zotio vault`
Sync the library into an Obsidian/Logseq Markdown vault
### `zotio vault audit`
Audit managed vault notes without writing
Audit managed vault notes without writing: notes whose Zotero item is gone, notes
that drifted behind the store, notes missing a '## Notes' boundary, and notes on the
old zotero-select link shape.
Run 'zotio library health --for vault' first. A note filename comes from the citation
key, so a missing or duplicated citekey renames or collides notes on the next
'vault push'; this audit only sees the damage afterwards.
zotio vault audit [--vault <dir>] [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--vault` | `string` | | Vault directory (overrides [vault].root + notes_dir from config) |
### `zotio vault conflicts`
List unresolved Zotero note write-back conflicts
zotio vault conflicts [--vault <dir>] [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--vault` | `string` | | Vault directory (overrides [vault].root + notes_dir from config) |
### `zotio vault pull`
Bring remote Zotero child-note edits into the "## Notes" region (fast-forward)
Pull edits made to a managed Zotero child note back into the vault note's
"## Notes" region. This is fast-forward only: a note is updated only when its
local region is unchanged since the last sync. If both the local region and the
remote note changed, it is reported as a conflict and nothing is merged or
overwritten (resolve by hand, or 'vault resolve --keep-vault' to keep the vault
copy). Only notes in the shape this CLI writes are pulled.
Pull previews by default and writes to the vault only under --yes; --dry-run
always wins over --yes. Each note write it plans (a pull or a conflict
artifact) counts as one change against --max-changes.
zotio vault pull [--vault <dir>] [flags]
Examples:
zotio vault pull --dry-run
zotio vault pull --yes
zotio vault pull --yes --vault ~/vault/refs
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--vault` | `string` | | Vault directory (overrides [vault].root + notes_dir from config) |
### `zotio vault push`
Write each note's "## Notes" region back to a Zotero child note
Push the user-owned Notes region of each vault note to a single managed Zotero
child note (Obsidian -> Zotero). Reads stay local; writes go to the Web API (an
api_key must be configured). Idempotent and conflict-safe: a note is only written
when its Notes region changed since the last push, the remote note body is checked
first, and a divergent remote is never overwritten — a conflict artifact is
written under _vault-zotero-conflicts/ and reported instead.
Previews by default; nothing is written until you pass --yes. --dry-run always
wins, even together with --yes. Each note write (create, update, or conflict)
counts against --max-changes.
zotio vault push [--vault <dir>] [flags]
Examples:
zotio vault push --dry-run
zotio vault push --yes --vault ~/vault/refs
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--vault` | `string` | | Vault directory (overrides [vault].root + notes_dir from config) |
### `zotio vault resolve`
Resolve a note write-back conflict by keeping the vault or the remote copy
Resolve a conflict (or a remotely-deleted note) by choosing a direction:
--keep-vault writes the vault's Notes region over the Zotero child note, using
the live remote version as the precondition. --recreate re-creates a child note
that was deleted in Zotero.
--keep-remote does the reverse: it pulls the live Zotero note body over the
vault Notes region, discarding local edits (a forced 'vault pull'). Only notes
in the shape this CLI writes are pulled.
Exactly one direction is required so the destructive side is explicit, but a
direction is not consent: it only selects WHICH side wins. Previews by default,
describing the note and side that would be written without creating, patching,
or removing anything; pass --yes to apply. --dry-run always wins over --yes.
The resolved conflict artifact is removed on success. Counts as one change
against --max-changes.
zotio vault resolve <citekey-or-item-key> (--keep-vault | --keep-remote) [--vault <dir>] [flags]
Examples:
zotio vault resolve smith2024 --keep-vault
zotio vault resolve smith2024 --keep-vault --yes
zotio vault resolve smith2024 --keep-remote --yes
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--keep-remote` | `bool` | `false` | Pull the Zotero note over the vault Notes region (discards local edits) |
| `--keep-vault` | `bool` | `false` | Write the vault copy over the Zotero note (required direction) |
| `--recreate` | `bool` | `false` | Re-create a child note deleted in Zotero |
| `--vault` | `string` | | Vault directory (overrides [vault].root + notes_dir from config) |
### `zotio vault sync`
Create/update one Markdown note per item in a vault (idempotent)
Materialize literature notes into an Obsidian or Logseq vault from the local
store (run 'sync' first). Notes are named from the citation key, carry zotero://
backlinks, and embed current annotations in a managed block.
Re-running is idempotent and non-destructive: only the managed frontmatter keys
and the fenced annotations block change; your prose and other frontmatter keys
are preserved.
Previews by default, showing create/update/unchanged without writing. Pass --yes
to write; --dry-run always wins over --yes. Each note that would be created or
updated counts as one change against --max-changes.
--collection, --tag and --item-type combine (collection AND tag AND type).
--scope selects one cohort through the shared grammar instead
(collection:KEY, tag:NAME, item:KEY, query:TEXT, saved-search:KEY), and joins
arms with AND to intersect them (collection:KEY AND tag:NAME). --scope is
refused alongside a filter that means a different cohort: --item-type has no
--scope arm, and --collection or --tag must match the scope or be one of its
AND arms.
zotio vault sync [--vault <dir>] [flags]
Examples:
zotio vault sync # uses [vault] root/notes_dir from config
zotio vault sync --vault ~/vault/refs --yes
zotio vault sync --vault ~/vault/refs --collection ABCD2345 --dry-run
zotio vault sync --vault ~/vault/refs --scope query:transformer --dry-run
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--collection` | `string` | | Only sync items in this collection key |
| `--format` | `string` | `obsidian` | Note format: obsidian or logseq |
| `--item-type` | `string` | | Only sync items of this type |
| `--limit` | `int` | `0` | Maximum items to sync (0 = all) |
| `--scope` | `string` | | Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library) |
| `--tag` | `string` | | Only sync items with this tag |
| `--vault` | `string` | | Vault directory (overrides [vault].root + notes_dir from config) |
## `zotio version`
Print version
## `zotio watch`
Keep the local store fresh with periodic incremental syncs
Watch keeps the local store fresh by running incremental sync cycles on
a configurable interval. It starts with an immediate sync, logs one concise
status line per cycle to stderr, and exits gracefully on SIGINT or SIGTERM.
Each resource argument takes a name that sync --resources accepts, such as
items or collections; with none, watch syncs the default set. An unknown name
exits 2 before the first cycle.
When --workflow is set, watch runs the workflow after every
successful sync cycle. It previews unless this watch invocation carries --yes.
A failed applied run leaves its checkpoint: subsequent applied triggers refuse
until it is resumed or deleted with zotio workflow run --yes --resume.
Add --workflow-on-change to run the workflow only after a cycle that changed
the mirror: a row stored, rewritten with different content, or reaped because
its object no longer exists upstream. An unchanged cycle logs that it skipped
the workflow. A cycle whose sync failed, or in which any resource failed, never
runs it; its changes wait for the next complete cycle.
Add --health-check-preprints to --health to watch the arXiv preprints in the
library for a published record. Each cycle looks up the preprints whose last
answer is older than --health-preprint-recheck (default 7d), a bounded number
per cycle, and reports a new preprint_published finding through the same drift
output and --health-webhook as the other health checks. Answers are cached in
the zotio state directory, so an unpublished preprint is not looked up again
inside the horizon and a published one is kept. A failed lookup is logged as a
skip and never clears an existing finding. Act on a finding with zotio items
preprint-check fix.
zotio watch [resource...] [flags]
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--health` | `bool` | `false` | Run quick library health checks after each successful sync |
| `--health-check-preprints` | `bool` | `false` | With --health: also watch arXiv preprints for a published record each cycle (the items preprint-check lookups); a newly published preprint is reported once as a preprint_published finding |
| `--health-check-retractions` | `bool` | `false` | With --health: also run the Crossref retraction check each cycle (same as library health --check-retractions); each DOI is re-checked at most once per 24h |
| `--health-for` | `string` | `quick` | Health preset for --health: quick, citation, systematic-review, vault, all |
| `--health-preprint-recheck` | `string` | | With --health-check-preprints: how long an unpublished answer is trusted before the preprint is looked up again (default 7d; e.g. 2w or 168h; at least 1h); published answers are kept |
| `--health-scope` | `string` | `library` | Health cohort for --health: Item cohort: library \| collection:KEY \| tag:NAME \| item:KEY \| query:TEXT \| saved-search:KEY; combine arms with AND to intersect, e.g. collection:KEY AND tag:NAME (default: the whole library); resolved each cycle, and findings for items leaving the cohort count as resolved |
| `--health-webhook` | `string` | | POST health drift JSON to this webhook URL |
| `--interval` | `duration` | `5m0s` | Sync interval |
| `--once` | `bool` | `false` | Run one sync cycle and exit |
| `--workflow` | `string` | | Run this workflow after every successful sync; previews unless --yes, and failed applied runs require zotio workflow run --yes --resume |
| `--workflow-on-change` | `bool` | `false` | With --workflow: run it only after a complete sync cycle that stored, rewrote, or reaped mirror rows; unchanged cycles log a skip |
## `zotio which`
Find the command that implements a capability
which resolves a natural-language capability query (for example, "export bibtex for a collection" or "find papers missing PDFs") to the best matching command from this CLI's curated feature index.
Exit codes:
0 at least one match found
2 no confident match - the query did not score against any indexed capability; fall back to '--help' or 'search'. Under --json or --agent the output is {"matches":[]}
zotio which [query] [flags]
Examples:
zotio which "export bibtex for a collection"
zotio which "find papers missing PDFs"
zotio which --limit 1 "undo that Zotero change"
zotio which # list the full capability index
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--limit` | `int` | `3` | Maximum number of matches to return (0 = no limit) |
## `zotio workflow`
Compound workflows that combine multiple API operations
### `zotio workflow archive`
Run zotio sync --strict over the default resources
Archive runs 'zotio sync --strict' over the default sync resources. It
uses the same code, JSON events, and exit codes as sync, so any resource that
fails to sync makes it exit non-zero. --full and --db mean what they mean for
sync. Run 'zotio sync' directly to choose resources, --since, or concurrency.
zotio workflow archive [flags]
Examples:
# Incremental sync of the default resources; a failed resource exits non-zero
zotio workflow archive
# Full resync, the same as zotio sync --full --strict
zotio workflow archive --full
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--db` | `string` | | Database path (default: ~/.local/share/zotio/data.db) |
| `--full` | `bool` | `false` | Full resync, as sync --full: ignore the stored checkpoint and per-row versions |
### `zotio workflow run`
Preview or apply a declarative multi-step workflow
Runs a declarative workflow spec in process. By default, every step that
changes anything is previewed with --dry-run: library writes, local state
writes, and steps that write a local file (an export, a report, a baseline).
Steps that change nothing run normally.
Specs may declare top-level "vars" and use ${vars.NAME} in step arguments.
Override declared values with repeatable --var NAME=value. Arguments may also
use ${steps.NAME.output}, the trimmed output of an earlier named step, or
${steps.NAME.json:PATH}, one string, number, or boolean selected from that
step's JSON output. A step can pipe an earlier step's raw output with
"stdin_from"; add "stdin_select": "PATH" to pipe the selected JSON values
instead, one per line. A missing, malformed, or incompatible selection fails
the step, and a selection with no values skips it. "when" can run a step only
when an earlier step is ok, failed, or skipped. In preview mode, substituted
step outputs are preview outputs.
A run started by tail --workflow can read its change batch: the selector
${trigger.json:PATH} and "stdin_trigger": "PATH" select from
{"source", "resource", "events": [{"event", "resource", "key"}],
"upsert_keys", "delete_keys"}. "stdin_trigger": "upsert_keys" feeds
--keys-from -. The batch is data; it never approves a write.
Pass --yes once to apply the whole workflow. Steps apply in order, and every
mutation from that run shares one journal run ID (zotio journal list --workflow
). There is no rollback: if a step fails, writes from earlier steps
stay applied. The run stops at the first failed step unless the spec sets
"continue_on_error". A failed or interrupted applied run keeps its checkpoint
sidecar; continue it with --yes --resume, which skips the completed steps.
zotio workflow run <file.json> [flags]
Examples:
zotio workflow run workflow.json
zotio workflow run workflow.json --var PROJECT=demo
zotio workflow run workflow.json --yes
zotio workflow run workflow.json --yes --resume
{"steps":[{"name":"diagnose","args":["library","health","--json"]},{"name":"fix","args":["items","enrich","--keys-from","-"],"stdin_from":"diagnose"}]}
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--resume` | `bool` | `false` | Resume an interrupted applied workflow from its checkpoint sidecar |
| `--var` | `stringArray` | `[]` | Override a declared workflow variable (NAME=value) |
### `zotio workflow status`
Show local archive status and sync state for all resources
zotio workflow status [flags]
Examples:
# Show archive status
zotio workflow status
# Show status as JSON
zotio workflow status --json
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--db` | `string` | | Database path |