Live state & actions
An island renders complete HTML on the server and is the region the runtime can update in place afterwards. Most islands never need a client script — declarative actions cover them.
The action loop
A live state is a serializable model a page provides, reflected by one view island. @click or @submit on any element names a method on it. When the event fires, the runtime posts the state to the server; the server runs the method, persists the new state, re-renders the island, and the runtime swaps the view in place — the same component code as the first render, so the markup stays consistent.
form.<field> reads the bound form live at event time; checkboxes become booleans.ctx is supplied server-side, for methods that reach the data plane.<component tag="chat-panel">
<prop name="channelId" type="String" required/>
<prop name="messages" type="List<myapp.MessageRow>" required/>
<script server provides="panel">
myapp.ChatPanelModel(channelId, messages)
</script>
<island name="chat-view" :key="channelId">
<for m in panel.messages>
<chat-msg :message="m"/>
</for>
<form @submit="panel.send(form.message, ctx)">
<input type="text" name="message" required/>
<button type="submit">Send</button>
</form>
</island>
</component>Models
A model method that takes ctx receives the caller's authenticated GraphQL client — so toggling a bookmark or sending a message runs as the person viewing the page, with the same permissions they'd have anywhere else on the platform.
Components can declare their own live state too — one model per instance, so a card inside a <for> loop carries its own toggle without a page-level über-model.
// @click="mark.toggle(ctx)" runs this on the server
@Serializable
class ReaderMarkModel(val metadataId: String, var markId: Long? = null) {
val saved: Boolean get() = markId != null
suspend fun toggle(ctx: RenderContext) {
val gql = ctx.gql ?: return
val current = markId
markId = if (current == null) {
gql.execute(AddMark, AddMark.Variables(metadataId))
lookUpNewMarkId(gql, metadataId)
} else {
gql.execute(DeleteMark, DeleteMark.Variables(id = current))
null
}
}
}Privacy & escape hatches
By default, live state round-trips through the page and survives a refresh per tab. Adding scope="server" keeps the model in a server-side session instead — the model's JSON never reaches the client, only an opaque HttpOnly session cookie. Use it whenever the state should stay private, like carts and entitlements, or is too large to round-trip.
When a flow doesn't fit the action model, declare a <contract>: a Kotlin interface in the page file. The compiler emits a server dispatcher and a typed TypeScript stub, and client code calls the method as an ordinary typed async function.
<contract>
interface GroupChatOps {
suspend fun updateLastRead(channelId: String, sequence: Long)
}
</contract>
<script client>
import { GroupChatOps } from "./GroupChatOps"
// The generated typed stub — a plain async call.
GroupChatOps.updateLastRead(channelId, Number(maxSequence))
</script>Keep exploring
What BML is, what makes it powerful, and how to use it.
Pages, components, control flow, and scoped styles.
The typed GraphQL data plane and client-managed auth.
The compiler, the IDE plugin, the dev loop, and one-process deploys.
Ready for the reference? Read the BML documentation