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.client() directly. Passing ctx explicitly is also supported.<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)">
<input type="text" name="message" required/>
<button type="submit">Send</button>
</form>
</island>
</component>Models
A model method reaches the caller's authenticated GraphQL client ambiently — client() is in scope during any render or action — 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. No context threading.
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()" runs this on the server
@Serializable
class ReaderMarkModel(val metadataId: String, var markId: Long? = null) {
val saved: Boolean get() = markId != null
suspend fun toggle() {
val current = markId
markId = if (current == null) {
client().execute(AddMark, AddMark.Variables(metadataId))
lookUpNewMarkId(metadataId)
} else {
client().execute(DeleteMark, DeleteMark.Variables(id = current))
null
}
}
}Privacy & escape hatches
By default, live state has page scope: it round-trips through the page and a refresh starts from the server-rendered model. Use scope="client-session" to retain it for the current browser session, or scope="server-session" to keep the model's JSON on the server behind an opaque HttpOnly cookie. Server session scope suits private state such as carts and entitlements, or state that 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.
Localized sites and versioned, email-safe transactional templates.
The compiler, the IDE plugin, the dev loop, and one-process deploys.