Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

on settled — run something once, when a stream finishes

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

`on settled(x) { … }` runs once when the stream `x` finishes arriving — whether it completed or failed. It is how you store a streamed answer, and it runs exactly once without you having to guard it.

preview1 example compiled by CIuicomponentstreaminglifecycle

Summary#

A stream<T> arrives in pieces and then stops. on settled(x) { … } is the block that runs when it stops — once, whichever way it ended. That is where you save what arrived.

Signature#

on settled(<a live var holding a stream>) { … }

Description#

A component that watches a stream usually has something to do when it finishes: store the answer, mark the conversation read, move on to the next step. on settled is that block.

It runs exactly once, and you do not write a guard for it. It is attached to the moment the stream stops being open, which happens once and cannot happen twice. This matters more than it sounds, because the obvious alternative does not work:

on change { if (answer.Done) { Save(chatId, string.Concat(answer)); } }   // ✗ runs again and again

on change is a reaction — it re-runs whenever a value it read changes. answer.Done stays true once the stream is done, so the block fires again on every later render. In a real app this stored one reply fourteen times.

It runs when the stream FAILS too, not only when it completes. A reply that broke off halfway still said what it said, and the pieces that arrived are still there — so a block that saves the result gets to save the partial. If you need to tell the two apart, ask the stream:

on settled(answer) {
  if (!answer.Failed) { Save(chatId, string.Concat(answer)); }
}

It may write the component's own state, which an on change block may not. A block that runs once cannot loop, so there is nothing to protect against — the same reason on mount may.

Name the stream. A component can watch more than one, so on settled always says which: on settled(answer). Naming something that is not a stream is a compile error, and so is giving one stream two settle hooks — both would run and the second one's writes would win, so put everything in one block.

If nobody is watching, it does not run. The hook belongs to the component, so a reader who navigates away before the answer finishes never triggers it. Anything that must happen regardless belongs on the server, in the function that produces the stream.

Examples#

Store a model's reply when it finishes — the whole reason the block exists:

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); }
  }
}

See also#

Related

LlmClient.Stream — a model's answer as it is written

`LlmClient.Stream(prompt)` gives you a model's answer in the pieces it was produced in, so a reader sees it being…

The reactivity & lifecycle model

How an Osy# component comes alive and stays in sync: declarations are live value bindings, `on mount`/`on unmount` are…

on change

`on change { … }` is a reactive **side-effect**: the runtime re-runs it whenever a value it read changes, so it's how…