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×DoneunderlineHeading
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.)
| Symbol | notation | section | status | Meaning |
|---|---|---|---|---|
• | 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:
| Field | Type | Notes |
|---|---|---|
text | string | Line content, with the leading notation symbol stripped. |
section | string | tasks | notes | standup_notes | heading | other, or the legacy events (see below) |
notation | string | bullet | cross_done | check_done | dash | underline | none, or the legacy circle (see below) |
status | string | null | open | done | null |
priority | boolean | True if highlighted. |
confidence | number | 0.0–1.0 OCR confidence for the line. |
line_id | string | Stable 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 carrynotation: "circle"andsection: "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.