Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Why Osy# · Chapter 09 · Markdown

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

Markdown is how people write and how models write, so it is a type here, not a blob. A document is stored as its sections, one row each: a person edits one section in a rich editor while an agent rewrites another through a tool, and neither overwrites the other. Every section is in the app's memory, so a question in your own words comes back as a citation — the section, the sentence, when it was learned — a reply renders while it is still arriving, and the front-matter is ordinary queryable data. Declare Markdown Body; and all of that is already true.

In practiceOne type: an editor kit, nine agent tools, streaming and a memory that cites its sources follow from it — nothing to integrate.You stop building
  • a rich-text editor integration
  • a document store beside the database
  • a merge for concurrent edits
  • a chunker, an embedder and a vector index
  • a renderer that copes with half-typed replies
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 {
  /// 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.
  [Searchable] Markdown? Body;
demo/markdown-demo/model/pages/editor.osyverbatim — this file compiles
    MarkdownEditor(
      ownerType: "User",
      ownerId: me.Id,
      property: "Notes",
      face: editorial,
      outline: true,

01

A document is its sections

Markdown is how people write and how models write, so it is a type here, not a string that happens to hold prose. You assign markdown and read markdown back — but underneath, the platform splits the document on its headings and keeps one row per section. 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 {
  /// 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}";
1  [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.
2  [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.
3    allow read, create, update, delete where Owner == user;
  }
1

A short field is searchable the same way. The title is indexed on its row; the body's sections go into the app's memory, cut into sentence-sized pieces. One search spans both, and a hit on the body says which section and which sentence answered — see §5.

2

The document. One member, used like a string. It is stored as its sections, so a person editing one and an agent rewriting another are writing different rows — and neither overwrites the other. The ? is redundant: a document nobody has written is genuinely not there, and reads back null.

3

The rule, stated once. The editor in §2, the agent tools in §3 and the search in §5 all read and write through it. There is no second permission model for documents.

The same document, read A note in a personal archive: the Markdown member from the listing above, rendered by one atom in the app's own typeface. Read-only here, because a note you are only reading should not pay for the editor — that is §2, and it is the same rows. 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);
        }

02

The editor is a kit you opt into

A rendered view and a source view, section identity that survives arbitrary edits, partial saves with per-section preconditions and conflict recovery, a block handle and slash menu, an outline rail, tables, code highlighting, a selection toolbar, find and replace, footnotes, private images, maths and diagrams. It is one line in app.osyuse Osysharp.Markdown@1; — and an app that never writes that line never pays for it.

The editor, over a real document It takes the address of a document — an entity, a row, a member — not its text. A document is a section-structured thing the editor rewrites continuously; handing it the text as a prop would re-marshal the whole document on every keystroke. Flip to see the page that mounts it.
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
  render {
    MarkdownEditor(
      ownerType: "User",
      ownerId: me.Id,
      property: "Notes",
      face: editorial,
      outline: true,
      findQuery: query,
      replaceWith: replacement,
      findRequested: Find,
      matchesChanged: Matches,
      extraItems: [
        new MenuEntry { Label = "Archive this doc", Run = Archive },
        new MenuEntry { Label = "Delete block", Run = MarkdownEditor.DeleteBlock },
        new MenuEntry { Label = "Make it a heading", Run = () => MarkdownEditor.TurnIntoHeading(2) }
      ]
demo/markdown-demo/model/pages/editor.osyverbatim — this file compiles
  render {
    MarkdownEditor(
1      ownerType: "User",
      ownerId: me.Id,
      property: "Notes",
2      face: editorial,
      outline: true,
3      findQuery: query,
      replaceWith: replacement,
      findRequested: Find,
      matchesChanged: Matches,
4      extraItems: [
        new MenuEntry { Label = "Archive this doc", Run = Archive },
        new MenuEntry { Label = "Delete block", Run = MarkdownEditor.DeleteBlock },
        new MenuEntry { Label = "Make it a heading", Run = () => MarkdownEditor.TurnIntoHeading(2) }
      ]
1

An address, not a value. Which entity, which row, which member. The editor reads and writes the sections behind it directly, under the entity's own security rules.

2

Yours to style. The kit is headless: a reading face, a density, and the theme tokens it reads are the app's, so the document looks like the app and not like a widget.

3

The engine is the kit's; the bar is yours. The editor ships no find bar. It holds the matcher and raises events, and the page below draws whatever bar it wants in the editor's toolbar slot.

4

Your commands in its menu. An entry is a label and an action — the app's own, or one of the editor's, such as turning the block into a heading.

03

An agent edits with tools, not a text box

The moment an entity declares a Markdown member, the app's MCP server carries nine document tools. An agent reads the outline first, then addresses a section by its slug — and its edit lands on that section's row, which is why it cannot clobber the paragraph a person is typing in three headings down.

the tools, as the agent sees them · registered when any entity has 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
markdown_append   a new section, at the end or after a named one
markdown_rename   a section's heading, and optionally its level
markdown_move     a section, or its whole subtree, to after another
markdown_delete   a section, or its whole subtree
markdown_import   a whole document in, re-sectioned on its headings

Precision is the point. A model asked to fix one sentence in a long document will, given a text box, rewrite the document — and drift the eleven paragraphs it was not asked about. Given markdown_patch, it names a section and a string, and 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 same tools serve an agent declared inside the app: a document an agent produces as a deliverable is stored sectioned and indexed for recall, so it can be read section by section, edited afterwards, and found months later by what it says.

04

An answer renders while it is still arriving

A model's reply comes 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. Rendered literally that flickers raw syntax at the reader for a frame. So the renderer is told the text is still arriving, holds the unfinished tail as though it were closed, and settles it when the stream ends.

demo/chat-demo/model/pages.osyverbatim — this file compiles
/// The model's answer, arriving.
[Composable] component Reply(Guid chatId) {
1  live var answer = Answer(chatId);

2  on settled(answer) { Save(chatId, string.Concat(answer)); }

  render {
    Stack(gap: Space.S, w: "100%") {
3      Markdown(string.Concat(answer), streaming: !answer.Done);
      if (answer.Failed) { Text(answer.Error ?? "", color: Colors.Subtle); }
      else if (!answer.Done) { Text("…", color: Colors.Subtle); }
    }
  }
}
1

A stream is a value. Answer is a stream<string> that yields as the model does; binding it live re-renders the component as pieces land. The transport is a per-request, authenticated stream — no broadcast, no registry.

2

When it finishes, it is saved — as a real document. The transcript replays from stored rows through the same frames, so a reply looks the same the second time it is read as it did while it was being written.

3

The half that matters is turning it off. The renderer can see text being appended; what it cannot see is that the stream has ended. Flipping the flag is what re-renders the tail as ordinary markdown — a document left streaming keeps its caret forever.

05

Search is memory, and a hit is a citation

The word undersells it. Mark a member [Searchable] and its text is cut into sentence-sized pieces, each indexed on its own, with the whole 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. One call asks a question in your own words across everything the caller may read, and what returns is not a row. It is a hit that can say where it came from, when it was learned, on whose say-so, and what it is linked to.

demo/memory-lab/model/memory.osyverbatim — this file compiles
/// Search everything the caller may read. The plain case, and the one where an agent's own phrasing — not a
/// corpus generator's — decides whether retrieval works.
List<SearchHit> Recall(string query) {
1  return Memory.Search(query, limit: 5);
}

/// Search narrowed to ONE subject.
List<SearchHit> RecallAbout(Subject subject, string query) {
2  return Memory.Search(query, about: [subject], limit: 5);
}

/// The same, following stated links one hop out. Anything reached that way comes back with `Via` filled in, so an
/// agent can tell "this is about what you asked" from "this is about something related to it" — which is the
/// distinction the whole hop design turns on.
List<SearchHit> RecallRelated(Subject subject, string query) {
3  return Memory.Search(query, about: [subject], related: 1, limit: 8);
}

/// State that two subjects are related, in words.
bool Relate(Subject a, Subject b, string reason, string reverseReason) {
4  return Memory.Link(a, b, reason: reason, reverseReason: reverseReason);
}
1

One question, the whole corpus. Meaning first — "when do they want the report?" finds "the monthly numbers go out as a spreadsheet" with no words in common. An order number or a product code in the query is also matched exactly, and weighted heavily; a question with no such token runs on meaning alone, because a literal matcher on ordinary words returns confident nonsense.

2

Narrowed to a record. about: names instances, of: names types, by: names who authored a memory, kinds: and origin: say what sort of memory you will accept. The narrowing is of the search, not the result: a hit about the record you named is never crowded out by the rest of the corpus.

3

One hop along stated links. Anything reached that way carries the link's own words in Via"superseded by — renegotiated after the Q2 review" — so a reader can tell about what you asked from about something related to it. A linked memory can take every place but the first: the lead result belongs to the record you named.

4

A relationship in your app's words, readable from either end. Every hit about either record then carries the link, so whoever reads the result can decide whether to go and fetch the other one.

what one hit carries · SearchHit, from the reference
Content
  the whole section that answered — never the fragment that matched it
Score · Distance
  how relevant, and how close; recency breaks a tie, so a correction beats what it corrected
SourceLabel · SourceEntityType · SourceEntityId
  the record in words, and the row a machine can load
Section · PageFrom · PageTo · SourceDocument
  the heading it sits under; the pages and the file, when the source has them
MatchOffset · MatchLength
  the sentence inside Content that actually matched — the thing to quote
RememberedAt · Origin · AuthoredBy
  when it was learned; whether the record said it or somebody chose to remember it; who
Via · Links
  how a hop reached it, in the link's words; what the record connects to, and why
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
}

A quoted fact nobody can check is not much better than an unquoted one. That is why a hit carries the record's name as words, the heading, the page range and the offset of the sentence that matched — enough to say "Contract MSA-2024-11, p. 45" and be right. And why it carries Origin: the contract says and somebody said are different claims, and an agent building an answer out of hits should know which it is holding.

None of it is a new data path. A hit you may not read never appears, exactly as a filtered query never returns a row you cannot see; memory is reachable only through the record it is about. Files join the same memory when somebody says so — Memory.Remember(file, about: row) — and the memory records who. With no embedding provider configured the same fields work as full-text search from the first deploy, and start ranking by meaning the day one is wired, with no change to your code. For one entity's own rows with a ranking you compose, the same relevance is available as query verbs — Matches, TextScore, Similarity — that you weight with plain arithmetic.

What happens when you change it

Document changes, and what they cost

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.
Paste HTML into a documentcompilesIt renders as text. A document is content, never markup — there is no way in to the page through one.
Mark the member [Searchable]compilesEach section is cut into pieces and indexed from then on; a hit names the section and the sentence. No index to build, no vector you ever touch.
Search before an embedding provider is configuredcompilesIt works as full-text search from the first deploy, warns that meaning-based ranking is off, and upgrades the day one is wired — with no change to your code.
Ask for related: hops with no about:refusedA compile error: following links out of the whole corpus would reach everything linked to anything, so a hop needs a starting record.
Leave streaming: true after the reply has endedcompilesIt compiles — and the document keeps its caret forever. Turning the flag off is what settles it, which is why it is bound to Done.
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.