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 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:
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):
--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:
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:
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:
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:
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:
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:
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:
Then open the current handoff URLs:
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.
Use your library's OpenAthens link¶
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.
- 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 exampleexample.edu. Your library's help page can also tell you. - 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
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:
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; orBLOCKED, 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:
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 isready.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:
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¶
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:
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:
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:
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:
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.