Notation & structure_json

When you write a page, Penlog OCRs it and emits structure_json — a flat list of typed lines. The key idea for agent authors: a line's section and status are a deterministic function of the symbol you drew, not the model's free interpretation. Read the structure; you don't have to re-parse prose.

The notation legend

Penlog reads four marks:

  • •  Task
  • —  Note
  • ×  Done
  • underline  Heading

Extraction applies the same rules to every journal. A journal's notation_legend field is the key shown in the app; it isn't sent to the OCR model, so changing it doesn't change how a page is read.

Notation → section & status

Each drawn symbol maps to a section and status. This mapping is enforced server-side: if the model's classification disagrees with the symbol, the symbol wins. (The one exception is none — with no drawn symbol there's nothing to enforce, so the model's context-based section is trusted.)

SymbolnotationsectionstatusMeaning
• bullet tasks open An open to-do.
×, X cross_done tasks done A completed to-do.
✓, ✔ check_done tasks done Also treated as completion.
—, - dash notes null A note — not a task.
underline underline heading null A section heading.
(none) none notes / standup_notes null No drawn symbol — context decides (see below).

For notation: "none", a standup-style label (Y:, T:, B: for yesterday / today / blockers) routes the line to standup_notes; otherwise it falls to notes.

Priority

A colored marker highlight over any part of a line sets priority: true. Any highlight color fires, and partial coverage promotes the whole line. Priority is orthogonal to section/notation/status — highlighting a task doesn't change that it's a task, it just flags it as important.

The structure_json shape

structure_json is a single object with a lines array. Each line carries:

FieldTypeNotes
textstringLine content, with the leading notation symbol stripped.
sectionstringtasks | notes | standup_notes | heading | other, or the legacy events (see below)
notationstringbullet | cross_done | check_done | dash | underline | none, or the legacy circle (see below)
statusstring | nullopen | done | null
prioritybooleanTrue if highlighted.
confidencenumber0.0–1.0 OCR confidence for the line.
line_idstringStable identity across OCR passes — see below.
{
  "lines": [
    {
      "text": "Follow up on API spec",
      "section": "tasks",
      "notation": "bullet",
      "status": "open",
      "priority": true,
      "confidence": 0.97,
      "line_id": "ln_3"
    },
    {
      "text": "Shipped the release",
      "section": "tasks",
      "notation": "cross_done",
      "status": "done",
      "priority": false,
      "confidence": 0.95,
      "line_id": "ln_4"
    },
    {
      "text": "Project Alpha",
      "section": "heading",
      "notation": "underline",
      "status": null,
      "priority": false,
      "confidence": 0.9,
      "line_id": "ln_5"
    }
  ]
}
Legacy values. A drawn ○ is not part of Penlog's notation. Since September 24, 2026, extraction stores a circled line as a plain note (notation: "none", section: "notes", status: null), so it never becomes a task. Pages extracted before then can still carry notation: "circle" and section: "events", so your parser should accept both and treat them as notes: task sync ignores those lines, and there's no events feature behind them. Tasks that were created from circled lines before the change stay in your task list until you complete them. None of this touches a calendar. Calendar events reach a page only as typed blocks from the read-only calendar connector.

line_id and status reconciliation

line_id is the durable identity of a line. On each OCR pass, Penlog diffs against the previous structure_json and carries the same line_id forward (by exact or prefix-extension match), or mints a fresh one. That stable id is what lets a task keep its identity as its handwriting evolves — no fuzzy text matching.

It's also how status stays honest. The status in raw structure_json is derived from the ink (a bullet reads open until it's physically crossed out). But a task can be completed in Notion or the in-app Briefing without re-marking the page. When you fetch a page with get_page, task lines are overlaid with the reconciled status: the Notion/Briefing-aware status, the original ink status preserved as page_status, a status_source flag, and completed_date. Trust the reconciled status over how a task looks on the page.