Skip to content

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.

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
  1. Sync. The notes source pulls updated notes and transcripts and writes them into D1. With Granola, sync runs inside the GranolaSyncAgent Durable Object on a cron schedule, and can also be triggered on demand.
  2. Storage. Everything persists in Cloudflare D1 (see Storage).
  3. Extraction. The FollowUpAgent Durable 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.
  4. Speaker context. The SpeakerContextAgent Durable Object resolves and caches per-meeting speaker identities. The pipeline calls it on demand to attribute action-item excerpts to real speakers.

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
ComponentFormTrigger
Worker fetchThe composed defineApp handlerHTTP requests (all bearer-authenticated)
Notes syncDurable Object (GranolaSyncAgent)Cron + on-demand /sync
Follow-up extractionDurable Object (FollowUpAgent)Cron backstop + on-demand /follow-ups/run and /follow-ups/backfill
Speaker resolutionDurable Object (SpeakerContextAgent)On demand from the pipeline

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.

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.