Introduction
# Zotero Skill
Interact with Zotero personal or group libraries via the REST API v3.
## Setup
Requires two environment variables:
``` ZOTERO_API_KEY — Create at https://www.zotero.org/settings/keys/new ZOTERO_USER_ID — Found on the same page (numeric, not username) ```
For group libraries, set `ZOTERO_GROUP_ID` instead of `ZOTERO_USER_ID`.
Optional env var for CrossRef/Unpaywall polite pool (improves DOI lookup success rate):
``` CROSSREF_EMAIL — Your email (optional; uses fallback if unset) ```
If credentials are missing, tell the user what's needed and link them to the key creation page.
## CLI Script
All operations use `scripts/zotero.py` (Python 3, zero external dependencies).
```bash python3 scripts/zotero.py <command> [options] ```
### Commands
| Command | Description | Example | |---------|-------------|---------| | `items` | List top-level items | `zotero.py items --limit 50` | | `search` | Search by query | `zotero.py search "cognitive load"` | | `get` | Full item details + attachments | `zotero.py get ITEMKEY` | | `collections` | List all collections | `zotero.py collections` | | `tags` | List all tags | `zotero.py tags` | | `children` | List attachments/notes for item | `zotero.py children ITEMKEY` | | `add-doi` | Add item by DOI (dedup enabled) | `zotero.py add-doi 10.1234/example` | | `add-isbn` | Add item by ISBN (dedup enabled) | `zotero.py add-isbn 978-0-123456-78-9` | | `add-pmid` | Add item by PubMed ID | `zotero.py add-pmid 12345678` | | `delete` | Move items to trash (recoverable by default) | `zotero.py delete KEY1 KEY2 --yes` | | `update` | Modify item metadata/tags | `zotero.py update KEY --add-tags "new"` | | `export` | Export as BibTeX/RIS/CSL-JSON | `zotero.py export --format bibtex` | | `batch-add` | Add multiple items from file | `zotero.py batch-add dois.txt --type doi` | | `check-pdfs` | Report which items have/lack PDFs | `zotero.py check-pdfs` | | `crossref` | Match citations vs library | `zotero.py crossref bibliography.txt` | | `find-dois` | Find & add missing DOIs via CrossRef | `zotero.py find-dois --limit 10` | | `fetch-pdfs` | Fetch open-access PDFs for items | `zotero.py fetch-pdfs --dry-run` |
### Global Flags
- `--json` — JSON output instead of human-readable (works with items, search, get)
### Common Options
- `--limit N` — Max items to return (default 25) - `--sort FIELD` — Sort by dateModified, title, creator, date - `--direction asc|desc` — Sort direction - `--collection KEY` — Filter by or add to collection - `--type TYPE` — Filter by item type (journalArticle, book, conferencePaper, etc.) - `--tags "tag1,tag2"` — Add tags when creating items - `--force` — Skip duplicate detection on add commands
## Workflows
### Add a paper by DOI
```bash python3 zotero.py add-doi "10.1093/jamia/ocaa037" --tags "review" # Warns if already in library. Use --force to override. ```
Duplicate detection: translates DOI to metadata, searches library by first author, compares DOI fields.
### Bulk add from a file
```bash # One identifier per line, # for comments python3 zotero.py batch-add dois.txt --type doi --tags "imported" ```
Skips duplicates. Reports summary: added/skipped/failed.
### Export bibliography
```bash python3 zotero.py export --format bibtex --output refs.bib python3 zotero.py export --format csljson --collection COLLKEY ```
### Update tags/metadata
```bash python3 zotero.py update ITEMKEY --add-tags "important" --remove-tags "unread" python3 zotero.py update ITEMKEY --title "Corrected Title" --date "2024" python3 zotero.py update ITEMKEY --doi "10.1234/example" python3 zotero.py update ITEMKEY --url "https://example.com/paper" python3 zotero.py update ITEMKEY --add-collection COLLKEY ```
### Delete items
```bash python3 zotero.py delete KEY1 KEY2 --yes # Trash (recoverable, default) python3 zotero.py delete KEY1 --permanent --yes # Permanent delete ```
### Cross-reference citations
```bash python3 zotero.py crossref my-paper.txt ```
Extracts `Author (Year)` patterns from text and matches against library.
### Find missing DOIs
```bash # Dry run (default) — show matches without writing anything python3 zotero.py find-dois --limit 20
# Actually write DOIs to Zotero python3 zotero.py find-dois --apply
# Filter by collection python3 zotero.py find-dois --collection COLLKEY --apply ```
Scans journalArticle and conferencePaper items missing DOIs, queries CrossRef, and matches by title similarity (>85%), exact year, and first author last name. Dry run by default — use `--apply` to write. Only patches the DOI field; never touches other metadata. 1s delay between CrossRef requests (polite pool with mailto).
### Fetch open-access PDFs
```bash # Dry run — show which PDFs are available and from where python3 zotero.py fetch-pdfs --dry-run --limit 10
# Fetch and attach as linked URLs (no storage quota used) python3 zotero.py fetch-pdfs --limit 20
# Also save PDFs locally python3 zotero.py fetch-pdfs --download-dir ./pdfs
# Upload to Zotero storage instead of linked URL python3 zotero.py fetch-pdfs --upload --limit 10
# Only try specific sources python3 zotero.py fetch-pdfs --sources unpaywall,semanticscholar ```
Tries three legal OA sources in order: Unpaywall → Semantic Scholar → DOI content negotiation. By default creates linked URL attachments (no Zotero storage quota needed). Use `--upload` for full S3 upload to Zotero storage. Use `--download-dir` to also save PDFs locally.
**Sources:** `unpaywall`, `semanticscholar`, `doi` (default: all three)
**Rate limits:** 1s between Unpaywall/Semantic Scholar requests, 2s between DOI requests.
### Scripting with JSON
```bash python3 zotero.py --json items --limit 100 | jq '.items[].DOI' python3 zotero.py --json get ITEMKEY | jq '.title' ```
## Notes
- Zero dependencies — Python 3 stdlib only (urllib, json, argparse) - Write operations require an API key with write permissions - If Zotero translation server is down (503), DOI lookups fall back to CrossRef - **Input validation:** DOIs must be `10.xxxx/...` format. Item keys are 8-char alphanumeric (e.g., `VNPN6FHT`). ISBNs must be valid checksums. - `check-pdfs` fetches all items; for large libraries (500+), this may be slow - `fetch-pdfs` also processes all items — use `--collection` to scope for large libraries - Rate limits are generous; batch-add includes 1s delay between items - For common errors and troubleshooting, see [references/troubleshooting.md](references/troubleshooting.md)