Keeps a Notion database of Temporal demo repositories in sync with GitHub — unattended, durably, and enriched by an LLM. Point it at some GitHub orgs/repos and it discovers the demos, classifies them, and upserts them into a Notion catalog you can browse, filter, and share.
It's a thin application built on top of
durable-sync, an
open-source, generic Temporal engine for syncing any source to any destination.
The engine provides the durable orchestration, idempotent upsert, headless OAuth
refresh, and the GitHub-source / Notion-destination connectors. This repo
supplies only the domain-specific bits: which repos to pull, how to enrich
them, and the Notion wiring.
durable-sync runs one long-lived Temporal entity workflow per source, each
with its own durable timer. On every tick (and on demand via a signal) it:
- Pulls repos from GitHub with your token, filters them (org sources are gated by an inclusion topic; named repos are always included), and grabs each README.
- Enriches each repo: a deterministic code scan derives the SDK(s), Temporal primitives, and integrations directly from the source; an LLM (Claude) reads the README + those signals and classifies the demo into a controlled vocabulary (summary, use cases, design patterns, capabilities, target audiences, …).
- Upserts into Notion through the Notion MCP server over OAuth — authorized as an individual user, so it needs no Notion integration token or workspace-admin access. Because MCP rides the normal Notion API, it can write into an existing database, put the README into the page body, and set relations as ordinary relations.
The upsert is idempotent, keyed on the immutable GitHub repo id (query → update if present, else create), so Temporal's retries never duplicate rows. It's append-style: a repo dropping out of a source never deletes its Notion row, so hand-added metadata survives.
The generic engine (entity workflow, upsert, OAuth token workflow, MCP transport,
GitHub source, Notion destination) lives in durable-sync. This repo is just the
wiring on top:
| File | Role |
|---|---|
github_config.py |
Which orgs/repos to pull, the inclusion topics, repos to always skip, and which orgs count as "members". |
enrich.py |
Deterministic source-side enrichment hook: readable name, SDK(s) (from code), Type (from topics), etc. |
llm_enrich.py |
The LLM layer: controlled vocabulary, strict-tool-use schema, and the mapping from the model's output to Notion columns. |
static_analysis.py |
Read-only code scan of each repo's tarball — detects SDKs, Temporal primitives, integrations, and cloud-readiness. |
pipeline.py |
The one wiring point: builds the GitHub source + Notion destination from the modules above. |
config.py |
App knobs: Notion target, title column, sync interval, LLM model + toggle. |
notion_ids.py |
Workspace-specific Notion IDs in one place (Maintainer Map, target-personas DB, persona→page map) — the one file to point at your workspace; all env-overridable. |
worker.py |
The long-running worker (hosts the workflows + activities from the engine). |
bootstrap.py |
Starts one entity workflow per source. |
reconcile.py |
A periodic reconcile loop for things the per-source sync doesn't cover (see below). |
backfill_llm.py |
One-time LLM backfill of existing rows (e.g. after a vocabulary change). |
pip install -r requirements.txt
# GitHub — a read-only token is enough for public repos.
export GITHUB_TOKEN=...
# Notion target — the data source (collection) id of your catalog database.
export NOTION_DATA_SOURCE_ID=...
# LLM enrichment (optional — unset it and enrichment degrades to a no-op, no crash).
export ANTHROPIC_API_KEY=...
# Temporal Cloud (omit for a local dev server):
export TEMPORAL_ADDRESS=your-ns.tmprl.cloud:7233
export TEMPORAL_NAMESPACE=your-ns
export TEMPORAL_API_KEY=...Then, in order:
# Local Temporal dev server (skip if using Temporal Cloud).
temporal server start-dev
# 1. Authorize Notion once (opens a browser; writes ./.notion_auth.json).
python -m durable_sync.connectors.notion.bootstrap
# 2. Launch the token-owner workflow — it owns the rotating refresh token and
# hands valid access tokens to the worker and reconcile loop.
python -m durable_sync.connectors.notion.start
# 3. Start the worker (keep it running — VM, container, or Temporal Cloud worker).
python worker.py
# 4. Start one entity workflow per source (idempotent — re-running reconciles).
python bootstrap.py
# 5. (Optional) run the reconcile loop alongside the worker.
python reconcile.py --loopDrive / inspect a source by workflow id (prefixed demos-sync:):
temporal workflow signal --workflow-id "demos-sync:org:<org>" \
--name sync_now --input '[]' # sync now
temporal workflow query --workflow-id "demos-sync:org:<org>" \
--type status # last run, stats, errorsA push webhook can target a single repo by passing its full name in the signal
payload (--name sync_now --input '["<owner>/<repo>"]') for near-real-time
refresh, with the interval as the backstop.
- LLM enrichment is create-only. The classifier runs only when a row is first
created, so human edits in Notion stick and steady-state token cost is ~zero.
Re-classification is a deliberate, opt-in action (a
Reclassifycheckbox on the row, picked up by the reconcile loop). - README freshness: the page body is written on create; later runs refresh properties only.
- OAuth ownership: once the token-owner workflow is running, it is the sole
owner of the rotating refresh token. The worker and reconcile loop get tokens by
querying it — never by refreshing the file directly. Don't run the one-time
file-based scripts (
backfill_*) while that workflow is up, or the competing refresh will get the grant revoked. (Recovery: re-run the bootstrap, then the connector'sreauthorize.) - Idempotent & append-style: upsert is keyed on the immutable GitHub repo id; a repo leaving a source never deletes its row.