Connection, actions, state, and events
Find public operations and lifecycle handling after obtaining the page bridge.
Obtain the interface and await readiness
The host injects the bridge into trusted local pages; no network SDK download is needed. Ordinary browser and development-server previews cannot communicate with Blender. Obtaining the object does not mean the handshake succeeded.
const webui = window.acquireNexusWebUI();
const info = await webui.ready();
// info 是握手信息,不是另一个 SDK 对象。export interface WebUIReadyInfo {
protocolVersion: number;
wireRevision: number;
connectionEpoch: number;
features: string[];
[extra: string]: WebUIJSONValue | string[] | undefined;
}
window.whenNexusWebUIReady() also returns readiness information. features is the negotiated capability set, not proof that application logic ran. Handle initialization failures and do not access internal globals.
Arguments, timeouts, and errors
export interface WebUIRequestOptions {
/** Wait for an individual request response in milliseconds. Default 5000. */
timeoutMs?: number;
/** Extra automatic retries for pure reads (get/describe). 0-3, default 2. */
retryAttempts?: number;
}export interface WebUIError extends Error {
name: 'WebUIError';
/** Stable wire error code, e.g. 'permission_denied', 'resync_required'. */
code: string;
retryable?: boolean;
debugId?: string;
}export type WebUIJSONValue =
| null
| boolean
| number
| string
| WebUIJSONValue[]
| { [key: string]: WebUIJSONValue };timeoutMs defaults to 5000 ms and budgets one page request. It is neither a Python task cancellation deadline nor the total workflow timeout. Readiness, retries, and uploads can increase total duration.
retryAttempts applies to supported read-only operations: an integer from 0 to 3, defaulting to 2 additional attempts. state.get and state.describe use it for retryable errors; actions are not automatically resent. Repeated failures may temporarily reject reads.
Handle rejections with try/catch or Promise.catch. Preserve code, message, and debugId when present. retryable does not mean every operation is safe to repeat. After a write timeout or disconnect, query the actual result before risking duplicate creation, deletion, or export.
action.call: invoke an add-on action
await webui.action.call(name, payload, options);The name must exactly match Python registration. Omitting payload sends null; other payloads follow the JSON contract. Returns a Promise whose successful value is the add-on's JSON result, not a fixed success object.
async function showEcho(output) {
const webui = window.acquireNexusWebUI();
try {
const result = await webui.action.call("plugin.my_addon.echo", {text: "你好"});
output.textContent = result && result.ok === true
? result.text
: "动作已返回,请检查业务结果。";
} catch (error) {
output.textContent = `${error.code || "调用失败"}:${error.message}`;
}
}
An application result of {ok: false} does not automatically reject the Promise; follow the action's contract. Use action.callWithResource for attachments. For the complete file flow, see Resource readers and file tools; for Python arguments and context, see Actions and call context.
state.get and describe: one-time queries
const result = await webui.state.get(key, query, options);
const schema = await webui.state.describe(key, options);export interface WebUIStateGetResult {
canonicalKey: string;
snapshot: WebUISnapshot;
stateRef?: string;
}
get returns a wrapper containing canonicalKey and snapshot. List data is in
result.snapshot.items; single-item data is in
result.snapshot.item and may be null. It does not create a subscription. snapshotRevision is host-managed; do not increment it yourself or treat it as an application counter.
describe returns state descriptions: omit key or pass null for the directory, or supply a key for one description. It returns an object, not a subscription handle. Check actual descriptions for query capabilities; a field in TypeScript does not imply every state accepts it.
export interface WebUIStateQuery {
fields?: readonly string[];
filter?: Record<string, WebUIJSONValue>;
sort?: readonly WebUIStateSortEntry[];
limit?: number;
cursor?: string | null;
/** Detail states: single target id, e.g. 'obj:1024'. */
id?: string;
/** Detail states: batch lookup. */
ids?: readonly string[];
sceneId?: string;
viewLayerId?: string;
[extra: string]: WebUIJSONValue | readonly WebUIStateSortEntry[] | undefined;
}export type WebUIStateSortEntry =
| string
| { field: string; direction: 'asc' | 'desc' };fields selects fields, filter supplies filters, sort accepts field names or direction objects, and limit caps results. Current custom states and the built-in states below do not accept cursor. Use extra arguments such as id, ids, and sceneId only as supported by that state. Custom Python state queries accept fields, filter, sort, and limit; the add-on handler performs filtering and sorting. See State declarations and query scopes.
stateRef is an opaque guard reference returned by some one-time blender.* queries, not guaranteed for custom state. For guarded writes, retain the actual snapshot and matching reference. Do not construct, guess, or permanently cache guard fields.
Built-in Blender state and object operations
These states are provided by the framework without registration. When sceneId/viewLayerId are omitted, they use the Blender context bound to the page window. Obtain identifiers from actual state, not object names or invented IDs.
| State | Results and queries |
|---|---|
| blender.context | snapshot.item contains sceneId, viewLayerId, mode, and windowId. |
| blender.objects / blender.selection | snapshot.items contains all or selected objects in the current view layer. Default fields are id and type; the default limit is 100 and can be raised to 1000. |
| blender.activeObject | snapshot.item is the active object, or null if none is active. |
| blender.object | Requires id or ids. id returns one object; ids returns a batch in snapshot.item.items and may include missingIds. At most 1000 IDs are accepted, and results still respect limit. |
Object lists filter by type, visible, active, and selected, and sort by id or type. Explicitly request name through fields; name is not sortable. Details default to id, type, location, rotation, scale, visible, and selected. rotation is an array in radians corresponding to rotation_euler in transforms. Check truncated: count and message-size limits mean list length is not the scene's total object count. An initial query for a nonexistent object fails; if an object is deleted after a detail subscription is established, item may become null.
action.call("blender.object.select", payload)
Deselects other objects in the target view layer, selects and activates the target, and returns {id, selected:true}.
blender.object.delete Deletes the object data and its links, returning {id, deleted:true}; this is not merely removal from the current collection. These actions do not show confirmation dialogs or promise an undo step. Implement confirmation and undo in your add-on as needed.
Both built-in actions require id, stateKey, and canonicalKey, plus either a query's stateRef or a valid subscription's subscriptionId and snapshotRevision. stateKey must refer to one of the four object states above, and the target must be in that snapshot. If the source is stale or the object changed, read again and confirm. Do not guess guard fields or blindly retry deletion. For custom plugin.* action guards, see Action state guards.
state.subscribe: live snapshots and release
const handle = await webui.state.subscribe(key, query, (snapshot, meta) => {
// snapshot 可以为 null;先检查 meta.error。
}, options);export interface WebUISubscriptionHandle<TSnapshot = WebUISnapshot> extends WebUIReleasable {
readonly subscriptionId: string | null;
readonly canonicalKey: string | null;
getSnapshot(): TSnapshot | null;
}export interface WebUISubscriptionMeta {
canonicalKey: string | null;
subscriptionId: string | null;
/** Present when the subscription died with an error frame. */
error?: WebUIError;
}Callbacks receive the snapshot itself, not the get wrapper. A callback can run before the subscription Promise resolves, so do not read an unassigned handle in the first callback. A ready handle does not guarantee the first snapshot arrived; getSnapshot() may still be null.
meta.error indicates a subscription error; stop treating old data as current. Identical queries may share internal subscriptions, but each component retains and releases its own handle. Prefer
await handle.release(); alternatively use the unsubscribe alias or
webui.state.unsubscribe(handle, options). When passing a subscription ID string, know its owner to avoid removing a shared listener incorrectly.
For actions declaring state_guard_keys, use the actual subscribed state name for stateKey. Obtain canonicalKey and subscriptionId from the current handle or callback meta, and snapshotRevision from its corresponding snapshot. Submit only without subscription errors and with all fields present. Update guards with new snapshots; never mix fields from different subscriptions. Resource writes also require target IDs from that snapshot. See Action state guards.
export interface WebUIReleasable {
release(options?: WebUIRequestOptions): Promise<{ unsubscribed: boolean }>;
unsubscribe(options?: WebUIRequestOptions): Promise<{ unsubscribed: boolean }>;
}function mountPresets(output) {
const webui = window.acquireNexusWebUI();
let closed = false;
let handle = null;
const pending = webui.state.subscribe(
"plugin.my_addon.presets", null, (snapshot, meta) => {
if (closed) return;
output.textContent = meta.error
? `${meta.error.code}:${meta.error.message}`
: snapshot ? `收到 ${snapshot.items.length} 项预设` : "正在读取……";
},
).then(async (subscription) => {
if (closed) await subscription.release();
else handle = subscription;
});
// 页面中显示订阅或异步释放失败,不留下未处理的拒绝。
pending.catch((error) => {
if (!closed) output.textContent = `${error.code || "订阅失败"}:${error.message}`;
});
return async () => {
closed = true;
if (handle) {
const current = handle;
handle = null;
await current.release();
}
await pending;
};
}The example uses the preset list state in the Python reference. Retain the cleanup function, await it on unmount, and handle cleanup failures without writing to removed controls.
state.store: a page-readable state container
export interface WebUIStoreOptions extends WebUIRequestOptions {
/**
* Snapshot shape of the underlying state: 'list' (default) yields
* items/ids snapshots, 'item' yields a single-item snapshot.
*/
shape?: 'list' | 'item';
}export interface WebUIStateStore<TSnapshot extends WebUIStoreSnapshotBase = WebUIStoreSnapshot> {
readonly key: string;
readonly shape: 'list' | 'item';
readonly disposed: boolean;
/**
* Current snapshot, synchronously. Reference-stable: equivalent content
* keeps the exact same object, so shallow compares can bail out.
*/
getSnapshot(): TSnapshot;
/** Calls listener immediately with the current snapshot, then on changes. */
subscribe(listener: WebUIStoreListener<TSnapshot>): WebUIStoreUnsubscriber;
/** Calls listener immediately, then only when selected value or store meta changes. */
select<TSelected>(
selector: WebUIStoreSelector<TSnapshot, TSelected>,
listener: WebUIStoreSelectorListener<TSelected>,
equalityFn?: WebUIStoreEquality<TSelected>,
): WebUIStoreUnsubscriber;
dispose(): void;
}
webui.state.store(key, query, options)
Returns a store synchronously while establishing its subscription asynchronously. shape defaults to list; specify item for single-item Python state. getSnapshot() always returns current store state, but initially loading is true. Initial empty data is not a host-confirmed empty result.
| Interface or field | Usage and limitations |
|---|---|
loading / error |
Loading ends on a real snapshot or error. Old values may remain on error; always check error too. |
items / ids / entities |
List stores provide items, ordered IDs, and entities indexed by ID, plus truncated and nextCursor. |
item |
Single-item stores provide an object or null, not list data. |
subscribe(listener) |
Immediately calls back with the current snapshot, then on changes. Returns a synchronous cleanup function removing only this listener. |
select(selector, listener, equalityFn?) |
Computes the selected value immediately, then calls back when it or subscription metadata changes. Uses Object.is by default. |
dispose() |
Synchronously releases the entire store and listeners. A component should destroy only stores it owns. |
Equivalent content reuses snapshot references. Do not mutate snapshots or entity objects. Keep edits separately and submit actions after confirmation. A temporary disconnection does not automatically set loading; listen for connection status and disable writes. Frontend integration is covered in Frontend adapter reference.
event.on and lifecycle.on: two notification types
const eventHandle = await webui.event.on(name, (payload, meta) => {}, options);
const lifecycleHandle = webui.lifecycle.on("disconnected", (payload) => {});export interface WebUIEventMeta {
name: string;
subscriptionId: string;
/** True when the host dropped older events for this subscription. */
dropped: boolean;
}export interface WebUIEventHandle extends WebUIReleasable {
readonly subscriptionId: string | null;
readonly name: string;
}export interface WebUILifecycleHandle {
release(): WebUIReleaseResult;
unsubscribe(): WebUIReleaseResult;
}Add-on events deliver the actual JSON payload. meta.dropped means older notifications were dropped; query current application facts again. Events neither replay history nor persist task records. event.on and its release are asynchronous. lifecycle.on is a synchronous local listener whose release returns {released}.
| Interface or field | Usage and limitations |
|---|---|
ready |
A handshake completed. Listeners do not replay history; initialization should still await ready() rather than waiting only for an event. |
disconnected |
The connection disconnected and pending requests fail. The SDK attempts reconnection and restores still-valid state and event subscriptions. |
reconnected |
Sent after restoring the handshake and active subscriptions. Check actual application results; do not duplicate listeners or replay writes. |
stale / session_closed |
The old document or session is invalid and subscriptions terminate. Do not treat it as a temporary disconnect while waiting for the old page to recover. |
For event sending and receiving examples, see Events and notifications. Keep notification callbacks short and handle asynchronous rejections yourself; the framework does not await their application work.
Close the current window and clean up the page
export interface WebUIWindowAPI {
close(options?: WebUIWindowCloseOptions): Promise<WebUIWindowCloseResult>;
}export interface WebUIWindowCloseOptions extends WebUIRequestOptions {
reason?: string;
}export interface WebUIWindowCloseResult {
closed: true;
}
await webui.window.close({reason: "完成"})
Requests closure of the window containing this page, not arbitrary windows or Blender itself. reason is optional. Success returns {closed:true}; failures reject. A closing page may not display a final message, so do not use that as your only completion evidence.
| Interface or field | Usage and limitations |
|---|---|
Event/state handles |
await handle.release(); release only subscriptions owned by the component. |
Connection listeners |
handle.release() returns its release result synchronously. |
State stores |
Remove your listeners first. Call store.dispose() only if you own the store. |
Transform gestures |
Finish or cancel your own operation; see the transform interaction reference. |
webui.dispose() |
Terminates the document's shared bridge. Not suitable for unmounting an individual component, and does not close the Blender window. |
Repeated acquisition within one page returns a shared SDK. Do not destroy the whole connection when switching one tab region. Components must also remove their DOM listeners, timers, and asynchronous work.