Advanced configuration, automation, and troubleshooting.
Run paper suggestions and weekly digests automatically from GitHub Actions, without needing a laptop running. Your local machine handles the sync (Zotero to reMarkable to notes); GitHub Actions handles the emails.
The flow works like this:
distillate locally to sync papers and process highlightsdistillate --sync-state to upload your state.json to a private GitHub Gist--suggest-email (daily) or --send-digest (weekly) and sends the emaila. Create a private Gist to hold your state
# Create the gist with your current state
$ gh gist create state.json
# Get the gist ID (you'll need it for the secret)
$ gh gist list
b. Create a GitHub classic PAT
Go to github.com/settings/tokens and create a classic Personal Access Token with the gist scope. This token lets the workflow read your state from the Gist.
c. Add repository secrets
In your fork's Settings > Secrets and variables > Actions, add these secrets:
| Secret | Description |
|---|---|
ZOTERO_API_KEY | Your Zotero API key |
ZOTERO_USER_ID | Your Zotero user ID |
ANTHROPIC_API_KEY | For AI-powered suggestions |
RESEND_API_KEY | For sending emails |
DIGEST_TO | Your email address (recipient) |
DIGEST_FROM | Sender address (configured in Resend) |
STATE_GIST_ID | ID of the private Gist from step (a) |
GH_GIST_TOKEN | Classic PAT with gist scope from step (b) |
OBSIDIAN_VAULT_NAME | Your vault name, for deep-links in emails |
d. The workflow is already included
The file .github/workflows/emails.yml is part of the repo. It runs on two schedules:
schedule:
# Daily suggestion at 8am ET (1pm UTC)
- cron: '0 13 * * *'
# Weekly digest on Sunday at 9am ET (2pm UTC)
- cron: '0 14 * * 0'
e. Keep your state in sync
Run distillate --sync-state after each local sync so GitHub Actions has current data. You can add it to your schedule or run it manually.
GITHUB_TOKEN cannot access Gists. You need a classic Personal Access Token with the gist scope. Fine-grained tokens also do not support Gist access.
obsidian:// links to open notes directly in your vault.
--sync-state after every local sync. If the Gist state is stale, suggestions will be based on outdated reading history.
You can trigger the workflow manually from the command line:
$ gh workflow run emails.yml -f command=--suggest-email
$ gh workflow run emails.yml -f command=--send-digest
Every processed paper gets an engagement score from 0 to 100, measuring how deeply you interacted with it. The score appears in your notes (YAML frontmatter), --status output, --digest, and email digests.
Three components, weighted:
Each component is normalized to 0.0–1.0, then the weighted sum is scaled to 0–100:
score = round((density * 0.3 + coverage * 0.4 + volume * 0.3) * 100)
A paper you skimmed lightly might score 15–25. A paper you highlighted thoroughly across most pages will score 70+. The score is a quick signal for which papers you actually engaged with versus those you just scanned.
Use --reprocess to re-extract highlights and regenerate notes for a paper that's already been processed. Common reasons:
# Substring match on the paper title
$ distillate --reprocess "Attention Is All"
What it does: re-downloads the bundle from Saved/ on your reMarkable, re-extracts highlights, re-renders the annotated PDF, regenerates the AI summary, and updates both the markdown note and the reading log.
Distillate uses two model tiers, configurable via environment variables in your .env:
CLAUDE_SMART_MODEL (default: claude-sonnet-4-5-20250929) -- used for paper summaries and one-liner descriptionsCLAUDE_FAST_MODEL (default: claude-haiku-4-5-20251001) -- used for paper suggestions and key learnings# In your .env file:
CLAUDE_SMART_MODEL=claude-opus-4-6
CLAUDE_FAST_MODEL=claude-sonnet-4-5-20250929
Without ANTHROPIC_API_KEY set, AI features are skipped entirely and papers use their abstract as a fallback summary.
The KEEP_ZOTERO_PDF setting controls whether the original PDF stays in Zotero cloud after being uploaded to your reMarkable.
true (default) -- PDF remains in your Zotero libraryfalse -- PDF is deleted from Zotero cloud after it's confirmed saved locally and uploaded to reMarkable# In your .env file:
KEEP_ZOTERO_PDF=false
This is useful if you're on Zotero's free tier (300 MB storage). The safety order is: the PDF is first saved to your local Distillate/Inbox/ folder, then uploaded to reMarkable. Only after both succeed is the Zotero cloud copy removed. Your local copy is always preserved.
Set LOG_LEVEL=DEBUG for verbose output:
# One-off
$ LOG_LEVEL=DEBUG distillate
# Persistent (add to .env)
LOG_LEVEL=DEBUG
Behavior depends on how distillate is running:
~/.config/distillate/distillate.logDebug output includes: rmapi commands and responses, API call details, file read/write operations, and state changes. Useful for diagnosing issues like "why didn't my paper sync?" or "why are highlights missing?"
Distillate tracks all your papers in a local state.json file. The --sync-state command uploads it to a private GitHub Gist, which enables two things:
# Upload state to your Gist
$ distillate --sync-state
Requires STATE_GIST_ID and GH_GIST_TOKEN in your .env.
If state.json becomes corrupted (malformed JSON), distillate automatically backs it up as state.json.bak and starts with a fresh state. No data is silently lost -- the backup file preserves whatever was there.
state.json is plain JSON. You can read it directly, pipe it through jq, or edit it if you know what you're doing:
# Pretty-print your state
$ cat state.json | jq .
# Count processed papers
$ cat state.json | jq '.documents | length'
When you process a paper, Distillate writes your reMarkable highlights back to Zotero as native annotation items. These are visible in Zotero's built-in PDF reader on desktop and mobile.
.rm filesitemType: "annotation"All Distillate-created annotations are tagged distillate. On re-sync, existing Distillate annotations are replaced (your manual annotations are never touched).
# In your .env file (default: true)
SYNC_HIGHLIGHTS=true
Set to false to disable highlight back-propagation entirely.
If you processed papers before v0.2.0, you can back-propagate their highlights retroactively:
# Back-propagate highlights for all processed papers
$ distillate --backfill-highlights
# Or just the last 5
$ distillate --backfill-highlights 5
Papers that already have synced highlights are skipped. To force a re-sync, use --reprocess instead.
When a paper is added, Distillate queries Semantic Scholar to fill gaps in the Zotero metadata. This happens automatically during sync and can also be triggered manually.
liu_embeddings → liu_embeddings_2024)Zotero is always the source of truth: S2 only fills empty fields, never overwrites existing data.
Use --refresh-metadata to re-fetch metadata from both Zotero and Semantic Scholar for all tracked papers:
$ distillate --refresh-metadata
This is useful after editing papers in Zotero (adding dates, fixing titles) or as a one-time migration to enrich older papers with S2 data. Shows progress and only reports papers that changed.
Notes and annotated PDFs are named using citekeys -- short identifiers like einstein_relativity_1905 instead of full paper titles. This makes filenames predictable and compatible with Obsidian plugins that use citekeys.
If you use Better BibTeX for Zotero, Distillate reads the citekey from the item's extra field (where Better BibTeX stores it as Citation Key: AuthorYear).
Without Better BibTeX, Distillate generates a citekey from the first author's surname, the first meaningful word of the title, and the year. Accented characters are normalized (e.g. Lála → Lala, Müller → Muller):
# "Attention Is All You Need" by Vaswani et al., 2017
→ vaswani_attention_2017
# "Biology needs to become prospective" by Avasthi, 2026
→ avasthi_biology_2026
When a paper's citekey changes -- because you edited the date in Zotero, or Semantic Scholar filled a missing year -- Distillate renames everything automatically:
Distillate/Saved/ and Distillate/Inbox/citekey, pdf)Distillate/Saved/vaswani_attention_2017.mdDistillate/Saved/vaswani_attention_2017.pdfcitekey, year, and aliases (the full title, so search still works)[[citekey|title]] wikilinks for stable referencesDistillate is designed to complement -- not replace -- existing Obsidian plugins for academic workflows.
The Obsidian Zotero Integration plugin creates notes from Zotero items. If a note with the same citekey already exists when Distillate processes a paper:
<!-- distillate:start --> and <!-- distillate:end --> markersThis means both tools can write to the same note without conflicts.
PDF++ provides enhanced PDF viewing in Obsidian. Distillate's annotated PDFs are stored alongside notes in Distillate/Saved/ with citekey filenames, making them easy to reference from PDF++ links.
Distillate generates a Papers.base file for Obsidian Bases (available in Obsidian 1.9+), providing a native table view of all your papers with columns for title, dates, engagement score, and highlight counts. The existing Dataview template is also still generated for users on older Obsidian versions.
Built with love and coffee by Romain Lacombe. Powered by rmapi, rmscene, PyMuPDF, and Claude Code.