Osy#betaRun it on Osyrin: osyrin.com

Why Osy# · Chapter 09 · Markdown

A document a person and an agent can edit at the same time.

In Osy# a document is a type. Markdown is application data like any other member, and the value has structure: its headings divide it into sections the application can read and write on their own. A document is part of the application model, and its structure is part of the data.

Apps/recall-osy/model/model.osy · abridgedentity Note {
  [Required, MaxLength(200), Searchable] string Title;
  [Searchable] Markdown? Body;
  …
  [Required] User Owner;
  security {
    allow read, create, update, delete where Owner == user;
  }
}
In practiceDeclare a Markdown member and the application, its editor, its agents and its search all work on one structured document.

01

A document is its sections

Markdown is how people write and how models write, so it is a type here. You assign markdown and read markdown back, and the value has structure: its headings divide it, and the platform stores it as one row per section. Each section keeps its identity across edits, so a section can be read, written and pointed at on its own. Everything else on this page follows from that one decision.

Apps/recall-osy/model/model.osyverbatim — this file compiles
/// A personal note: a title, a markdown body, a category and free-text tags.
entity Note {
  [ModifiedAt] DateTime? ModifiedAt;
  /// THE CITATION. A search hit names the record it came from in these words (`SearchHit.SourceLabel`), read fresh
  /// on every search — so a result can say "Note · Northwind — supplier terms" rather than a bare id.
  semantic => $"Note · {Title}";
  [Required, MaxLength(200), Searchable] string Title;
  /// The document. One Markdown field, edited through the MarkdownEditor control and indexed for search — v1's
  /// separate `Content` string is folded in here.
1  [Searchable] Markdown? Body;
  NoteCategory? Category;
  [MaxLength(500)] string? Tags;
  [Required] User Owner;
  security {
    // Owner-only, stated once. v1 said the same thing with a deny-all default plus two rules naming the roles that
    // could then be re-allowed; secure-by-default already denies, so only the grant needs writing.
2    allow read, create, update, delete where Owner == user;
  }
1

The document. One member, read and assigned as a value, stored as its sections. The ? is redundant: a document nobody has written is genuinely not there, and reads back null. What [Searchable] adds is §4.

2

The rule, stated once. The editor in §2, the agent in §3 and the search in §4 all read and write through it, because the sections are rows of this entity.

Sections are rows, so an edit costs what the edit is worth: ticking a checkbox rewrites one row. Two writers collide only when they touch the same section, which §3 builds on. A document's --- front-matter binds to ordinary members with [FrontMatter], so its header is application data too.

The same document, read The Markdown member from the listing above, rendered by one atom in the app's own typeface. Flip to see the page that drew it.
A note from a personal archive, its markdown body rendered as prose in a serif on cream paper
Apps/recall-osy/model/pages/notes.osy verbatim — this file compiles
    Stack(maxW: Size.Read, mx: "auto", w: "100%") {
      Row(align: Align.Center, pb: 5) {
        Link(href: "/notes") {
          Text("←  Notes", fontFamily: Font.Sans, fontSize: FontSize.Label, letterSpacing: "0.14em",
               textTransform: TextTransform.Uppercase, color: Colors.TextMuted);
        }
        Spacer();
        if (note != null) {
          Link(href: "/notes/" + note.Id + "/edit") {
            Text("Edit", fontFamily: Font.Sans, fontSize: FontSize.Label, fontWeight: FontWeight.Semibold, letterSpacing: "0.14em",
                 textTransform: TextTransform.Uppercase, color: Colors.Primary);
          }
        }
      }

      if (note != null) {
        Stack(gap: 3, pb: 6) {
          if (note.Tags != null && note.Tags != "") { Eyebrow(text: note.Tags); }
          Text(note.Title, fontFamily: Font.Display, fontSize: FontSize.Title, color: Colors.OnBg,
               letterSpacing: "-0.015em", lineHeight: "1.05");
        }
        // The read-only renderer, set as prose: serif, long line height, on the paper rather than in a box. A note
        // you are only READING should not pay for a megabyte of editing machinery.
        Box(fontFamily: Font.Text, fontSize: FontSize.Body, color: Colors.OnBg, lineHeight: "1.7") {
          Markdown(note.Body);
        }

A document is application data with structure: headings, sections, rows.

02

The editor works on the document itself

A rich editor is handed the address of a document: which entity, which row, which member. It reads the sections behind that address and writes them back, section by section, under the entity's own rule. The editing experience is a kit, one line in app.osy (use Osysharp.MarkdownEditor@1;), and an app that never writes that line never carries it.

The editor, over a real document Three arguments name the document: ownerType, ownerId, property. The face and the outline rail are the app's choices. Flip to see the call.
The markdown editor over a signed-in user's notes on special relativity: the Lorentz factor, the Lorentz transformation and the derivation of E = mc² as rendered maths, with the outline rail listing every section
demo/markdown-demo/model/pages/editor.osy verbatim — this file compiles
    MarkdownEditor(
      ownerType: "User",
      ownerId: me.Id,
      property: "Notes",
      face: editorial,
      outline: true,

The editor knows which document in the application it is editing, and it writes that document's sections. What it can do with them, from tables and maths to find and replace, is in the editor kit reference.

03

A person and an agent can write the same document

A person is editing one section. An agent, meanwhile, asks for the document's outline, which names every section by a slug, and reads or rewrites one section by that name. Its edit lands on that section's row. The person's section is a different row, so neither overwrites the other, and nobody merges anything.

the tools an agent gets, the moment an entity declares a Markdown member
markdown_outline  headings, levels, slugs, sizes — start here; the slug is every other tool's anchor
markdown_read     the whole document, or one section by anchor
markdown_patch    find and replace one string in one section — for small edits; it must occur once
markdown_replace  rewrite one section's body from scratch

Append, rename, move, delete and import follow the same model, a section by its name; the full set is in the Markdown reference.

Precision is the point. Given markdown_patch, the agent names a section and a string. The platform refuses the edit if that string is not unique in that section. The blast radius of a bad edit is one row, and the person editing the next section never notices it happened.

The person and the agent are not sharing one text value. They are operating on the same structured document through addressable sections.

04

A searchable document remembers where its words came from

Mark the member [Searchable] and each section is cut into sentence-sized pieces, each indexed on its own, with the section kept beside them as the thing an answer resolves to: the small piece is what gets matched, the whole thing is what comes back. A question in your own words is matched on meaning, and what returns is a hit that says which record, which section and which sentence answered, and when it was learned.

Apps/recall-osy/model/functions.osyverbatim — this file compiles
SearchHit[] SearchEverything(string query) {
  if (query == "") { return []; }
1  return Memory.Search(query, limit: 30);
}
1

One question, everything the caller may read. The rule on Note in §1 governs this read as it governs every other: a hit on a section the caller may not read never appears.

the hit behind that card, as the function returned it · real output, trimmed to one result
$ osy run SearchEverything --arg "query=who signs off an urgent credit?" --as [email protected]
✓ Ran SearchEverything as [email protected].
{
  "content": "## Northwind — supplier terms > Credit approval\n\n
            An urgent credit above the standing limit is signed off by the CFO, and only the CFO — …",
  "score": 0.591,  "distance": 0.409,
  "kind": "MarkdownSection",
  "sourceLabel": "Note · Northwind — supplier terms",
  "sourceEntityType": "Note",  "sourceEntityId": "60f3e711-…",
  "rememberedAt": "2026-09-04T19:28:56Z",  "origin": "Derived",  "authoredBy": null,
  "via": null
}

The whole section that answered, the record it belongs to in words and as a row, the heading, and when it was learned. Enough to quote it and be right.

A search can be narrowed to one record, and stated links between records can be followed one hop out with the link's own words on the hit; how matching, ranking and provenance work is in the retrieval reference.

A sentence is found by meaning, and what comes back is an attributable piece of the application, under the same rule as every other read.

05

Markdown can also be a value still being written

A model's reply arrives a few characters at a time, and half-typed markdown is briefly not markdown: the ** has no closing pair yet, the | is not yet a table. So a reply is a stream<string> the component binds to, and the renderer is told the value is still arriving: it holds the unfinished tail as though it were closed, and settles it when the stream ends.

// from reference/ui/on-settled · compiled by the docs gate
entity Reply { string? Body; }

stream<string> Answer() {
  yield return "the wire ";
  yield return "is live";
}

void Store(string text) {
  new Reply { Body = text };
  UnitOfWork.Commit();
}

component Chat() {
  live var answer = Answer();
  on settled(answer) { Store(string.Concat(answer)); }

  render {
    Stack { Markdown(string.Concat(answer), streaming: !answer.Done); }
  }
}

A stream is a value. Answer yields as a model would, and binding it with live var re-renders the component as pieces land. When it finishes, it is saved: on settled runs once, whichever way the stream ended. And the flag is bound to the stream's end — streaming: !answer.Done — so turning it off is what settles the tail into ordinary markdown.

Markdown stays a useful application value while it is being produced.

What happens when you change it

Change the document. The model follows.

Each of these is a thing you would do on a Tuesday. The verdict is the compiler's, not a convention's.

You do thisVerdictBecause
Add a heading in the middle of a documentcompilesThe document is re-sectioned on save. Sections you did not touch keep their identity, so an agent's anchor into them still resolves.
A person and an agent save different sections at oncecompilesDifferent rows. Neither overwrites the other, and nobody had to merge anything.
An agent patches a string that appears twice in a sectionrefusedRefused: markdown_patch needs the string to occur exactly once, so the edit lands where it was meant or not at all.
Mark the member [Searchable]compilesEach section is cut into pieces and indexed from then on; a hit names the section and the sentence.
Tighten the entity's read rulecompilesSearch follows it. A hit on a section the caller may no longer read never appears.
Bind the document's --- header to a membercompiles[FrontMatter] string Title; fills from the header on assignment, so the header is a column you can query and require.