Skip to content

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:

[updates]
check = true

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).

zotio analytics [flags]

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

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)

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

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

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.

zotio auth logout

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

zotio auth status

Examples:

zotio auth status

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 capabilities drift

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

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

zotio collections tags

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

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.

zotio demo [flags]
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

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.

zotio desktop status

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.

zotio doctor [flags]

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 (.manifest.json) records each item's key, version, and canonical data hash for drift detection. An interrupted run continues with --resume using .checkpoint.json. Translator formats need Zotero to return the requested include field.

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
### `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
zotio groups list
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 targets
### `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.
zotio init [flags]
| 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
### `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
zotio items find [flags]
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:
zotio items get ABCD2345
| Flag | Type | Default | Description | | --- | --- | --- | --- | | `--format` | `string` | | Response format (json, bib, bibtex, csljson, ris, etc.) | ### `zotio items list` List all items in the library
zotio items list [flags]
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)
zotio items top [flags]
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:
zotio items trash
| 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
### `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
### `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
### `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
zotio profile list
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)
zotio profile use <name>
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
### `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
zotio schema item-fields
Examples:
zotio schema item-fields
### `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
zotio schema item-types
Examples:
zotio schema item-types
### `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
### `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:
zotio searches list
| 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.
zotio sync [flags]
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
### `zotio tags audit` Audit tags for case and spacing drift
zotio tags audit [flags]
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
zotio tags get <tagName>
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
zotio tags list [flags]
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
### `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 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
### `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 |