InlayDocs

Outputs and bindings

Two views of a produced value, and which one you want.

An agent declares outputs — named values it produces, like plan or assessment. The SDK surfaces them two ways, and picking the wrong one is the most common source of "why is this empty".

useBinding("plan")output rows
Answers"What is plan now?""Every time plan was produced."
ScopeCross-turn, latest winsPer-turn history, in order
PositionA keyed mapInline in the feed
Use forA sidebar, a preview paneA transcript that shows work as it happened

The latest value

function Sidebar() {
  const plan = useInlayChat(conn).useBinding("plan");
  if (!plan) return null;

  return (
    <aside data-building={plan.status === "building"}>
      {typeof plan.value === "string" ? plan.value : JSON.stringify(plan.value)}
    </aside>
  );
}

status is "building" while a streamed artifact is still arriving and "final" once it settles, so you can render progressively. format is present for streamed artifacts (e.g. markdown) and absent for whole-value outputs.

The production history

function PlanHistory() {
  const { snapshot } = useInlayChat(conn);
  const plans = snapshot.rows.filter(isOutputRow).filter((row) => row.name === "plan");

  return plans.map((row, i) => <pre key={row.id}>{String(row.value)}</pre>);
}

Use this when the agent refines something across turns and the earlier versions matter. bindings.plan has already forgotten them.

isOutputRow is a type guard, so row.name and row.value are available after it without a cast.

Some outputs are collections

Everything above is about slots — one value per name. An output declared as a collection behaves differently: it is a table of rows, each with its own identity and optional key, streamed row-by-row and edited row-by-row. Slots read through useBinding; tables read through useTable, and a generated client makes mixing the two up a compile error. The full story is State and memory.

Some values outlive the run

A declared output may be durable — kept per end user or per agent and rehydrated into the next conversation — or per-run (the default). A durable binding is already populated when you reopen a conversation, before anything streams. And any binding, durable or not, can be edited in place by the agent (artifact.edit) — useBinding simply re-renders with the new value; there is nothing to handle. Both are covered in State and memory.

Values are unknown

value is unknown on purpose. It is model output: the agent said it produced a plan, and nothing on the wire proves the shape.

You have two honest options:

  1. Narrow it yourselftypeof value === "string", a schema check, a parse.
  2. Generate a client. If the output declares a JSON Schema, inlay codegen emits its precise type and wires runtime validation, so the type is a checked cast rather than an assertion. See Typed clients.

What you should not do is cast blindly. An agent republished with a changed output shape will hand you something else, and a cast turns that into a render crash instead of a value you can check.

Output names are strings — until they are not

useBinding("plnn") compiles fine and returns undefined forever. So does rows.find(r => r.name === "shopping_list") against an agent that calls it grocery_list.

This is the single most annoying class of bug in an agent UI, because nothing fails — the panel is just empty. A generated client makes both of those compile errors. It is the main reason to generate one.

Next

What about values that persist, stream row-by-row, or change after they settle — State and memory.

On this page