An intelligent middleware for capturing thoughts and organizing them in Notion.
Synapse eliminates the friction of manual data entry in Notion. It accepts unstructured, natural-language text, uses a multi-step AI chain (parse → classify → extract) to understand and structure the content, and then routes it to the correct database or page in a Notion workspace. The entire project is written in Python and deployed on Modal.
- Modal hosts everything.
app.pyis the single deployment shim — image, secrets, endpoints. All business logic lives insrc/core/as plain Python with no Modal imports. webhook— a proxy-authedfastapi_endpoint. Callers sendModal-Key+Modal-Secretheaders; unauthorized requests are rejected at Modal's edge for free. It validates the payload andspawn()s the worker — spawn IS the queue (no Pub/Sub).process— the background worker (timeout=600,memory=512,max_containers=1to serialize runs since Notion dedupe is query-then-create, retries with backoff). Runscore.pipeline.run.- Gemini (
gemini-3-flash-preview, env-overridable viaGEMINI_MODEL, with automatic fallback toGEMINI_FALLBACK_MODELon a 404) does parsing, classification, and extraction with structured JSON output. - Secrets are env vars only: the Modal secret
synapsein the cloud,op runlocally..env.tplis the canonical manifest (op:// refs, committed) — just the 6 credentials. Notion DB ids are committed config indatabases.yaml, not secrets (aNOTION_<X>_DB_IDenv var still overrides).
flowchart LR
R["Receptor<br/>(iOS/macOS Shortcut)"] -->|"POST {raw_text}<br/>Modal-Key / Modal-Secret"| W["webhook<br/>(Modal fastapi_endpoint,<br/>proxy auth)"]
W -->|"process.spawn()"| P["process worker<br/>(core.pipeline.run)"]
P -->|"parse / classify / extract"| G["Gemini"]
P -->|"enrichment"| X["Spotify · YouTube ·<br/>Google Places · web scrape"]
P -->|"create / update pages"| N["Notion databases"]
P -->|"outcome logs"| L["Notion Logs DB"]
uv sync # install everything
just test # unit tests (no network)
just check # ruff lint + format check
just dev # live-reload deploy against real Modal infra (modal serve)
just test-integration # real Gemini calls (key via 1Password)
just deploy # test + sync-secrets + modal deploy
just recept "Buy eggs $ groceries" # send one thought to the deployed webhookLocal runs against real services use 1Password injection — never plaintext on disk:
op run --env-file=.env.tpl -- uv run <cmd>Everything else is code; these are one-time console/dashboard actions:
- Modal auth (local):
uv run modal token new. - Modal Proxy Auth Token: Modal dashboard → Settings → Proxy Auth Tokens → mint a token. Give the token ID/secret to the Receptor client (and store them on a 1Password item of your choosing). The webhook rejects requests without
Modal-Key/Modal-Secretheaders. - Google API keys: mint a Places API key and a YouTube Data API v3 key in the Google Cloud console (APIs & Services → Credentials) and put them on the 1Password item that
.env.tplreferences. - CI secret:
gh secret set OP_SERVICE_ACCOUNT_TOKENwith a 1Password service-account token that can read the project's vault (the one.env.tplreferences). - Push secrets to Modal:
just sync-secrets(reads.env.tpl, injects viaop, creates/updates thesynapseModal secret). - Notion select options: every
allowlistvalue indatabases.yamlmust exist as an option on the live Notion select/multi_select/status property (add missing ones in the Notion UI). Hydration intersects allowlists with live options and prints a⚠️ ... allowlist options missing from Notion selectwarning for any value it had to drop; the AI can never pick a dropped value. The Fun ActivitiesLocationallowlist is personal config: the committed yaml carries generic example cities — setNOTION_FUN_ACTIVITIES_LOCATIONS(comma-separated, in the env item.env.tplreferences) to your real city list. - Executions DB
Tagsproperty: aTagsmulti_select with theproject-appendoption must exist on the Executions DB.
The whole pipeline is YAML-driven. To add a new Notion database category:
- Add the category definition to
databases.yaml, including itsdb_id— that single edit is the whole onboarding. (Non-category ids live in the top-leveldb_idsmapping.) No 1Password or.env.tplchange needed.
| Field | Required | Usage |
|---|---|---|
description |
✅ Yes | The Classifier Prompt. Used by the AI to decide if an incoming item belongs to this category. |
helper |
No | true marks a helper DB (trips, logs, youtube-channels) that is only related to, never a classification target. |
properties |
✅ Yes | Maps exact Notion column names to their rules. |
type:title,rich_text,rich_text_list,url,date,select,multi_select,status,relation,boolean.required:trueforces the AI to produce a value.instruction: the extraction prompt for this field. Placeholders:{current_date}(Eastern time),{raw_text}. Fordatefields the instruction MUST demand ISO 8601 —notion_utils._notion_dateraises on anything else.virtual:truehides the field from the AI; Python fills it.allowlist: strict enum for select/multi_select/status (intersected with live Notion options at runtime — see manual setup step 6: the options must also exist in Notion).create_new:truelets the AI invent new values beyond the allowlist.
- Core Syntax
@splitter: separate multiple distinct items in one message.$context: define the Project, Date, Status, or category hint.
- Defaults (if not specified)
- Tasks: Status
To Do| TagChore| PriorityHigh| DateToday(Eastern) - Movies/TV:
Not Started· YouTube:Watched· Podcasts:Not Started· Fun Activities:To Do· Groceries:On List
- Tasks: Status
- Category cheatsheet
- Tasks (default):
Update dating profile - Projects:
Refactor code $ Synapse(strict: must name the project in context; use "note"-flavored phrasing for project notes) - URLs: auto-route to Places (Google Maps), YouTube, Podcasts (Spotify/TAL), or Bookmarks
- People:
Will Barlow Theo's Friend - Dates/status:
Cancel Uber One $ Jan 1·The Matrix $ movie priority
- Tasks (default):
- Batch example:
Arun Vantage Senior Associate @ https://youtu.be/xyz @ Buy eggs $ groceries
app.py # the ONLY file that imports modal
src/core/ # business logic (pipeline, ai_engine, handlers, notion_utils, ...)
src/core/databases.yaml # Notion schemas + extraction rules
src/core/prompts.yaml # parser/classifier/extractor system prompts
tests/ # pytest suite (unit + test_integration.py for real Gemini)
scripts/ # one-off clients for the deployed webhook
.env.tpl # secrets manifest (op:// refs)
Push to main → GitHub Actions runs pytest -m "not integration" and modal deploy app.py (Modal tokens loaded from 1Password). Manual: just deploy.