Architecture
Notewright is a single Cloudflare Worker composed from provider packages. The data path is linear: a notes source fills D1, the follow-up pipeline reads from D1 and writes to a follow-up sink, and the speaker-context agent enriches extraction along the way.
The data path
Section titled “The data path”flowchart TD
NS[Notes source]
D1[(D1<br/>meetings + transcripts)]
FP[Follow-up pipeline]
SINK[Follow-up sink<br/>tasks]
SC[Speaker-context agent]
NS -->|sync| D1
D1 --> FP
FP -->|create| SINK
SC -.->|on demand| FP
classDef external stroke-width:1.5px;
class NS,SINK external
- Sync. The notes source pulls updated notes and transcripts and writes them
into D1. With Granola, sync runs inside the
GranolaSyncAgentDurable Object on a cron schedule, and can also be triggered on demand. - Storage. Everything persists in Cloudflare D1 (see Storage).
- Extraction. The
FollowUpAgentDurable Object runs the extraction pipeline: it loads a meeting from D1, resolves speakers, asks the LLM (through AI Gateway) for action items, and creates each one in the follow-up sink. It records runs, task mappings, and failures so they can be inspected and retried. - Speaker context. The
SpeakerContextAgentDurable Object resolves and caches per-meeting speaker identities. The pipeline calls it on demand to attribute action-item excerpts to real speakers.
The extraction pipeline
Section titled “The extraction pipeline”Inside FollowUpAgent, a single meeting flows through these stages. The
speaker-context agent is consulted on demand, and each action item becomes one
call to the follow-up sink:
flowchart LR
D1[(D1<br/>meeting)] --> LOAD[Load meeting]
LOAD --> SPK[Resolve speakers]
SPK --> EXTRACT[Extract action items<br/>LLM via AI Gateway]
EXTRACT --> CREATE[Create follow-ups]
CREATE --> REC[(Record runs,<br/>mappings + failures)]
SCA[Speaker-context agent] -.->|on demand| SPK
CREATE -.->|one per item| SINK[Follow-up sink]
class SINK external
What runs where
Section titled “What runs where”| Component | Form | Trigger |
|---|---|---|
Worker fetch | The composed defineApp handler | HTTP requests (all bearer-authenticated) |
| Notes sync | Durable Object (GranolaSyncAgent) | Cron + on-demand /sync |
| Follow-up extraction | Durable Object (FollowUpAgent) | Cron backstop + on-demand /follow-ups/run and /follow-ups/backfill |
| Speaker resolution | Durable Object (SpeakerContextAgent) | On demand from the pipeline |
Boundaries
Section titled “Boundaries”The extraction and sync internals are written with Effect,
but Effect never crosses a package boundary. Every public export is plain
TypeScript: data types, classes, and Promise-returning functions. Providers
signal failure by throwing a ProviderError, never an Effect error. This keeps
third-party provider authoring free of any Effect knowledge. See
Writing a provider.
Authentication
Section titled “Authentication”Every HTTP route on the Worker is guarded by a shared bearer secret,
SYNC_SHARED_SECRET. Requests without a valid Authorization: Bearer header get
401. The CLI sends this secret on every call. See the
HTTP API reference.