Skip to content

User guide

papio finds scholarly papers, checks each PDF is the paper you asked for, and offers finished PDFs toward your reference library — into Zotero through zotio, which always shows you a preview first, or toward any other destination through a best-effort on_ready hook. Hook failures never fail or retry the acquisition job. It does not handle institution logins, two-factor codes, CAPTCHAs, or bulk-downloading from subscription databases.

Use config-reference.md to change policy and troubleshooting.md when a job needs attention.

1. Initialize the local profile

Run the guided setup before any acquisition:

papio init

papio init writes your configuration, creates the data folder and its database, checks the zotio program, installs the browser connector (unless you skip browser setup), and runs doctor. You can run it again safely. It asks for a contact email, the zotio path, an attachment mode, and whether to set up browser integration.

For an unattended profile, retain existing values unless an option overrides them:

papio init --non-interactive --email you@example.org --skip-browser

Use --zotio-path to select the executable and --attachment-mode with either stored or linked-file when those values need changing. Run papio doctor after any manual configuration change.

2. Discover a research set

Start with a discovery search (OpenAlex by default; add arXiv or Semantic Scholar via discovery.sources in config, or pick one backend for a single query with --source arxiv|openalex|semanticscholar):

papio search "appropriate reliance on AI" --limit 20 --year-from 2023

--oa-only limits results to works marked open access. --year-to sets an upper publication-year limit. Search output marks a result already found by the local zotio library or a configured library.sources authority as [in library]; JSON output exposes the same state as owned and, when available, owned_item_key. Without either authority, results are unowned/unclassified.

Search results lead with confident title matches: when a result's title clearly answers the query — an exact match, a phrase match, or most of the query's words — it moves to the front of the list, best match first. Every other result keeps the order the backend returned, since ranking by abstract, concepts, and citation graph is what a discovery backend does better than title comparison can. Each result reports how well it matched (match_score and match_kind in --json output), and text output says so plainly when nothing in the result set matched strongly, instead of returning the closest unrelated papers as if they were right. A query under three words is treated as a keyword search and left in the backend's own order, as is a citation-snowball search below.

Use --new-only when you want the result set to omit works marked owned by zotio or a configured library.sources authority:

papio search "appropriate reliance on AI" --limit 20 --new-only --json

New-only search reads up to ten pages per call and up to 5,000 rows across a continuation chain. It returns continuation, partial, pages, and scanned in JSON output. Resume with the same query, filters, and limit using --continuation <token>. A budget limit or unsupported paging does not imply exhaustion. Incomplete ownership evidence fails the request instead of calling the results unowned. Zotio and generic holdings require a supported exact identifier; use ordinary search for title-only results.

Grow from a seed paper

The three citation-snowball options take a DOI. Free-text query is optional when one of them is present.

papio search --cites 10.1000/example --limit 20
papio search --cited-by 10.1000/example --limit 20
papio search --related-to 10.1000/example --limit 20

--cites finds papers that cite the DOI (forward citations); --cited-by finds papers cited by the DOI (backward references); and --related-to finds OpenAlex-related papers.

3. Acquire the selected works as a batch

Give acquire --batch a JSONL file of work records, or a RIS (.ris), BibTeX (.bib or .bibtex), CSL-JSON (a .json file whose top level is an array), or MEDLINE/NBIB (.nbib) file. A plain text file with one identifier per line also works: a DOI, a doi.org URL, arXiv: or an arxiv.org URL, PMID: or a pubmed.ncbi.nlm.nih.gov/<pmid> URL. Blank lines and lines starting with # are skipped. One line that papio cannot read refuses the whole file and names the line. papio campaign import takes the same list for DOI, arXiv and PMID lines only. A JSONL line may also be a work-request/1 document, which keeps its own request_id. papio detects file formats by extension and content-sniffs standard input (-). A batch holds up to 50 works; identifier normalization and deduplication are identical for every format. Reruns reuse only live jobs. A settled work creates a new job unless zotio or a pdf_present library source reports an existing PDF. To inspect an existing run without re-acquiring it, use papio batch report latest or pass its batch ID. To filter already-held works before submission, configure zotio or library.sources ownership.

Acquire exact search keys

papio search save selected.json "appropriate reliance on AI" --limit 20
papio search acquire selected.json --keys <first-key>,<second-key>
papio search acquire selected.json --keys <first-key>,<second-key> --confirm

search remains read-only. search save writes a new bounded snapshot with stable result keys and refuses to overwrite an existing file. Select 1–50 exact keys. The preview shows the selected metadata, current ownership, count, and configured cost policy. Only --confirm submits the selected set through batch acquisition; automatic Zotero import remains off.

Stage a larger selected bibliography

papio campaign import screening.json
papio campaign preview <campaign-id>
papio campaign run <campaign-id>
papio campaign pause <campaign-id>
papio campaign resume <campaign-id>
papio campaign report <campaign-id> --json

screening.json is a CSL-JSON array. Campaign import also accepts RIS, BibTeX, and MEDLINE/NBIB citation exports; it does not accept native JSONL.

Import accepts 1–200 source records and stores the selected set and ownership configuration before submission. Each run submits at most one chunk of 50 canonical works. A 50-active-work window prevents advancing while prior nonterminal work fills the window. Pause prevents new staged submission; it does not cancel existing jobs. Resume enables submission but does not submit until the next run.

The report retains one row per original record, including owned, invalid, and unsuccessful records. Each row carries the source digest, format, ordinal, and original BibTeX key, RIS ID, or CSL id when present. Duplicate source records can share one job without losing their separate source identities. Citation fields follow the supported parser subset; this is not lossless vendor reimport. Campaign receipts retain acknowledged jobs across restart, terminal states, and changes to library ownership after submission.

papio acquire --batch works.jsonl --auto-import \
  --collection "AI reading" --label "appropriate-reliance"

For example, export a reference list from Zotero, Rayyan, or Covidence as RIS:

papio acquire --batch refs.ris --label "thesis background"

Each record needs an identifier (DOI, PMID, arXiv, ISBN, OpenAlex) or a complete title/authors/year tuple — the same identity rule as JSONL input.

--auto-import asks papio to plan and apply the zotio import after a job becomes ready. It is non-fatal to acquisition: an import error remains visible in the batch report and can be retried through the normal zotio preview flow.

--collection carries the requested zotio collection with each work; the collection is created on demand by zotio, and importing the same work again is safe.

In stored mode, papio gives a new paper's collection, DOI and abstract to Zotero desktop with the paper, so the paper is in its collection and has its abstract as soon as Zotero saves it. The abstract comes from the DOI registry (Crossref or DataCite), or from OpenAlex when the registry has none and auto_enrich is on. The import needs Zotero desktop to be open: papio does not send the paper through the Zotero Web API instead, because that route stores the PDF in Zotero's own file storage.

Sometimes zotio cannot describe a new paper. The DOI registries that zotio asks, Crossref and DataCite, do not hold every DOI: a DOI from another registration agency, such as mEDRA, is not in either. And zotio does not read a PMID or an ISBN from the PDF. In these cases papio describes the Zotero item from its own record of the paper: the title, the authors as papio holds them, the year, the DOI, and the PMID or arXiv ID as a line in Extra. Before it does this, papio looks for the paper's identifiers in your library, so a paper that is already there gets the PDF instead of a second item.

Some steps still happen after the import, through the Zotero Web API: filing a paper that was already in your library, filing into a collection whose name matches no collection (zotio creates it) or more than one, and filing into a collection that Zotero desktop does not show yet. The Web API has a new item only after Zotero desktop syncs it, so such a step can take minutes. papio repeats a step the Web API refused with 404 after 1 minute, and then after 10 minutes, 1 hour, and 6 hours, and then stops. It does not run the import again.

When Zotero desktop is closed, papio does not spend an import attempt on a paper that needs it: the paper waits, papio activity shows "Waiting for Zotero desktop", and you get one notification. Open Zotero and papio imports the waiting papers within seconds. When Zotero is open but does not respond, papio tells you to restart it; when its connector is off, papio tells you to turn on "Allow other applications to communicate with Zotero" in Zotero's Advanced settings. This needs a zotio with the desktop status and desktop wait commands; with an older zotio, papio tries a failed import again after 1 minute, 10 minutes, 1 hour, and 12 hours, and then stops.

When zotio is configured, papio first classifies batch works against your zotio library: works already owning a PDF are skipped, a known item without a PDF is queued on its existing-item attachment route, and other works are acquired as new items. Add --include-owned only when a batch should also submit works that already carry a zotio PDF.

Without zotio configured, the --auto-import and --collection behaviour above does not apply. A ready job's PDF receives a best-effort on_ready hook handoff instead. De-duplication still works if you configure a library source — --batch then skips papers that source says you hold, and refuses to create any jobs at all if it cannot read the source, unless you explicitly use papio acquire --batch --include-owned. This batch-only override accepts ownership uncertainty and proceeds despite the unreadable source. With no library source configured, every work is treated as new and a batch will re-acquire a paper you already have. Hook failures never fail or retry the acquisition job.

You can queue one work instead:

papio acquire 10.1371/journal.pone.0262026 --auto-import --wait

The one-work command also accepts --doi, --pmid, --arxiv, --isbn, or --openalex; title-based requests need --title, repeatable --author, and --year. Use --desired-version with published, accepted, preprint, or any, --source or --deny-source to constrain sources, and --max-cost to cap paid-source cost. --label works here too: it records the query context and seeds the target collection when --collection is unset.

Bulk import from discovery interfaces

A discovery interface's own search result list is often the easiest place to gather a reading set — a OneSearch or Primo results page, a Google Scholar search, a systematic-review tool's export — but papio does not query, scrape, or paginate a third-party discovery interface's search API on your behalf. The reliable route through one is silent-UI: use the interface's own Export control to write a RIS (or BibTeX / CSL-JSON) file of the results you selected, then hand that file to --batch directly:

papio acquire --batch export.ris --label "spring literature review"

This is the same --batch command described above — RIS .ris, BibTeX .bib/.bibtex, CSL-JSON, JSONL, and MEDLINE/NBIB .nbib are all accepted regardless of where the file came from, the same ledger/cache deduplication applies, and a title-only entry with no identifier still submits through the same enrichment path as a title-only JSONL work — arriving as an exported RIS row is not itself grounds for refusal.

Where a page instead shows exact, on-page identifiers you can select yourself — a reference list, bibliography, or results page with visible DOIs — the extension popup's "Select papers on this page" action is the browser-native path: it reads only what's visibly on the page and submits your selection as an ordinary batch. Structured citation export through this section is the preferred fallback whenever the interface withholds identifiers from the page itself, which is the common case for a search portal's own results list — the export already carries identifiers a visual scan cannot recover.

Export a search or a campaign for screening tools

papio export search <snapshot-file> writes the rows of a snapshot saved by papio search save as CSL-JSON, RIS, or BibTeX. Add --keys k1,k2 to export only the named snapshot rows; an unknown key is an error. papio export campaign <campaign-id> writes every distinct work staged in a campaign. Source records that failed to parse have no work, so the command skips them and reports how many (on stderr, and as skipped_source_records in the --json receipt).

Both commands read local files only: no daemon call, no job, and no file is written except the one named by -o. --format, -o, --force, --json and --include-duplicates work as for papio export ledger.

4. Follow the work instead of guessing

status groups your jobs into working, awaiting-human, needs-review, ready, imported, and failed or unavailable phases. ready means a validated PDF is waiting for import; once a zotio apply files it, the job moves to imported with its Zotero item keys, so neither surface keeps presenting finished work as actionable:

papio status --follow

The text view shows paper titles when metadata supplies them, with identifiers as the fallback. Each paper keeps its job ID. Within a phase, papers share guidance only when their institution profile, recorded provider, blocker, and quiet reason match. Unknown providers remain unknown.

Browser actions include an exact papio actions open --action <action-id> command. It opens that action, not the whole queue. Add --dry-run to inspect the route without opening it. A document-delivery action instead points to papio delivery get <job-id>, which shows the request and its blockers.

Status also names actions whose automatic browser offers or reminders have stopped. Seven-day-old actions remain open; repeated drives without progress can stop automatic offers sooner. An explicit open still works. This display does not reset either limit or change the queue rules.

The JSON view retains its phase groups and includes each paper's command, institution, and quiet_reason when available.

--follow refreshes the dashboard every two seconds. For a single job, use papio jobs get <job-id> --wait; papio jobs list --state <state> filters the job list, and papio jobs retry <job-id> explicitly retries a failed, unavailable, or retry-wait job.

For a compact event-oriented view of the daemon's durable work, run papio activity. It is newest-first and bounded; --limit changes the number of rows, --job <job-id> narrows the view, and --json gives the same page envelope used by other list commands. A page that stops short ends with the command for the older events: --before-seq <seq> shows only events older than that sequence number, so pass the seq of the last row you have. The command and the inbox activity panel read the same daemon events; the reference page is generated, so use it for the complete option list.

Use the inbox activity panel

Open Open inbox in the extension popup when you want the browser-side triage view. Alongside jobs and human actions, the inbox can show a compact activity timeline: download started/completed, institutional sign-in returned, provider outcomes, adoption, and other recent daemon events. The panel is a solicited pull, not a live push stream. It refreshes while the inbox tab is open, and older daemons that do not advertise the activity-feed feature show a clear unavailable message rather than stale or guessed entries.

Use each papio surface for one job

The extension and daemon deliberately keep five surfaces distinct:

Surface What it answers
Inline result / toast strip Did the action I just took land?
Popup What is happening now for this page and this browser?
Badge and tooltip Is papio disconnected, blocked, or waiting for me?
Desktop notification Did something worth interrupting me for happen while I was elsewhere?
Inbox and Activity What needs a decision, what is continuing, and what happened?

The popup is a current-page lens with a compact global pulse, not a second inbox. The badge is ambient and lossy, not a progress bar. The inbox is the durable decision surface, and Activity is its recoverable history. A feedback strip acknowledges a local action; it never becomes a work queue.

Choose notification interruptions

Desktop notification routing is daemon-owned. The default milestones preset interrupts for useful milestones rather than every event: newly opened decisions are coalesced, pending decisions become a digest no more often than every four hours, batch completion gets at most a useful checkpoint and a meaningful final summary, discoveries stay in catch-up/digest views, integrity notices are capped per scan, and a named degraded episode notifies once. Continuing work and scheduled retries stay quiet. The quiet preset is less interruptive, while verbose surfaces more category events. Use [notify] in the configuration reference for the preset table and per-category overrides.

Webhooks are a separate automation channel. Human quiet hours and the desktop rate limit do not delay them. Each category can set its own webhook mode. papio sends a stable delivery key with each webhook. If the request cannot reach the endpoint (the connection is refused, DNS fails, or the TLS certificate is rejected), or the endpoint answers 429 or 503, papio tries the same notification again with the same key. Each retry waits at least 5 minutes, and retries stop 24 hours after the first failed attempt. Other failures, such as a 4xx answer or a connection that breaks after the request went out, are not retried, because the endpoint may already have the notification. papio records each failed attempt and its cause in Activity. If papio stops during a send, the result can be unknown. The receiver can use the key to detect repeat requests. Check the inbox and Activity for the durable outcome when a notification does not arrive.

By default a webhook receives a JSON body. To post to a chat app, set notify.webhook_format to ntfy (an ntfy topic URL such as https://ntfy.sh/your-topic, with an optional bearer) or slack (a Slack incoming-webhook URL). The format renders the message, the event name, and the count in the target's own shape; the configuration reference lists the exact fields. papio notify show and papio notify preview print the format from your configuration file. papio notify test request_outcome --webhook posts one test notification in that format and reports whether the endpoint answered with a 2xx status.

Read the pulse for a batch

For a one-shot daemon reading, run:

papio pulse
papio pulse --json

The popup and inbox request the same typed pulse on their existing refresh cadence. Depending on the authoritative projection, its vocabulary is:

  • Moving — work is in flight or eligible to continue automatically.
  • Waiting on you — no work is moving and an effective researcher turn is open.
  • Stalled — work has a named, durable degradation episode rather than an ordinary scheduled wait.
  • Scheduled — only future retries, delivery polls, or source gates remain.
  • Idle — a complete projection reports no nonterminal work.
  • Unknown — the daemon is disconnected, the reading is stale or contradictory, or the projection is incomplete for the requested conclusion.

When a batch is partial or a measurement is unavailable, papio says so instead of turning missing data into zero. It shows no progress percentage, success ETA, or queue position. A next action time is a scheduled retry or check, not a promise that the work will succeed then.

Choose the toolbar count

In Options → Feedback and interruptions, choose one of:

  • Decisions waiting (the default): show the exact daemon-owned count of effective required turns;
  • Everything pending: show the broader legacy pending inventory; or
  • No number: keep the badge's disconnected and blocker states without a numeric count.

The ! disconnected/blocker indicator always takes precedence. Watch hits, retractions, dependent sibling papers, and continuing work do not inflate the default required-turn number. When an older daemon cannot provide the required turn projection, the extension labels the fallback honestly as pending items.

Recover Activity after being away

Activity remains a solicited pull, not a live push stream. The inbox requests pages of at most 50 entries and can use Show more to walk older entries. The extension stores an activity_seen_through_seq read watermark in browser profile storage. It advances that watermark only after a visible Activity render succeeds; background polling does not mark entries read.

When the daemon can calculate the difference, the inbox shows Activity (N new) and a Since you were last here divider. If retained history has a gap, it says that newer Activity is available without inventing an exact count. Activity remains ordered and recoverable while repeated polling stays quiet for screen readers.

Desktop capability and acknowledgement

Desktop notifications are daemon-owned and best-effort. The supported senders are macOS (osascript), Linux (notify-send or gdbus), and Windows (PowerShell toast); a platform whose helper binary is missing reports the capability as unavailable instead of pretending that a notification was shown. The operating system does not provide papio with a delivery or visibility acknowledgement, so the Activity audit language says attempted, never delivered. A desktop notification is never the only record of an outcome: use the inbox, Activity, or the corresponding CLI command to recover it.

When an inbox row says manual download, open the provider's PDF in the ordinary browser and use the popup's Send this PDF to papio action. The activity panel then makes the download and adoption steps visible while validation runs. If you already have the PDF as a file, use papio jobs supply-pdf instead (see Supply a PDF you already have).

5. Complete one browser pass when required

When no usable direct candidate remains, assisted and delegated access modes can park a job for the ordinary Chrome extension. First inspect the queue without opening a browser:

papio actions list
papio actions open --dry-run

Then open the current handoff URLs:

papio actions open

The extension popup keeps daemon-owned decisions in its pulse and browser-local work under Do this in the browser. A shared explanation appears once rather than under every paper; hover or focus a compact control for its secondary detail. Use Focus only when authentication or a provider-owned decision is required. papio actions open asks a compatible extension to use the browser where its tracked session lives; without one, it opens the resolver URLs normally.

The acquire action itself sits in the popup's header: an accented + beside the inbox and settings icons, offered only when the current tab has a paper to add and nothing already running for it. Hover or focus it to read exactly what it will act on, DOI included. Everything else about that action — a refusal and its remedy, a which paper is this? choice, an acquisition already in progress — appears in the card below instead, so the icon is never the thing reporting a problem. Select papers on this page keeps its full label there: it is a different, multi-step action that scans only after you click it.

The inbox keeps itself current without a manual refresh: it updates as soon as you return to the tab, and checks in on its own every so often while the tab stays open, so a new or resolved job doesn't wait for you to notice. The toolbar badge keeps pace on the extension's own wake cycle — its existing one-minute keepalive alarm — whether or not a papio tab is open, since the browser decides how often a sleeping extension is woken.

The inbox follows the same rule: one sentence-case heading and instruction per same-kind block, with each paper reduced to its title and one identifying fact. A one-paper security check, terms page, or institutional sign-in still keeps its instruction; shared copy is never hidden merely because there is only one row. The summary line carries only the exact effective turn count (N need you); Actions and Watch hits keep their own inventory counts in the tabs instead of those totals being repeated and added together above them.

The popup also reports the background service's health: it shows a version line when all is well, and clear warnings when the service is unreachable or the two sides are out of date. The toolbar badge shows ! when attention is needed, and the options-page footer shows the extension and background-service versions together.

For institutional handoffs, papio uses your library's OpenURL resolver first. If it links straight to the provider, papio follows it. When Alma/Primo shows an online-services menu instead, the extension follows your library's top full-text link in papio's own tab; you do not need to click Available Online or View full text. It never chooses physical-item, scan, interlibrary-loan, or terms-acceptance options — those stay your decisions. If your library's resolver is on a domain the extension isn't preapproved for, that step stays assisted.

Opening a browser-handoff row from the inbox focuses that job's tracked resolver tab; it does not open the DOI in a separate, untracked tab. Re-running papio actions open behaves the same unless that tracked tab is on an authentication page (or awaiting authentication), when the extension re-drives its retained resolver URL for a fresh sign-in attempt. It never navigates a provider page that is already progressing a download. If Alma or Primo explicitly reports that no electronic full text or online link exists, the extension marks the route unavailable instead of sending you through the same institutional-login loop again. An empty or still-loading resolver page remains assisted rather than being treated as proof of no access.

papio also closes the tabs it opened once it no longer needs them: after it files a paper, when a job ends, or when a newer tab for the same paper replaces one. Pin a tab, or move it out of papio's window or tab group, to keep it. A toast with Reopen gives back the tabs of filed papers. See When papio closes its tabs.

Grant optional extension host permissions only for publisher sites you use. The friendly publisher name is the only visible row label; hover the row or focus its switch to reveal the exact host pattern being granted. The full pattern remains in the control's accessible name. While handoff jobs are still open, the extension keeps one pinned, muted tab and reloads it now and then to keep your session alive. If it detects that your institution's login page has taken over, it stops reloading, brings the tab forward, and flags a single sign-in request. Sign in normally there; once you're back, the extension resumes. This keeps you to one login per research session — it does not automate your credentials.

Some libraries show a resolver page that stays empty in a background tab. If your library uses OpenAthens, you can skip that page for papers from publishers papio has an adapter for. papio then opens the paper's DOI through your library's OpenAthens redirector. The redirector signs in to the publisher with your institution and opens the article.

  1. Find your OpenAthens domain. Open a link from your library that goes to a publisher. Look for go.openathens.net/redirector/<domain> in the address or in the link itself. The <domain> part is the value you need, for example example.edu. Your library's help page can also tell you.
  2. Add it to your config. Use the lowercase name only. Do not add https://, a slash, a port or a query.
[browser]
openurl_base_url = "https://example.primo.exlibrisgroup.com/nde/openurl"
openathens_domain = "example.edu"

For a named profile, put the same key under [browser.resolvers.<name>]. 3. Update papio to a version that knows the key before you save the config. An older papio rejects the whole config file when it sees an unknown key.

papio offers the redirector link first. If that route ends without a PDF, for example because your library has no access or the page changed, papio moves the paper to your library's OpenURL route. It offers the redirector once for each paper. The redirector does not sign you in by itself. If OpenAthens asks you to sign in, sign in normally in the tab.

Watch the institution session

The popup's Institution session card reports the browser-local resolver state: Signed in, Signed in - due a recheck, Signed out or expired, Sign-in needed - papio paused, or Keep-warm off. A newer inconclusive check says what papio could not verify instead of repeating an older verdict. One exception keeps both facts visible: a fresh signed-in verdict followed by an empty check says Signed in recently - no library page open to recheck. When every configured institution is signed in and freshly verified, the card says All institutions signed in.

If a paper is waiting for sign-in, a completed HTTPS navigation on that paper's declared publisher host schedules a fresh check of the matching library resolver. The extension reads no publisher page content for this trigger. The publisher page cannot set the popup's signed-in or signed-out verdict. If papio's own resolver tab is still paused on an identity-provider page, papio leaves that page open and creates a muted background resolver tab for the check. The replacement surfaces only when the resolver itself says that sign-in is still required. A tracked papio tab returning from authentication can still release same-institution queued work through its existing job-scoped evidence path. Other hosts, queued-only papers, unknown bindings, and ambiguous institutions do not authorize this publisher-triggered check. A same-document sign-in with no completed navigation falls back to the normal keep-warm refresh cycle.

If the browser still needs access to the configured resolver, the card says Library page access required and offers Allow for that institution only. The permission is separate from the session verdict, so a missing grant does not mean you are signed out. Sign in focuses the ordinary resolver tab when a library route is configured. Sign in, complete any MFA or other institution step yourself, and return to the provider page; papio never fills credentials or copies cookies.

The Options page controls this browser-local behavior. Keep-warm mode has three settings:

  • While papio has work refreshes the resolver while institutional papers are active.
  • Always for current library keeps the configured resolver warm even when no paper currently needs it.
  • Off stops automatic resolver checks.

Refresh interval chooses how often either active mode refreshes the resolver (2–30 minutes). The card and options controls describe this browser's session only. They do not change daemon access policy or send login details anywhere.

Let papio open parked handoffs for you

Without help, a parked handoff waits until you run papio actions open or open it from the inbox. The paced drive does that step for you. Use it when many papers are parked and you want papio to work through them slowly, while your browser stays open and you do other work. It is off by default.

To turn it on, add this section to your configuration file, then restart the background service:

[drive]
enabled = true
papio daemon stop   # the next papio command starts it with the new setting

The drive then works like this:

  • It opens one parked handoff at a time, oldest first, through the same path as papio actions open. Your extension settings still decide where the tab opens.
  • By default, it opens at most 10 papers in any hour, and it does not open the same paper again for 6 hours, whatever the result was.
  • It waits while no browser is connected to papio, while any other browser work is still running, while a sign-in is in progress, and during your notify.quiet_hours.
  • It skips papers from a provider that is refusing this browser, together with every paper that has the same DOI prefix.
  • If the sign-in for one paper does not come back within 10 minutes, it leaves that paper, holds back every paper with the same DOI prefix for 6 hours, and goes on to the next paper. A sign-in that comes back and then asks again starts a new 10 minutes.
  • If this happens to the two most recent papers, and they come from different providers through the same library, the drive pauses and sends you one notification. Sign in to your library in the browser. The drive continues by itself when any sign-in comes back, or when you run papio drive resume.

The drive never accepts publisher terms, never submits a document-delivery request, and never resolves an identity review. Those decisions stay with you. It sends nothing new off your machine: it opens the same handoff links that papio actions open opens, in your own browser.

Use these commands to check and control it:

papio drive status   # what it would open next, and what blocks it
papio drive pause    # stop paced opens until you resume
papio drive resume

papio drive status prints one line, for example how many of the hourly opens it used, the next job, and a blocked by: list. The blocker names are:

Blocker Meaning
disabled [drive] enabled is not true.
paused You ran papio drive pause, or two sign-ins in a row did not come back.
quiet_hours The time is inside notify.quiet_hours.
holder_absent No browser is connected to papio to receive handoffs.
rate_limit The hourly limit is used up.
in_flight The previous paced open has not settled yet.
browser_busy Other browser work is still running, including work you started.
sign_in_pending A sign-in is in progress and still inside its 10-minute wait.
no_eligible_job No parked paper can be opened now.

A pause stays in force across a restart of the background service. The drive never lifts a pause that you set; only papio drive resume does. A coding agent that uses papio's MCP tools can pause the drive but cannot resume it.

Each paced open is recorded on its job as drive.paced_open, so papio jobs get <job-id> shows it. To turn the drive off, set enabled = false (or remove the section) and restart the background service. The limits above are defaults you can change; see [drive] in the configuration reference.

Let an article agent choose the download control

Some publisher pages have no packaged papio adapter, or the page changed and the adapter no longer works. In delegated mode, the optional article agent can continue there. It sends a short list of the buttons and links on the article page to Jev, a model that TypeSafe runs, and Jev chooses one. The agent runs only after the packaged and generic routes have failed.

You need:

  • access_mode = "delegated";
  • a TypeSafe API key, which is a paid service;
  • site access for that publisher in the extension; and
  • a daemon and extension that both support the agent. With an older extension, the page stays assisted.

To turn it on, save your key, then restart the background service:

papio config agent set      # asks for the key at a hidden prompt
papio daemon stop           # the next papio command starts it with the key
papio config agent status   # shows the setup without printing the key

To read the key from a secret manager instead of a prompt, pipe it into papio config agent set --key-stdin. papio keeps the key in macOS Keychain, Windows Credential Manager, or Linux Secret Service, and writes only a reference to it in your configuration. papio config credentials manages the same record under the target agent.typesafe.

What leaves your machine: for each decision, TypeSafe receives your key, the article's DOI, a title of at most 400 characters, and at most 80 visible controls, each with a label of at most 240 characters, its role, and whether it is disabled. Page addresses, page text, form values, cookies, passwords, and other tabs are not sent. Privacy lists the details.

What the model can do: it answers with exactly one of these:

  • one enabled control from the list it received;
  • WAIT, when the page is still loading; or
  • BLOCKED, when a person must act (a sign-in, a challenge, a payment, or terms), or when no safe control moves toward the PDF.

It cannot type text, supply an address, run code, or choose a control that is not on the page. papio refuses any other answer. The extension clicks the chosen control in papio's own tab, and the downloaded file goes through the same PDF and identity checks as every other file.

Each decision is one paid TypeSafe request. For one download attempt, papio allows at most 60 decisions in ten minutes and 30 seconds for each model call. You cannot change these limits in the configuration.

To see what the agent did, run papio jobs get <job-id>. When the agent chose the control that produced the file, the artifact.producer line says producer=agent and names the decision. papio stats producers counts these papers (see See who produced each paper).

To turn the agent off, run papio config agent remove and restart the background service. If you set PAPIO_TYPESAFE_API_KEY in the daemon's environment, remove it too. remove keeps the stored key; to delete it, run papio config credentials delete REFERENCE --yes (without --yes it only describes the deletion). The agent section of the configuration reference covers Windows sign-in sessions and the Firefox download rules.

Send a PDF already open in the browser

If a handoff or a provider page has reached a PDF, open the popup and choose Send this PDF to papio. The popup recognizes a direct PDF URL and the built-in Chrome or Firefox PDF viewer, then queues the DOI or associates the current tab with its existing job. The extension starts a browser-managed download named papio/<job-id>/paper.pdf. That name is relative: Chrome's download steering can only place a file inside the browser's own download directory, so the file lands at <your browser download folder>/papio/<job-id>/. The daemon's adoption root is the matching papio folder — by default <your download folder>/papio, which papio init creates for you — and it adopts the file from that job-scoped location, running the same validation used for a directly fetched PDF. The popup reports Sending PDF to papio… and then papio adopted PDF (validating); the inbox and papio activity show the later outcome.

You only need download_adoption_root if your browser downloads somewhere other than this account's download folder; set it to that folder's papio subdirectory. papio doctor fails the adoption_root check, and names the path it resolved, whenever the configured root is not a papio directory a browser could steer into — the failure mode is otherwise completely silent, because nothing errors, files simply never get adopted.

The download path is deliberate: do not rename a file into another job's directory. If validation rejects the PDF, the job remains actionable so you can provide a different file. If the current tab is only a DOI or provider landing page, use the header's + (Acquire this page) or the browser handoff first; Send this PDF to papio is for a PDF page.

Firefox does not expose Chrome's downloads.onDeterminingFilename hook. The popup's direct Send this PDF to papio download still uses its job-scoped filename, but a provider button that starts its own click download cannot be rerouted automatically and remains human-assisted. When that happens, wait until the PDF is open and use Send this PDF to papio rather than assuming an unrelated file in Downloads will be adopted.

Supply a PDF you already have

If you already have the paper as a file — an email attachment, an interlibrary loan copy, or an earlier download — give it to its job directly:

papio jobs supply-pdf <job-id> ~/Documents/paper.pdf

The command refuses a directory, a symbolic link, a file that does not begin with %PDF-, and a file larger than [fetch] max_bytes. It copies the file into <data_dir>/supplied/<job-id>/, and the daemon reads the copy only from there. Your original file does not change. The daemon then runs the same checks as Send this PDF to papio: the PDF must be readable, and it must be the paper that the job asked for. A PDF of a different paper never becomes ready.

The command prints the job, the outcome, the job state, and the SHA-256 of the file. The outcome is one of these:

  • accepted: the PDF passed, and the job is ready.
  • needs_review: the PDF names a different work, or it is encrypted or holds active content. The job waits for your review action.
  • rejected: the PDF failed validation. The job waits for a different file.

The job must still be open: queued, resolving, fetching, retry_wait, or awaiting_human. A paywalled job often waits in retry_wait between attempts; the command takes it from there. The command refuses a job that is validating or waiting for review, and a resolving or fetching job that papio is working on at that moment. Try again when papio jobs get <job-id> shows the job waiting. The command also refuses a job that is waiting on a document-delivery request, in any state. Check that request with papio delivery get <job-id>, and settle or cancel it first.

The command moves a queued or retry_wait job to awaiting_human before it reads the file. If it then fails before validation starts, it puts a retry_wait job back in retry_wait. A queued job cannot go back to queued, so it stays in awaiting_human with a manual_download action, and the error says that the job is parked awaiting a PDF. Supply the PDF again.

A finished job (ready, imported, unavailable, failed, or cancelled) cannot take a PDF. Submit the work again, for example with papio acquire --doi <doi>, and then supply the PDF to the new job id. The command prints this remedy when it refuses a finished job.

papio stats producers counts a supplied PDF as manual.

Use papio jobs add-component <job-id> <path> --role supplement (or appendix) for supplements and appendices. The job must be ready or imported. The file can be anywhere on this computer: papio copies it into its data directory and leaves your original in place. The MCP command tools do not offer supply-pdf or add-component, because they read a file on this computer.

6. Read the batch outcome

Ask for a joined view of the original batch manifest, live job state, events, and human actions:

papio batch report latest --markdown

Use a concrete batch ID instead of latest when tracking more than one run. Without --markdown, the command prints the normal table; --json provides the structured report. Outcomes include imported, browser-fetched-then-imported, existing-item-attached, acquired (validated but not imported, the normal outcome without --auto-import), import-failed, awaiting-human, needs-review, failed, skipped-owned, and in-progress.

Archive an acquisition you do not intend to file

papio jobs archive <job-id> --confirm
papio jobs disposition <job-id>
papio jobs get <job-id>
papio jobs restore <job-id>

Archive applies only to a validated ready acquisition. It records an audit event and removes the job from actionable ready views and uncollected warnings. The PDF, identity evidence, and cached-artifact references remain intact. Restore makes the job actionable again; neither command deletes PDF bytes. Import backfill excludes archived jobs.

Archive refuses jobs with a Zotero apply receipt, including a failed receipt, because an external mutation can report failure after committing. It also refuses an acquisition while a hook execution reservation remains unresolved. See the hook execution limits.

7. Turn a successful search into a watchlist

A watch repeats discovery, ownership filtering, capped submission, auto-import policy, collection routing, and notifications on a schedule:

papio watch add "appropriate reliance on AI" \
  --cadence weekly --limit-per-run 10 --collection "AI reading" --oa-only
papio watch list
papio watch run <watch-id>
papio watch resume <watch-id>
papio watch remove <watch-id>

--cadence accepts daily, weekly, or Nh; --limit-per-run accepts 1 through 50. --year-from and --year-to apply the same publication-year limits as search. Watch execution is serial, records its last result, and auto-disables a watch after five consecutive failures. To bring a disabled watch back, fix the cause, force a run with papio watch run <watch-id>, and once that run succeeds, run papio watch resume <watch-id>. The watch keeps its id, query, and digest history, and its next scheduled run comes one cadence after the forced run. Resume refuses while the latest run is still a failure. Removing a watch does not remove jobs or Zotero items created by earlier runs. It does delete the watch's digest, so papio watch remove refuses a watch that still has pending digest works until you add --discard-digest.

Change settings without resetting history

papio watch edit <watch-id> --cadence weekly --limit-per-run 10 \
  --collection "Revised reading"

Edit changes the label, cadence, per-run limit, collection, and OA/year filters. It keeps the watch id, digest, and previous run history. Query, citation seed, and alert/acquire mode do not change through edit; create a new watch for those changes. Editing an alert does not turn it into automatic acquisition.

Backfill a non-Zotero citation source

papio watch backfill add --source citations --cadence daily --limit-per-run 10
papio watch run <watch-id>

Name a configured record_present library source explicitly. The watch reads the source before submitting jobs, excludes positive pdf_present claims, and records durable once-only submission receipts. A malformed or incomplete ownership feed prevents submission. A BibTeX file field never proves PDF ownership. The Zotero backfill path remains separate.

Use the managed folder destination or an existing best-effort hook for non-Zotero filing.

How far a watch looks

Each run reads the first page of results, because new papers appear there. When that page holds nothing new, the run reads deeper pages, up to four pages of at most 25 results for each discovery source. A work is new when you do not already hold it and, for an alert watch, when it is not in the watch's digest. A digest entry you cleared or acquired still counts, so an alert watch never reports the same work twice. An acquire watch treats a paper as new until it is in your library, so a paper that is still being acquired keeps the run on the first page.

The watch stores where each source's deeper scan stopped, and the next run continues from that position instead of reading the same pages again. When a source reports the end of its results, the next deeper scan starts again after the first page. A stored position that no longer fits the watch's search is discarded, and the scan starts again; this is not a failure.

A run that could not look at every result is not a clean, empty success. papio watch list shows the reason after last run:, and papio watch run adds discovery scan incomplete:

  • scan limit of 4 pages reached with more results left: the next run continues from the stored position.
  • <source>: <error>: the source failed, or its request budget is spent. The source keeps its stored position, and the next run tries it again.
  • later results lie past the source's result window: arXiv and Semantic Scholar search do not page past a fixed number of results.
  • cannot page past its first results: the source returns only one page.

Result order is not fixed. OpenAlex and Semantic Scholar rank by relevance, and the ranking changes as their catalogues grow, so a deeper scan can repeat or miss a work. arXiv lists the newest papers first, so it can only repeat a work. The deeper scan is a rolling sweep, not proof that papio saw every match.

Alert-only watches: report first, acquire on demand

--mode alert runs the same discovery and ownership filtering but records new works in a per-watch digest instead of acquiring them — each work is reported once, ever:

papio watch add "appropriate reliance on AI" --cadence weekly --mode alert
papio watch digest <watch-id>            # review what's new
papio acquire --from-digest <watch-id>   # queue everything pending
papio acquire --from-digest <watch-id> --keys 10.1000/example  # or just some
papio watch digest clear <watch-id> --all  # discard every pending work

Acquired entries leave the digest automatically; cleared ones simply stop being pending (they will not be re-reported). Clearing discards every pending work, including works papio watch digest did not show (it lists 100 by default), so it requires --all; --dry-run counts them first.

Alert watches check ownership twice: when the run records new works, and again when --from-digest acquires them. Both checks use zotio when it is configured and your library.sources otherwise, with the same rules as acquisition watches. A digest entry you already hold leaves the digest without a job. If a library source cannot be read, the run fails and --from-digest creates no jobs, rather than report an unknown work as new.

Backfill watches: acquire missing PDFs

--kind backfill takes no query — each run queues Zotero items that are missing an attached PDF, exactly like papio acquire --from-zotio, bounded by --limit-per-run:

papio watch add --kind backfill --cadence daily --limit-per-run 10 \
  --collection "AI reading"

Re-runs are idempotent: already-queued or completed items are skipped. Items whose last attempt ended unavailable wait for zotio.unavailable_recheck_days (default 14) before being checked again.

See exception state inside Zotero

With zotio.exception_tags = true (requires zotio ≥ 0.13.0), the daemon maintains two automatic tags on linked items in your personal library. These tags show acquisition exceptions directly on the linked Zotero items:

  • papio:needs-action — acquisition is parked on you (SSO login, terms consent, identity review). Open your browser; the extension shows the prompt.
  • papio:unavailable — every OA and institutional route failed as of the last attempt. A saved search for this tag lists items that need another access route or manual follow-up. The tag clears itself if a later re-check succeeds or you attach a PDF manually.

Nothing else is tagged: a clean acquisition's only trace is the attached PDF. Tags converge with job state on the daemon's maintenance cadence; papio zotio tags reconcile forces one pass. Both are automatic-type tags, so Zotero's tag selector can hide the whole namespace, and you can assign colors as you prefer.

papio never retypes or removes a same-name manual tag. Before uninstalling, disable the feature, restart the daemon so it reloads that setting, then force the cleanup pass:

# after setting zotio.exception_tags = false
papio daemon stop
papio zotio tags reconcile

The pass removes only automatic tags owned by papio.

Mark retracted papers in Zotero

With zotio.retraction_tags = true, a Zotero item whose DOI exactly matches a current retraction notice gets the automatic tag papio:retracted. Corrections and expressions of concern are never tagged. Preview before you write:

papio zotio tags retractions            # preview only
papio zotio tags retractions --apply    # write the tag

The preview works while the option is off, so you can review the effect before you enable it. To add tags, turn on the option and run papio daemon stop so the daemon reloads the setting. With the option on, the daemon repeats the pass when the notice set changes and at most once a day. Acknowledging the inbox notice does not remove the tag. When a notice is withdrawn, or when you turn the option off, run papio daemon stop before papio zotio tags retractions --apply. With the option off, --apply only removes the tag papio owns. A manual tag with the same name stays as it is. Items outside the retraction check's scope get no notice and no tag; set retraction.scope = "library" to cover your whole library.

Triage failures

When acquisitions die, see where they cluster before digging into single jobs:

papio jobs failures --since 30d

Rows group by state, provider host, and terminal reason with a sample job id for papio jobs get.

Incident rows show a keyed fingerprint plus bounded safety_domain and registrable host_family labels for local diagnosis. The fingerprint omits raw hosts and identifiers and is keyed per installation, so it resists stable cross-install correlation; those bounded labels are intentionally visible in local jobs failures and jobs incidents output.

8. Resolve identity reviews deliberately

A PDF can be well-formed yet still land in needs_review when papio isn't sure it's the paper you asked for. papio actions list --json shows the open verify_identity action with the path to the quarantined file (quarantine_path), its digest (quarantine_sha256), and the action's revision. Open that file and check it before deciding:

papio actions resolve <action-id> --accept --revision <revision> --sha256 <quarantine_sha256>
# or
papio actions resolve <action-id> --reject --revision <revision>

The revision and digest bind your verdict to the review you inspected. If the action or its file changed after you listed it, nothing is applied and the command exits nonzero; list it again and re-inspect the file.

--accept states that you opened the quarantined PDF and confirmed it is the work you wanted. The daemon imports that same file — no second download — and records the result as user_confirmed, not as an automatic match. If the quarantined file has since been removed or altered, the candidate is fetched again instead. --reject records that it is not the right work and cancels the review. Resolution applies only to an open verify_identity action. It does not waive an encrypted or active-content rejection, or a wrong-work rejection other than a conflicting front-matter DOI. For that one case, --accept on the exact digest you reviewed overrides the rejection and records user_confirmed.

See your acquisition history and impact

The extension popup shows a compact Your papio impact summary — papers acquired and your success rate — with a View history link that opens a full-tab history page. Both figures are counted from your own job records: papio reports what it did and does not estimate time saved, hours of searching avoided, or any other figure it cannot measure.

The history page adds a 12-week chart of weekly acquisitions, your success rate (acquired vs. failed), a breakdown by access route (open access / institutional / licensed API / other), and how often an acquisition needed a human handoff. Every figure is an aggregate computed locally from your own job history — nothing is sent anywhere to produce it. Against an older daemon that doesn't support the feature, the popup hides the summary and the history page shows a muted "stats unavailable" note instead of an error.

See who produced each paper

Every paper that reaches ready records who produced the file and whether a person had to help. Use this to see how much of your work papio finishes without you, for example after you turn on the paced drive or the article agent:

papio stats producers                 # the last 24 hours
papio stats producers --since 168h    # the last 7 days
papio stats producers --since 2026-09-01T00:00:00Z --until 2026-10-01T00:00:00Z

--since takes an RFC 3339 time, a Go duration such as 168h, or a whole number of days such as 7d. --until takes an RFC 3339 time and defaults to now. Add --json for the full breakdown, which also counts who opened each handoff.

The first line gives the total, split into unattended (no person needed), sign-in only (only an institutional sign-in), and intervened (anything more). No producer record counts papers that reached ready without a record: papers from before papio 0.22.0, and papers served from a copy papio already had. The producers are:

Producer Meaning
adapter The extension downloaded a PDF that the page declared, inside a drive that papio started, with no agent decision.
agent The article agent chose the control that produced the download.
viewer_capture The extension saved the one response of a signed PDF viewer, such as ScienceDirect's, in a tab that papio armed for the paper: Firefox kept a copy of the PDF, or Chrome's download rule saved it. The record names the adapter that matched the paper's page. It does not record who pressed View PDF.
daemon_fetch papio chose the address: it fetched the file itself, or the browser fetched a public address that papio selected.
manual A person supplied the file: a PDF you sent from an open tab, a download that came after the adapter had stopped, or a file you gave with papio jobs supply-pdf.
unknown A browser download with no record of who clicked. A paper that the removed macOS helper of papio 0.22 saved also counts here.

The interventions it counts are open (someone other than the paced drive opened the handoff), sign_in, terms, challenge, review, and manual_file. A handoff that the paced drive opened is not an intervention.

Know one limit: unknown often includes downloads that a packaged adapter made in a sign-in handoff tab. The extension does not tell papio which adapter clicked in that tab, so an adapter click and your own click look the same, and papio does not guess. Read a high unknown count as "not recorded", not as "done by hand". A signed viewer PDF that the extension saved counts as viewer_capture only with extension and daemon versions that both support the record; with an older extension or daemon it counts as unknown.

For one paper, papio jobs get <job-id> prints the record on its artifact.producer line, for example producer=adapter … opened_by=pacer unattended.

Why a batch parks

A batch report labels awaiting_human work with one of these reasons:

Reason Meaning Next action
institutional No direct candidate completed; an institutional OpenURL handoff is waiting. Sign in to your institution first, then open the handoff. Open the queue, sign in through ordinary Chrome if needed, and complete the allowed provider flow. If the provider reports a stale session, re-run papio actions open for a fresh link.
oa_browser The work is open access — no login needed; its URL just refuses non-browser downloads. Use the offered browser handoff; the browser may download through its existing cookie jar or present a page for you.
terms The extension observed terms acceptance is required. Without recorded auto-accept consent, read and decide on the publisher's terms yourself; papio does not accept them for you. With explicit persistent consent and a supported accept rule, papio can accept them on your behalf.

needs_review is separate from these browser states: it is an identity decision on a quarantined file. openurl_available is an advisory action in conservative mode; it records that institutional access exists but was not opened automatically.

Work with no fetchable identifier never parks for a sign-in at all. If no DOI, PMID, or arXiv id can be confirmed — the usual case for books, chapters, reports, and theses — the job settles unavailable with the reason no_identifier instead of opening an institutional handoff. A library login or retrying the same identifier-less request cannot make it fetchable, so papio says so rather than spending your sign-in on it. Find a DOI and re-submit a manual request with papio acquire --doi <doi>; for a Zotero item, apply zotio --yes items enrich --missing-doi and re-run papio acquire --from-zotio. An ISBN alone is carried into the institutional link so a monograph is described as a book; it is not a fetchable identifier, and the only source that reads it is the open-book lookup described next.

A DOI that does not exist is treated the same way. Before opening an institutional handoff for a work whose only identifier is a DOI, papio asks the DOI system whether that handle is registered at all. Metadata sources cannot answer this — Crossref, OpenAlex, EuropePMC and Unpaywall all report "no record" and "no open copy" as the same empty result — so a mistyped DOI used to reach the link resolver, match nothing, and drop you on a "DOI NOT FOUND" page with a handoff that could never be completed. An unregistered DOI now settles unavailable with the reason doi_not_registered; correct the DOI and re-submit. If the registry itself is unreachable the handoff is offered as before, because an outage must not terminate fetchable work.

ISBN-only requests

papio acquire --isbn <isbn> (with no DOI, PMID, arXiv or OpenAlex id) asks the open_books source for an open-access copy. It searches the DSpace 7 repositories in [open_books] dspace_servers (the World Health Organization's IRIS by default) and, when open_books.oapen = true, the OAPEN Library. It makes no request at all for a work that also has another identifier, because an ISBN names a whole edition and a book chapter must be fetched as the chapter.

A repository record becomes a candidate only when all of these hold:

  • the record's own ISBN field lists the ISBN you asked for (a search also returns documents that merely mention the number, and papio discards them);
  • the record carries an open licence statement, or the repository is open access by definition (OAPEN);
  • the record holds exactly one PDF in its ORIGINAL bundle. A record with several PDFs, or with none, is refused rather than guessed at.

Most ISBNs are commercial books with no open copy anywhere, and for those the lookup finds nothing: the job ends as described above. When a copy is found, papio still checks the file. You gave no title to compare, so the repository's title is used. The file is accepted without review only when its front matter prints the ISBN and also prints the complete title, every word including short words and numbers, as its own lines (not inside a sentence and not under a "references" or "see also" heading). A file that does not is parked for review (needs_review), never accepted on the repository's word alone, so a different edition of the same title is parked. A title or authors you supplied are never replaced: a title you gave is the title the file must carry, and authors you gave keep the normal review rules in force.