← Back to distillate.dev

Power Users Guide

Advanced configuration, automation, and troubleshooting.

GitHub Actions Automation

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.

Architecture

The flow works like this:

  1. Run distillate locally to sync papers and process highlights
  2. Run distillate --sync-state to upload your state.json to a private GitHub Gist
  3. GitHub Actions reads state from the Gist on a schedule
  4. Actions runs --suggest-email (daily) or --send-digest (weekly) and sends the email

Step-by-step setup

a. 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:

SecretDescription
ZOTERO_API_KEYYour Zotero API key
ZOTERO_USER_IDYour Zotero user ID
ANTHROPIC_API_KEYFor AI-powered suggestions
RESEND_API_KEYFor sending emails
DIGEST_TOYour email address (recipient)
DIGEST_FROMSender address (configured in Resend)
STATE_GIST_IDID of the private Gist from step (a)
GH_GIST_TOKENClassic PAT with gist scope from step (b)
OBSIDIAN_VAULT_NAMEYour 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.

Gotchas

GH_GIST_TOKEN must be a classic PAT The built-in 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.
Set OBSIDIAN_VAULT_NAME for deep-links Without this, digest emails won't include obsidian:// links to open notes directly in your vault.
State freshness matters Run --sync-state after every local sync. If the Gist state is stale, suggestions will be based on outdated reading history.

Manual trigger

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

Engagement Scores

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.

Reprocessing

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.

Custom AI Models

Distillate uses two model tiers, configurable via environment variables in your .env:

# 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.

Storage Management

The KEEP_ZOTERO_PDF setting controls whether the original PDF stays in Zotero cloud after being uploaded to your 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.

Debug Mode

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:

Debug 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?"

State Sync and Backup

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.

Corrupt state recovery

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.

Manual inspection

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'

Zotero Highlight Back-Propagation

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.

How it works

  1. Highlighted text is extracted from the reMarkable .rm files
  2. Each highlight is located in the original PDF via text search
  3. Bounding-box coordinates are converted to Zotero's format (PDF bottom-left origin)
  4. Annotations are created via the Zotero Web API with itemType: "annotation"

All Distillate-created annotations are tagged distillate. On re-sync, existing Distillate annotations are replaced (your manual annotations are never touched).

Configuration

# In your .env file (default: true)
SYNC_HIGHLIGHTS=true

Set to false to disable highlight back-propagation entirely.

Backfilling existing papers

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.

Requires the document on reMarkable Backfill downloads the document bundle from your reMarkable's Saved/ folder. If you've deleted the document from reMarkable, backfill will skip it.

Metadata Enrichment

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.

What gets enriched

Zotero is always the source of truth: S2 only fills empty fields, never overwrites existing data.

Refresh all papers

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.

Citekey Naming

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.

Better BibTeX integration

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).

Automatic fallback

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

Automatic rename

When a paper's citekey changes -- because you edited the date in Zotero, or Semantic Scholar filled a missing year -- Distillate renames everything automatically:

What changes

Obsidian Plugin Compatibility

Distillate is designed to complement -- not replace -- existing Obsidian plugins for academic workflows.

Zotero Integration plugin

The Obsidian Zotero Integration plugin creates notes from Zotero items. If a note with the same citekey already exists when Distillate processes a paper:

This means both tools can write to the same note without conflicts.

PDF++

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.

Obsidian Bases

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.