Skip to content

Safe-by-default writes

Writes are preview-first. Every command that changes your Zotero library or your vault previews by default, applies only under --yes, and counts what it plans against --max-changes. That covers the bulk fixers and importers (items enrich, tags audit fix, items duplicates resolve, import apply, import doi, import file, import url, and the generic import <resource>), every single-item create (items new, import pmid, import arxiv, import isbn), the single-resource CRUD commands (items create/update/delete/restore, collections create/update/delete), and every vault command (vault push, vault pull, vault sync, vault resolve); workflow run extends the same contract across a multi-step plan. Read the contract once and you never have to guess which command is dangerous.

zotio write-safety lifecycle Preview-first write command lifecycle with gates, explicit apply, Web API write, local mirror replay, append-only journal, and reversible undo. Safe by default: preview, gate, apply, journal, undo Every zotio write command starts as a reviewable plan; nothing mutates until explicit consent passes safety gates. preview / local gates apply / Web API journal / undo 1. COMMAND tags audit fix items enrich --missing-doi 2. PLAN (preview) plan / result envelope default: preview only NO changes yet 3. GATES --max-changes cap 500 · agent 50 --allow-destructive irreversible --require-fresh fresh mirror REFUSE loud failed gate = no write 4. Decision --yes provided? No: STOP preview is the final output no yes 5. APPLY explicit mutation applies the change ▸ --agent does NOT auto-apply ▸ --dry-run always wins WRITE to Zotero Web API update / delete / enrich / tags with configured api_key REPLAY to local mirror read-your-writes fresh re-audit without sync 6. RECORD append-only JOURNAL journal list journal show journal undo <run-id> UNDO reverses tag rename collection membership REFUSES rest merges, deletes field overwrites loud, not silent ▸ Hook: safe by default — preview, gate, apply, journal, undo.

The contract

  • Preview is the default. You get a plan/result envelope with zero changes. --yes applies; --dry-run always wins. A preview runs the same reads as an apply, so it reports real no-ops and real versions, and it evaluates the same gates (--max-changes, --allow-destructive, input validation). The → writing via Zotero Web API notice and any config save happen only on apply.
  • --dry-run means no write of any kind. It covers Zotero writes and also local files (including .lock siblings), config saves, journal entries, and mirror write-through. Read commands run normally under --dry-run.
  • --deliver is output routing. --deliver file:<path> or webhook:<url> sends the command's own output to a sink, like shell redirection. --dry-run does not suppress it.
  • --agent does not auto-apply. Agent mode sets JSON + non-interactive defaults, but a write still needs an explicit --yes.
  • Every Zotero write is gated. Every command that writes to Zotero or to the vault needs --yes, and --dry-run overrides it. A workflow run preview injects --dry-run into each step that supports it. Steps that cannot preview, such as sync, are skipped with the reason not previewable; runs only on apply. Commands that destroy local state (auth logout, profile delete) also preview by default and apply with --yes. Commands that create local state (auth set-token, profile save) apply directly. The vault commands keep their own per-note report rather than the generic envelope, but they obey the same gate.
  • Gates cap the blast radius. --max-changes defaults to 500 (50 under --agent); irreversible ops (merge, permanent delete, collection delete, creators rename, creators audit fix, vault resolve) refuse to run without --allow-destructive.
  • Read-your-writes. A write applied through the mutation envelope is replayed into the local mirror immediately and the post-write item state comes back in the envelope, so a re-audit sees the fix with no follow-up sync. Replay is deliberately conservative: scalar field edits, tag/collection membership, item creates, trash, restores, and permanent deletes are replayed when the mirror can express them, while unsupported structural edits are left for the next sync to reconcile authoritatively. A mirror update that itself fails is reported as a warning without failing the already-applied write.
  • Journaled, and honest about undo. Runs applied through the mutation envelope are recorded append-only (journal list / journal show), including the single-resource CRUD commands and partially-rejected batches (a 100-item create where one element is refused still journals the 99 that landed). Some gated writes deliberately sit outside the envelope and so are not journaled: the vault commands, which keep their own report. journal undo <run-id> reverses only what it can invert losslessly — tag and collection membership toggles, creates (reversed by trashing the created item), and items enrich writes of a DOI, abstract, citation fields, or subject and MeSH tags — and loudly refuses everything else (merges, deletions, other field overwrites) rather than guessing. An enrich run records its own inverse while it writes: the value each field held, the tags it added, and the exact zotio: provenance line it appended to Extra. Undo takes that line out of the live Extra and leaves every other line alone; it restores a field only while the field still holds the value enrich wrote; and it still works when the line is already gone. A run journaled before enrich recorded this keeps its old behaviour: its tags undo, its Extra line stays, and its field overwrites are refused. A journal entry is therefore an audit record, not a promise of reversibility: items delete is journaled but not undoable, because the journal records what a run did, not a full pre-image of what it replaced. Recover a deleted item from Zotero's trash with items restore.
  • One writer at a time. Applying a write takes the installation-wide advisory lock (~/.zotio/.writer.lock). export snapshot and collections bundle instead lock their canonical output path, so runs writing to different directories stay parallel, and a vault write holds both the installation lock and its vault-path lock. Writers do not queue: a second concurrent writer in the same scope exits 9 immediately and is safe to retry once the first finishes. Reads and previews are never blocked, so parallelize those and serialize applies. See Architecture decisions › Single-writer concurrency.
  • Partial success is loud. Zotero answers a batched write with HTTP 200 even when it rejected some elements. Those rejections are reported with their source index and message, and the command exits 13 (degraded) — never 0.

Across a whole workflow

The contract scales from one command to a plan. zotio workflow run previews every mutating step by default and applies the whole plan on a single --yes; every mutation in the run shares one journal run ID (journal list --workflow <id>), and an interrupted run resumes from a checkpoint without replaying a write whose outcome is uncertain. See Workflows & triggers.

Where writes land

Writes split by intent: new personal-library items prefer the keyless local desktop connector. Edits and note write-back need a Web API key. attachments add --via connector saves bytes through the desktop but still needs a Web API key to move the attachment to an existing item.

zotio hybrid routing architecture Hero diagram showing zotio routing reads locally without an API key, writes to Zotero Web API with a key, external metadata providers, local-only desktop commands, local version reads before writes, and replay into the SQLite mirror. zotio hybrid routing Local-fast reads, key-gated cloud writes, and an immediately refreshed local mirror. READ WRITE EXTERNAL LOCAL-only Semantic colors are shared across README diagrams. READ plane Local-first Local Zotero API :23119 Zotero desktop running items list/get collections annotations fulltext Local SQLite mirror Synced cache for compound reads search items audit stats Desktop connector New-item create path create items · attachments · PDFs prefers local · else Web API ▸local reads/creates need no key · cloud writes do CLOUD write path Zotero Web API api.zotero.org Auto-routed mutation target items update/delete/move enrich · tags audit fix collections create/update vault push · journal undo LOCAL-only plane Files · desktop · introspection vault sync items open doctor capabilities EXTERNAL plane Metadata providers CrossRef · OpenAlex Semantic Scholar Unpaywall · OpenCitations zotio routing + safety layer one CLI surface reads new items writes replay → mirror enrich · import local ops

The capabilities reference lists the operation, write target, destructiveness, and requirements for every command. See Authentication for key setup.

Example

zotio tags audit fix --agent            # preview: the merge plan, zero changes
zotio tags audit fix --agent --yes      # apply
zotio journal list                      # find the run id
zotio journal undo <run-id>             # reverse the tag renames