Data model

These are the entities the MCP tools read and write. You don't query the database directly — the tools do — but knowing the shapes explains what the tool responses mean and why writes behave the way they do.

How the pieces relate

  • A journal holds dated pages.
  • A page has user ink (the OCR'd structure_json) and zero or more typed blocks.
  • An agent_page_seed is the authoritative record of agent-placed content; it materializes into a block on the page.
  • task_links track the identity and reconciled status of tasks across the page and Notion.
A note on calendar events. Penlog has a one-way "shape of day" feature that displays your calendar events on the page (read-only; Penlog never writes to your calendar). That's a UI feature, not part of the agent-facing model — there are no event tools in the MCP API, and tasks are the only thing that syncs two-way. A handwritten is just classified as an events line in structure_json; it doesn't create or sync anything.

journals

The top-level container for a user's pages.

FieldNotes
id Journal UUID.
owner_id The owning user.
notation_legend JSON legend inlined into OCR (see Notation).
Penlog is single-journal per user. Tools resolve to the most-recently-touched journal automatically, so you rarely reference a journal id directly.

pages

One dated page within a journal — the unit agents read.

FieldNotes
id Deterministic UUIDv5 over (journal_id, "YYYY-MM-DD").
date The page's day. Unique per journal.
has_ink Whether the user has written ink on the page.
structure_json The typed lines (see Notation).
ocr_text / ocr_markdown Flat text renderings of the page.
pdf_path / png_path Storage paths; get_page returns signed URLs.
The page id is deterministic: the iPad and the server derive the same UUID for a given (journal, date). That's why create_task_seed can target a future date without orphaning the iPad's local page. The OCR columns are owned by the extraction function — the iPad never writes them.

blocks

Typed content placed alongside ink.

FieldNotes
id Block UUID.
page_id The page it sits on.
type "text" | "image" | "shape".
source "user" | "agent".
content / position / size / style What it is and where it sits.
Agents write blocks; users write ink. The two never overlap, so there are no merge conflicts. A block is a materialized cache of a seed (see agent_page_seeds) — the seed is the source of truth.

The identity and reconciled status of a task.

FieldNotes
id task_link_id — the handle update_task takes.
task_text Normalized task text.
status "todo" | "done".
first_seen_date When the task first appeared.
completed_date Set when status flips to done.
source_page_id The page the task was HANDWRITTEN on — null for agent-created tasks.
line_id Per-line identity carried from structure_json.
notion_task_id / notion_synced_at Notion linkage + last-write-wins checkpoint.
Identity is line_id, not text — no fuzzy matching. source_page_id is null for agent-created tasks on purpose: it marks handwriting origin, and pull logic skips re-seeding a task onto the page it was handwritten on.

agent_page_seeds

The authoritative record for agent-placed content.

FieldNotes
source_kind What kind of thing was seeded (e.g. "notion_task").
source_link_id The id of the source row (e.g. a task_links id).
page_id The page seeded onto.
block_id FK to the materialized block (ON DELETE SET NULL).
hidden / auto_hidden Visibility tri-state — see below.
UNIQUE on (source_kind, source_link_id, page_id) is the dedup gate. The seed is truth; the block is a cache materialized from it. (hidden, auto_hidden) is a tri-state: (false,false) active, (true,true) pending (arrived onto an inked page, awaiting Briefing opt-in), (true,false) user-hidden.

Two rules worth internalizing

  • Seed is truth, block is cache. To change what an agent placed on a page, you act on the seed (via the tools); the block follows.
  • The iPad owns task layout. A seed's on-screen position is an initial guess — the iPad renumbers it on next open. Don't depend on the exact position_y you write.