container_node
Your stage. The runtime gives you an empty wrapper with class widget_wrapper. Replace its children with your own DOM and you are done.
Xuda Developer Plugins
Each category's execution path is a short, predictable sequence. Before you write any plugin code, skim the matching path so you know where your function sits inside the runtime.
<xu-widget> to rendered DOM.
This is the full path func.runtime.widgets.render_node executes for a widget-backed node.
Each line maps to a function in runtime/js/xuda_runtime.browser.widgets.js.
async function render_node(options) {
// 1. Create or reuse the widget container
let $div = exist_elm_obj.div || await func.runtime.ui.create_container({...});
// 2. Resolve plugin metadata
const widget_context = func.runtime.widgets.create_context(SESSION_ID, paramsP, prop);
const { plugin_name, method, propsP, plugin: _plugin } = widget_context;
const definition = await func.runtime.widgets.get_definition(widget_context);
// 3. Environment & method guard
if (!func.runtime.widgets.supports_current_environment(definition)) {
return report_error(`plugin ${plugin_name} is not available in the current environment`, true);
}
if (!definition.methods[method]) return report_error('method not found');
// 4. Coerce typed field values from the node's props
const fields_ret = await func.runtime.widgets.get_fields_data(
widget_context, definition.methods[method].fields, propsP,
);
const fields = fields_ret.data;
// 5. Apply any xu-exp: expressions on the wrapper node
await func.runtime.render.set_attributes_new({ $elm: $div, ... });
// 6. Stylesheet (conditional)
if (_plugin.manifest['style.css'].exist) func.runtime.widgets.load_css_style(widget_context);
// 7. Per-app setup + API utils
const plugin_setup_ret = await func.utils.get_plugin_setup(SESSION_ID, plugin_name);
const api_utils = await func.common.get_module(SESSION_ID, 'xuda-api-library.mjs', {
func, glb, SESSION_OBJ, SESSION_ID, APP_OBJ,
dsSession: paramsP.dsSessionP,
job_id: jobNoP,
});
// 8. Build the params object
const params = func.runtime.widgets.build_params(
widget_context,
func.runtime.ui.get_first_node($div),
func.runtime.ui.get_data($div),
plugin_setup_ret.data,
api_utils,
);
// 9. Import runtime.mjs and invoke the method
const fx = await func.runtime.widgets.get_resource(widget_context, 'runtime.mjs');
await func.runtime.widgets.load_runtime_css(widget_context);
try {
await fx[method](fields, params);
} catch (err) {
func.utils.debug_report(SESSION_ID, `${plugin_name} widget`, err.message, 'E');
}
return $div;
}
Every widget method signature is async (fields, params) => …. The fields
object is the resolved, coerced field values. params is the runtime context.
params = {
SESSION_ID, // current session id (string)
method, // the method being invoked
plugin_name, // full package name, used by debug_report
container_node, // the mounted DOM element the plugin fills
plugin_setup, // resolved per-app setup (API keys, connection, ...)
_session: { // runtime session metadata
app_id, domain, app_mode, gtp_token, ...
},
api_utils: {
invoke_event(event_id), // trigger a named Xuda event
debug_report(source, msg, level), // log to the runtime debug pane
datasource: {
update(SESSION_ID, eventChanges), // push a diff back into the DS
},
...
},
};
container_nodeYour stage. The runtime gives you an empty wrapper with class widget_wrapper. Replace its children with your own DOM and you are done.
plugin_setupThe cached setup payload from the app's plugin configuration. Contains credentials and config specific to the installed plugin, resolved through get_plugin_setup.
api_utils.invoke_eventTriggers a named event on the app. Used to implement callbacks after async work completes: after a Firebase login, after a Stripe tokenization, after an AI response.
api_utils.debug_reportSend a line to the runtime debug console. Preferred over console.log because the studio surfaces these per plugin.
Libraries that do asynchronous work (auth, payments, AI) accept two special fields on every method:
event_name_for_successful_callback and field_id_to_return_result. The
studio renders them as an event picker and a field picker. At runtime, the plugin invokes the callback
event and writes the response into the chosen field.
const execute_event_successful_callback = async function (e) {
if (fields.event_name_for_successful_callback) {
await e.api_utils.invoke_event(fields.event_name_for_successful_callback);
}
};
const update_SYS_GLOBAL_OBJ_FIREBASE_AUTH_INFO = async function (obj, error) {
// Write the response into the shared datasource field
for (let [key, val] of Object.entries(obj)) {
SESSION_OBJ[SESSION_ID].SYS_GLOBAL_OBJ_FIREBASE_AUTH_INFO[key] = val;
}
SESSION_OBJ[SESSION_ID].DS_GLB[0].data_system["SYS_GLOBAL_OBJ_FIREBASE_AUTH_INFO"] =
SESSION_OBJ[SESSION_ID].SYS_GLOBAL_OBJ_FIREBASE_AUTH_INFO;
// Push the diff through the datasource
const eventChangesResults = {
rows_changed: [""],
fieldsChanged: ["SYS_GLOBAL_OBJ_FIREBASE_AUTH_INFO"],
dataset_obj: { "": { SYS_GLOBAL_OBJ_FIREBASE_AUTH_INFO: ... } },
dsSession: 0,
};
await func.datasource.update(SESSION_ID, eventChangesResults);
// Fire the callback event
const event_id = get_property_value("widgetCallbackEvent");
if (event_id && !error) {
await func.events.validate(SESSION_ID, "user_defined", dsP, event_id, "program");
}
};
UI plugins never own the container. The framework renders the element; the plugin decorates it.
The signature is fn(plugin_name, el, properties), called once on mount and optionally
on prop updates.
xuda-ui-plugin-momentexport const fn = function (plugin_name, el, properties) {
const raw = $(el)?.text();
const d = fixTimestampString(raw) || null;
const format = properties.moment_format?.value || "YYYY-MM-DD HH:mm:ss";
const locale = properties.moment_locale?.value || "en";
const showLive = properties.moment_show_current_time?.value || false;
const relative = properties.moment_relative_time?.value || false;
const calendar = properties.moment_calendar_time?.value || false;
moment.locale(locale);
const update = () => {
const value = relative ? moment(d).fromNow()
: calendar ? moment(d).calendar()
: moment(d).format(format);
el.innerHTML = value;
};
update();
if (showLive) setInterval(update, 1000);
};
{ value } — the studio evaluates the author's
expression and passes the result under value. Always read properties.X?.value,
not properties.X directly.
Database plugins never reach the browser. The runtime server imports server.mjs and
dispatches through named exports. The payload passed in is fully resolved: the app's setup doc, the
table definition, the index definitions, and the operation to perform.
import * as plugin_server from `@xuda.io/xuda-dbs-plugin-${name}/server.mjs`;
const ret = await plugin_server.read({
SESSION_ID,
setup_doc, // decrypted per-app accounts
table_obj, // resolved table definition + indexes
view, // the logical index being queried
key, // the query key
params,
});
// ret = { code: 1, data: rows } on success, { code: -1, data: 'message' } on failure.
Exceptions are caught and reported rather than bubbling. The runtime writes a debug entry, shows a studio-time error badge next to the node, and keeps the rest of the page rendering.
try {
await fx[method](fields, params);
} catch (err) {
func.utils.debug_report(options.SESSION_ID, `${plugin_name} widget`, err.message, 'E');
}
For programming errors — missing setup, unknown method. The runtime logs and surfaces them cleanly.
For expected validation failures — missing field, empty selection. Prefer invoking a failure event and writing a { error_code, error_msg } response into the return field.
Use api_utils.debug_report so every message is grouped by plugin name in the studio. Avoid raw console.log, which is noisier and not searchable.
Set capabilities.browser = false when a plugin must stay server-side. The loader will short-circuit with a friendly message instead of trying to render.
The runtime owns container_node. When the surrounding program unmounts or the wrapping
node is removed, the container is dropped from the DOM. Any timers, observers, or event listeners
attached to descendant nodes go with it.
If you open a setInterval, attach its id to the element (el.dataset.timer = id) or to container_node so a MutationObserver can clear it, or rely on the node being removed.
Heavy integrations (Google Maps, Stripe) often register globals. Namespace them with the plugin id and avoid leaking across re-renders.
Use the xu-cdn manifest in the studio HTML node to declare dependencies. The slim runtime loads them once; multiple plugins can share a single tag.
Framework plugins expose core.discard(id) for exactly this reason: when the theme script attached in core.init should be removed, call the matching discard.
The studio is the first consumer of index.mjs. It generates the properties pane, validates
field values, offers the event picker, and records defaults. Your plugin is usable the moment the
schema lands; the runtime only runs after the app is previewed or deployed.
Each field becomes a row. label is the row label, tooltip is the hint, render picks the input, options feeds the choices.
Shows the package's svg_icon, description, keywords, and rendered README.md.
Dbs and framework plugins' tabs(doc) drive the app-level setup UI. One tab per tab id; fields per tab. Values are saved into the app's setup doc.
The studio-ai-app-builder plugin introspects your fields and tabs schemas so the AI can configure new plugin instances on behalf of the user.
// === capabilities (widget, library) ===
export const capabilities = { browser: true, headless: false };
// === methods (widget, library) ===
export const methods = {
<method_id>: {
name: "Human Name",
tooltip: "Shown in the studio",
fields: { <field_id>: <FieldDef> },
response: {},
event_name_for_successful_callback: <FieldDef>, // optional
field_id_to_return_result: <FieldDef>, // optional
},
};
// === properties (ui) ===
export const properties = { <property_id>: <FieldDef> };
// === tabs (graphics: array, dbs/framework: function) ===
export const tabs = [ { label, items: [ { name, tags, html } ] } ];
export function tabs(doc) { return { <tab_id>: { label, type, fields, svg_icon } }; }
// === public_filter (dbs, optional) ===
export function public_filter(doc) { return { accounts: {} }; }
// === core & primitives (framework) ===
export const core = class { init(setup){} discard(id){} refresh(ct,ds){} rootTagName(){} renderEngineProperties(){} };
export const bind = class { setter(el,v){} getter(el){} listener(el,cb){} };
export const toast = class { async create(type, message, title){} };
export const loader = class { async create(){} };
export const popover= class { async open(e){} async set(e){} async close(e){} };
export const popup = class { async create(header,sub,msg,buttons){} };
export const modal = class { async create(e){} async init(e){} async present(m){} async close(){} };
export const page = class { async create(e){} async init(e){} };
// === runtime.mjs ===
// widget, library: one async function per method
export async function _default(fields, params) { /* ... */ }
// ui: one function on the mounted element
export const fn = function (plugin_name, el, properties) { /* ... */ };
// graphics: the tabs array directly
export const tabs = [ /* ... */ ];
// === server.mjs (dbs, privileged libraries) ===
export async function read(e, docP, table_obj) { /* ... */ }
export async function upsert(e, docP, table_obj) { /* ... */ }
export async function delete_(e, docP, table_obj) { /* ... */ }