Skip to main content
Notes are an OxiMail JMAP extension (urn:oximail:params:jmap:notes, ADR-144): the place for what is not a task. The rule that separates the two is one sentence: if it has a “done” state, it is a task; otherwise it is a note. A note generates tasks and keeps the link; a task points to the page it came from. The call summary, the idea, the points seen in a meeting used to be filed in a task’s description for want of anything else.

The capability object

transcriptionAvailable is a runtime fact written into the Session per instance: the community binary carries no transcription engine, and an extension may plug one in through the server’s seam. maxBodyOctets (1 MiB) bounds the rich body of a page.

Objects and methods

A Note carries id, notebookId (exactly one, movable), title (empty allowed, the client shows the first line), body, bodyText, preview (the first characters of the text, always served), tags (free, normalised to lowercase), links, attachments, source (manual, dictation, voicemail, meet, a closed vocabulary that says where the page came from and never switches behaviour except at creation), transcription and its four provenance fields, isPinned, isTrashed and trashedAt, createdBy (meaningful in a shared notebook), createdAt, updatedAt. The body is served only when named. body is the canonical rich document the editor produces (ProseMirror), stored without loss; bodyText is the plain text the server derives from it, what the index and an assistant read, never written by a client. Both are in the projectable vocabulary and out of the default projection, the bodyValues idiom of Email/get: a list of two hundred pages must not carry two hundred documents. Ask for them in properties. Links point to other workspace objects, {type, id} with type in task, email, contact, event, file, note; the form is verified at write and the target is not resolved, so a dead link is displayed and never breaks a page. Attachments are blobs (blobId, name, type), the dictated audio among them. Note/query filters on inNotebook, hasTag, linkedTo, text (answered from the full-text index, which holds the title, the derived text and every attachment transcript, never from a LIKE scan), source, hasAttachment and isTrashed; it sorts on updatedAt, createdAt, title and isPinned, pinned pages first and most recently updated next by default. A property a client cannot write (bodyText, preview, the transcription fields, trashedAt, createdBy, the timestamps) is refused by name on a create or update, never silently dropped; Note declares its write vocabulary to the dispatcher.

Sharing

A notebook is shared like a task list or a calendar, with the RFC 9670 shareWith map (sharing) and a vocabulary of OxiMail’s own, since a note has no assignee, no invitation and no privacy axis to map the calendar tiers onto. Three bits, with the two implications a human would assume imposed: mayReadItems (read the pages), mayWriteItems (create, edit, move, trash, restore and hard-delete the pages, a writer owns the page lifecycle, and requires mayReadItems), mayAdmin (rename, recolour, modify shareWith, destroy the notebook, and requires mayWriteItems). A grantee writes through the shared:<owner> account and createdBy says who wrote each page. The first share of a notebook opens an account in the grantee’s Session, so it moves sessionState (RFC 8620 §2), as a share of a mailbox, a calendar, a task list or a Drive node does: the grantee’s client reloads the Session and the shared account appears. A notebook share used to change the account list without moving the state, and a client never saw it.

The trash

Destruction is two-stage (ADR-035). Note/set destroy on a live page moves it to the trash (the page answers Updated with isTrashed: true); destroy on a trashed page deletes it for good (Destroyed); Note/set update {isTrashed: true} trashes as well, and {isTrashed: false} restores. The trash sweep destroys what has been in the trash longer than the tenant’s horizon, its notes.trash_retention_days override or else [notes] trash_retention_days (30 by default, floor 1; an override of 0 purges nothing, see per-tenant horizons), through the same exit as the Drive’s trash, respecting legal holds and announcing what it removed to synchronised clients (storage).

Dictation and transcription

Dictation is not a method: it is Note/set create with source: "dictation" and an audio attachment (type audio/*, refused otherwise). The page is created immediately, transcription: "pending", and after commit the audio is handed to the transcription engine; when the job reports, the body, the plain text and the per-attachment transcript (timed segments {start, end, text}, so a client jumps the audio to a paragraph and highlights the segment being played) are filled and the page answers done. Without an engine the page is accepted anyway and answers transcription: "failed" with transcriptionError naming the reason: honest, never silent, never a refusal of the page. Note/transcribe {accountId, id, blobId} is the other gesture, added later: transcribe a recording added to a page that already has content, and retry a failed one. An ordinary Note/set update (reordering pieces, renaming an attachment) never starts a job by side effect; the client asks. The call verifies the piece is an audio attachment of this page, puts the page to pending with that piece’s transcript and provenance erased (the other pieces keep theirs), and sends one request for that piece alone in append mode: the text is added to the body as it is when the job reports, merged by the store in its own transaction, so an edit committed meanwhile is never lost. The method exists on every instance, never unknownMethod; without an engine it fails the same honest way. A page says where its transcription happened. Local engine or third-party provider are both legitimate operator choices, but they do not say the same thing about the audio. Each audio attachment carries the provenance of its own transcription, written by the job on the piece it worked on, on success and on failure (the audio left all the same): transcriptionProvider (an open vocabulary naming an implementation, local_whisper, openai_compat, …, displayed and never decided on), transcriptionEndpointHost (a host, never a URL that could carry a key; null for a local engine), and transcriptionResidencyTier, the field a client branches on: local (the audio never left the instance), ch, eu, non_eu. null means unknown, never local. The page’s own three fields are derived from its pieces: two recordings may have been processed at different times by different engines, and the page can claim no more than the least sovereign of them. A sentence that over-claims sovereignty is the defect this design forbids. The transcription engine is an extension seam; the community binary ships none, which is exactly what transcriptionAvailable: false says. Strictness rules (unknown ids in notFound, invalidProperties over coercion, stateMismatch on ifInState) are identical to the mail surface.