Snowball¶
Status: keyword search, DOI / ORCID / collection seeds, hybrid (keyword
hits, then one hop), refs, cited-by, and OpenAlex keywords, depth up to 5 under caps, gates
dry-run / approve-each / approve-batch / auto, overlap ranking,
--fetch-pdfs, and pull-only watch (baseline then propose new arrivals) are
implemented. Config honors dedupe_scope, tag_prefix,
types, oa_only, venues, languages, min_seed_citations,
note_provenance, and backends. --refine writes query suggestions when
[llm] is on and does not create items. expand = cited_authors stays off.
Phases live in ROADMAP.
Snowball grows a library outward from a keyword, a DOI, a person, or an
existing collection. It proposes works, then creates items under an explicit
gate. run fills PDFs for items that already exist. fetch_pdfs
can do that in the same process, limited to the keys just created. How far
one command reaches is drawn below.
paperful snowball search "area based management tools" --year-from 2018
paperful snowball doi 10.1038/s41586-021-03819-2 --depth 2 --direction both
paperful snowball orcid 0000-0002-9162-9618
paperful snowball collection "Inbox/Seeds" --direction refs
paperful snowball run --profile doi-refs-gated
paperful snowball apply <run-id> -C "Inbox/Snowball"
paperful snowball hybrid "high seas EIA" --hybrid-seeds 5 --direction refs
paperful snowball search "high seas EIA" --gate auto --fetch-pdfs fast -C "Inbox/Snowball"
paperful snowball orcid 0000-0002-9162-9618 --gate auto --fetch-pdfs full -C "Snowball/0000-0002-9162-9618"
paperful snowball watch save bbnj --profile keyword-scout
paperful snowball watch run bbnj
search, hybrid, doi, orcid, and collection are seeds under one verb.
hybrid is the keyword-then-hop job. There is no separate top-level
harvest, crawl, or discover. watch re-runs a saved profile on a
schedule you choose; see Watch.
Watch¶
A watch is a saved snowball profile plus a seen-set on disk. You run it when
you want. Paperful does not schedule it (your own launchd or cron may call
watch run). The first run records the current frontier and proposes nothing.
Later runs write only works that were not in that set. Creating items stays on
snowball apply (or a separate writing-gate crawl). Watch always forces
gate = dry-run and fetch_pdfs = off, so a profile with gate = auto cannot
create parents or download PDFs from a watch.
paperful snowball watch save bbnj --profile keyword-scout
paperful snowball watch run bbnj # first time: baseline, inbox empty
paperful snowball watch run bbnj # later: N new in the inbox
paperful snowball watch show bbnj
paperful snowball apply <run-id> -C Inbox/Snowball # only if you want parents
Ledger under state/snowball/watches/<name>/:
File |
Role |
|---|---|
|
|
|
Identities already recorded ( |
|
Append-only proposed |
Each watch run also writes a normal state/snowball/<run-id>/ queue. After
baseline, that queue is empty (baseline N · proposed 0). After a later run,
it holds only the new proposals (proposed N · already seen M), with
keep = true so snowball apply can create them. OpenAlex keyword and
cited-by calls pass from_created_date from the cursor (YYYY-MM-DD of the
last run) so a work indexed after the last check can show up even when its
publication year is old. The seen set still drops overlaps. A hybrid profile
watches the keyword hit list only (depth 0); a refs hop of those hits stays a
separate snowball run.
Snowball and run¶
Job |
Command |
What you are doing |
|---|---|---|
Build |
|
Find works and, if the gate says so, create parents |
Fill |
|
PDFs and attach for items already in the library |
A dry-run ends with “candidates ready”. A writing gate with fetch_pdfs = off
ends with “items created (metadata only)”. Downloaded and attached appear in
the summary only after fast or full has run.
A snowball profile is a named job in profiles/. paperful run and
paperful all refuse it. Fetch profiles stay fetch profiles.
Seeds¶
Seed |
What it collects |
Default depth |
|---|---|---|
Keyword |
OpenAlex title/abstract search |
0 — the hit list. Depth 1+ expands those hits and must be set explicitly. A global |
Hybrid |
That hit list, then one hop from the top |
The hop is always 1. |
DOI |
The work’s neighbours ( |
1, direction |
ORCID |
That person’s works (ORCID public API, filled by OpenAlex author filter), then the same expander |
1. One hop out from those works |
Collection |
DOIs already in the seed collection path, then the same expander |
1. |
expand = cited_authors (every paper by every cited author) stays off. It
is a later, capped switch.
How a hop is cut¶
An ORCID run starts at that person’s own works (hop 0). --depth 1 takes
one hop out from those works and stops. It does not walk the neighbours’
neighbours. --direction both takes references and citing works. Each side
is capped on its own: --per-hop-limit 25 keeps 25 references and 25 citing
works per paper. --per-hop-rank chooses which ones: most-cited (default),
least-cited, or random. Use all (or 0) on either cap for every
neighbour or every row. After that hop, max_candidates (default 200) can
still cut the new + exists list.
paperful snowball orcid 0000-0002-9162-9618 --depth 1 --direction both --per-hop-limit 25 --per-hop-rank most-cited --max-candidates all
flowchart TB
person["ORCID"]
own["Own works · hop 0"]
person --> own
paper["Each of those papers"]
own --> paper
refs["References · 25 kept"]
cites["Citing works · 25 kept"]
paper -->|"direction both"| refs
paper -->|"direction both"| cites
list["Own works plus that hop"]
own --> list
refs --> list
cites --> list
trimmed["Cut to 200 new and exists"]
kept["Every new and exists row"]
list -->|"max-candidates 200"| trimmed
list -->|"max-candidates 0"| kept
DOI and collection seeds use the same hop. The centre is the seed papers, not a person’s works. Keyword search stays on the hit list unless you set depth. The usual numbers are in Stop rules.
direction including keywords adds another side. OpenAlex stores at most
five scored keywords on a work. The hop takes the top keyword_limit (default
3) at or above keyword_min_score and requests works with any of those slugs
(keywords.id:a|b|c), then keeps keyword_hop_limit of them. all is refused
on both keyword caps: the matching literature is not a closed neighbour list.
Works with no topic have no keywords. That is about 12% of OpenAlex. When keywords are in the direction, the run says how many seeds have none and will not expand on that side:
2 of 5 seeds have no OpenAlex keywords and will not expand on that side.
Those seeds are skipped on the keyword side only. References and cited-by still run. If keywords is the only side and every seed lacks keywords, the command exits 2 with that sentence and does not query.
Gates¶
The gate is a config value. The default for a new user is dry-run.
Gate |
Library writes |
When |
|---|---|---|
|
None |
Always safe. |
|
Only |
|
|
Each yes on |
Short lists, at most |
|
Every |
A named profile, after you have dry-run that job once |
dry-run, approve-batch, approve-each, and auto are the gates. A writing
gate (approve-batch, approve-each, auto) requires a target collection and
stops before the crawl if it is missing. Paperful may create that collection
path. It does not invent a silent default collection. auto is not a scheduler.
Dedupe runs before create: normalized DOI against dedupe_scope (library
by default, or collection, or none logged loudly), then the existing
title+year fingerprint. Only status = new rows can become parents.
One-shot PDFs¶
fetch_pdfs is a mode. The default is off.
Mode |
What happens after create |
|---|---|
|
Metadata only. Fetch later with |
|
First pass on the new keys: configured sources, no vault browser. |
|
That first pass, then the same stack as |
paperful snowball search "BBNJ EIA" --gate auto --fetch-pdfs fast -C "Inbox/Snowball"
paperful snowball orcid 0000-0002-9162-9618 --gate auto --fetch-pdfs full -C "Snowball/me"
Both modes stay on the keys snowball just created. Sci-Hub and Scholar run
only when they are already in sources. full prints the Sci-Hub and
browser-recovery lines when those lanes are in the second pass. If attach
fails, the parent remains and the report counts attach_deferred.
Example profile keyword-library: gate = auto, fetch_pdfs = true (fast),
target_collection set. keyword-scout is the dry-run twin.
doi-refs-gated is approve-batch. orcid-ego-auto creates metadata only.
Stop rules¶
The picture is How a hop is cut.
Knob |
Default |
Meaning |
|---|---|---|
|
0 for keyword, 1 for DOI / ORCID / collection |
Hops from the seed. Soft ceiling 5 |
|
|
|
|
3 |
How many of a work’s OpenAlex keywords to expand. Integer 1–5. |
|
50 |
Works kept per seed on the keyword side. A positive integer. |
|
0 |
Drop seed keywords below this similarity. 0 keeps whatever OpenAlex already assigned |
|
200 |
Stops after this many |
|
50 |
Neighbours kept per seed work per hop (references and cited-by). |
|
|
How a numeric |
|
unset |
Drop candidates outside the window |
|
journal-article-shaped |
OpenAlex / Zotero types |
|
false |
Metadata filter only. It does not change the PDF chain |
|
0 |
Skip cited-by expansion when the seed is below this count |
|
unset |
Applied when the source has the field. A missing language does not drop the row |
|
5 |
How many top DOI hits |
|
20 |
Largest |
|
false |
With |
A seed that fails to resolve is status = error. Other seeds continue. The
process exits non-zero if any seed failed, using the existing exit ladder
(2 for config / doctor).
Progress¶
A long crawl keeps a progress bar at the bottom of the terminal, the same bar run and attach use. The label is coloured by step: cyan for hops and references, blue for cited-by, magenta for Crossref and Semantic Scholar, green while items are created, cyan while PDFs are fetched. When the step has a known length (reference ids, cited-by seeds, rows to fill, items to create, PDFs to fetch) the bar shows done/total. Otherwise it spins. A stage line is printed when the step changes (hop 1/2 · 40 seeds · both). When the run finishes, one summary line remains:
hop 1/2 references · 80 ids · 40 searches · 800 papers
crossref · 30 searches · 15 fields updated
fetching PDFs · 4 PDFs
creating · 12 created
Counts start over when the step changes, so a references hop does not repeat fill or PDF totals. searches counts reads in that step (OpenAlex, Crossref, or Semantic Scholar). papers counts OpenAlex works returned. fields updated counts empty title, year, venue, or author fields filled. PDFs counts files saved while fetch_pdfs runs. created counts items written while they are created.
Candidate record¶
Each run writes state/snowball/<run-id>/candidates.jsonl and summary.json.
The summary counts requests, HTTP 429s, retries, and rows by status, backend,
and hop.
Schema paperful.snowball.candidate.v1:
Field |
Contents |
|---|---|
|
Version and run id |
|
|
|
Integer. Search hits are 0. References of a seed are 1 |
|
|
|
Normalized doi, openalex, s2, pmid, orcid, when known |
|
Title, year, authors, venue, type, optional OA url |
|
Short reason, for example |
|
|
|
When |
|
Backend, endpoint, |
|
The gate for this run |
|
|
The dry-run table shows title, year, DOI, why, in library, hop/direction,
and backend, with new rows first. Created items are tagged
paperful-snowball and paperful-snowball:<backend>. A child note holds the
seed, direction, hop, why, run id, and schema version when note_provenance
is on. API keys and the mailto address never go in that note.
A separate readable line follows [remarks].surface (a child note tagged
paperful-linked by default):
“Cited by 4 papers in this collection.” when at least one item already in the target collection lists this work in its references. Paperful fetches those OpenAlex reference lists once, caches them under
state/cites/, and reuses the cache while the collection’s DOIs stay the same. If the OpenAlex budget runs out, the item is still created and this sentence is skipped.When the hop is at least 1 and at least two seeds from this run point at the work: “In the bibliography of 2 of the papers you started from.”, “Cites 2 of the papers you started from.”, or “Linked to 2 of the papers you started from.” Search hits (hop 0) get only the collection sentence.
Config¶
Precedence matches run profiles: built-in
default, then [snowball] in config.toml, then profiles/<name>.toml,
then flags on the command.
[snowball]
enabled = false # doctor and the CLI refuse snowball until true
depth = 1 # keyword runs still default to depth 0
direction = "refs"
max_candidates = 200
per_hop_limit = 50 # all keeps every neighbour of a seed
per_hop_rank = "most-cited" # most-cited | least-cited | random
gate = "dry-run" # dry-run | approve-each | approve-batch | auto
target_collection = ""
dedupe_scope = "library" # library | collection | none
fetch_pdfs = "off" # off | fast | full. true means fast
tag_prefix = "paperful-snowball"
note_provenance = true
backends = ["openalex", "crossref", "semanticscholar", "orcid", "europepmc", "pdf"]
languages = [] # empty = do not filter; a missing language is kept
min_seed_citations = 0
hybrid_seeds = 5
approve_each_max = 20
refine = false # suggestions only; needs [llm].enabled
enabled = false until you opt in, the same posture as [llm]. doctor
reports that flag, whether keys are present, and whether a backend answers.
A missing OpenAlex key warns. A missing Semantic Scholar key does not, and neither missing key aborts the other backends.
email at the top of config.toml is the contact address for Unpaywall and Crossref.
OpenAlex ignores mailto. Snowball calls OpenAlex without an API key first, so a small search needs no account. That free allowance is shared by everyone on the same public address (a campus network or VPN exit included) and is about a tenth of a free key. When it runs out, a configured OPENALEX_API_KEY takes over for the rest of the crawl. With no key, the partial queue is kept and paperful snowball resume continues after you add one. One key only: a second free key or a user+tag@gmail.com alias does not add budget. A free key is about $1/day. Past that, OpenAlex sells pay-as-you-go credit and subscriptions.
Semantic Scholar’s Academic Graph is public, so snowball calls it with no key. That unauthenticated pool is shared and can be throttled. Heavier use needs a private key: request one (it arrives by email; the introductory limit is 1 request per second). Put it in SEMANTIC_SCHOLAR_API_KEY. Like OPENALEX_API_KEY, it stays in the environment, never in config.toml.
Each crawl writes state/snowball/<run-id>/candidates.jsonl as it goes, for every configured backend. A rate limit, outage, or interrupt keeps that file and deferred.json. Continue with:
paperful snowball resume <run-id>
A list call that hits the daily budget of the key already in use stops the same way. The reset is midnight UTC, or sooner if you add pay-as-you-go credit or a subscription on that key.
Short 429s and 5xx responses retry with exponential backoff. A reset of a minute or more does not keep polling until midnight.
snowball profile save writes seeds and knobs only, after a successful
dry-run, or with --force. It refuses to store a key.
Backends resolve in the configured order and emit each work once, keyed by DOI.
The default is OpenAlex, then Crossref (empty fields and DOI references), Semantic Scholar
(reference gaps, with or without a key), ORCID (the person’s own work list), Europe PMC
citations, then an open-PDF bibliography. Reorder backends when a key or proxy makes
a later source the better first try. OpenAlex wins when it and a later backend
disagree on a field that both filled. A 429 or 5xx pauses that backend and the pass
continues with the next one. After the pass, each paused backend is tried once more
on only its remaining DOIs. DOIs still blocked stay in deferred.json. Items no
paused backend still lists are created, and their PDFs are fetched when fetch_pdfs
is fast or full. An open PDF already downloaded for the bibliography is written
onto the item. Semantic Scholar responses are cached under state/snowball/cache/.
backends must include openalex. An unknown name is a config error. Dropping
orcid skips the public works list and keeps the OpenAlex author filter.
When OpenAlex lists no referenced_works for a seed, a refs hop does not treat
that as “cites nothing”. It asks Semantic Scholar, then Europe PMC, then an open
PDF bibliography, and resolves entries back to OpenAlex (DOI match, or a strict
title match). A pause on one of those sources does not stop the hop. Cited-by
stays OpenAlex-only.
snowball run --profile NAME prints that profile’s one-line description
before any request. mode is search, hybrid, doi, orcid, or
collection. snowball profile save --query … --hybrid stores mode = "hybrid".
--refine (or refine = true on a profile) asks the configured model for
query strings and prints them. If [llm] is off, or the call fails, the crawl
still finishes and summary.json records suggestions_error. Suggestions are
not seeds.
Where the crawl comes from¶
The personal site already does this hop for a people graph, not for Zotero:
Site |
Snowball |
|---|---|
|
One-hop |
|
|
|
Optional Semantic Scholar fill, disk cache under |
|
DOI, ORCID, and title normalization, aligned with |
No Zotero writes |
|
Leave on the site: the people-graph JSON, the hard-coded ego slug, Jekyll
outputs, and scholar_citations.py.
What to reuse elsewhere¶
OpenAlex is the graph (search, author, referenced_works, cites:). The
ORCID public API is the person’s own works; those lists are often incomplete,
so OpenAlex expands them. Semantic Scholar references and citations fill gaps
on the public API; a key is only for heavier use. Crossref, already used to verify DOIs, fills metadata
gaps. It is not the forward-citation graph.
Adjacent tools, and the piece worth copying:
Tool |
Copy |
Leave there |
|---|---|---|
|
The package, and any Scopus / Web of Science / IEEE requirement |
|
— |
DOI-list PDFs when you have no Zotero. See comparison |
|
Flag ideas |
The review project, the report, the screener |
|
Overlap rank: neighbours score |
The network UI |
|
— |
In-Zotero one-hop dialogs |
|
— |
A second HTTP client. Snowball extends the client paperful already uses for OpenAlex (mailto, sleep, backoff) |
ResearchRabbit, Litmaps, Connected Papers, Inciteful, Elicit, Consensus,
Scite, and Lens.org stay outside. --refine can suggest further queries behind
[llm]; it does not pick seed DOIs. Google Scholar stays the
existing PDF lane. It is not a snowball source. Screening a RIS file stays
with ASReview. Created items keep clean creators, DOI, and date so Better
BibTeX citekeys still make sense. Snowball does not depend on that plugin.
Out of this lane¶
A hosted discovery service, a force-directed graph, or a second OpenAlex browser
A built-in scheduler or cron helper (call
watch runyourself), or a snowball step insidepaperful allA TUI or local web reviewer
Sci-Hub or Scholar as ways to discover works
Pulling every publication of every cited author, until an explicit capped switch exists