Action reference

← Widgets · Documentation · Next: Themes →

This page defines every built-in action, all fields and defaults, execution semantics, errors, and examples.

Action envelope

Every action is a JSON object with a case-sensitive type:

{
  "type": "patch",
  "minSpec": 2,
  "patch": [
    { "op": "add", "path": "$root/state/status", "value": "ready" }
  ],
  "onError": {
    "type": "toast",
    "message": "$error.message",
    "style": "error"
  }
}

Common fields:

FieldTypeDefaultMeaning
typestringrequiredBuilt-in action type, or custom
minSpecintegerabsentSkip action when greater than BduiSpec.CURRENT
onErroractionabsentOptional handler for this action's failure

Unknown, unregistered, and too-new actions are compatibility skips, not errors: log, no-op, and continue a surrounding sync. The server uses X-Bdui-Spec to send compatible behavior.

Catalog

GroupActions
State/tree[patch](#patch)
Composition[sync](#sync-and-async), [async](#sync-and-async)
Data/logic[http](#http), [ask](#ask), [jsUpdateState](#jsupdatestate), [invokeTool](#invoketool), [custom](#custom), [validate](#validate)
Effects[dialog](#dialog), [toast](#toast), [navigate](#navigate), [openScreen](#openscreen), [popScreen](#popscreen), [dismiss](#dismiss), [openUrl](#openurl), [copyToClipboard](#copytoclipboard), [share](#share), [analytics](#analytics)

Context paths

Action values resolve recursively:

PrefixAvailable in
$root., $local., $parent., $id.<id>.Action fields that accept resolved values
$theme.Any compatible value
$event. / $eventAction caused by a control change or event
$response. / $responseHTTP success/error apply and nested actions
$error. / $erroronError and error observers

See state and paths. patch path / from use patch paths ($id.offer-42/state/liked), not dotted state paths ($id.offer-42.liked).

Error handling

An action finishes, is skipped, or fails. Exceptions are caught by the runtime and never escape into composition or the SSE collector. A failed mutation is atomic: no partial state/tree commit.

What fails

ActionFailure
patchMissing/duplicate id, invalid pointer, failed test, merge into a non-object, $local/$parent without a current widget
httpNon-2xx, timeout, connection error, required JSON cannot be parsed, or jsMap fails
jsUpdateStateDisabled, syntax/runtime error, timeout/limit, invalid return
customHandler throws or returns Fail
invokeToolUnknown Tool, invalid input, Tool executor throws, or mapper JS/native failure
Navigation/platform effectsHost/platform rejects operation, or openScreen/popScreen without a BduiScreenHost
dialogMissing content, unknown presentation, missing tooltip anchorId, missing anchor widget
toast, analyticsBest effort; malformed values are dropped/logged rather than failed

Failure pipeline

  1. Build and log BduiActionError.
  2. For HTTP, apply optional error response spec.
  3. Run optional per-action onError.
  4. Run matching $error observers.
  5. If no handler ran and source is ui, show the default error toast.
  6. Observer/SSE errors with no handler are log-only.
{
  "type": "copyToClipboard",
  "value": "$root.shareCode",
  "onError": {
    "type": "toast",
    "message": "Could not copy the code",
    "style": "error"
  }
}

$error:

FieldTypeMeaning
actionTypestringFailed action type
codestringStable category (path, patch, http, timeout, js, custom, navigate, platform, …)
messagestringSafe user-facing summary
httpStatusinteger/nullHTTP status when available
sourcestringui, observer, or sse
sourceWidgetIdstring/nullDispatching widget

An error handler's own failure is logged and does not re-enter handlers or error observers.


State and tree actions

patch is the only store-mutation action (RFC 6902 plus BDUI node roots and merge). Theme overrides are merge / replace on $root/theme.

patch

Applies an atomic operation list to the widget document and/or node state.

FieldTypeDefaultMeaning
targetIdstringrootBase node for paths that start with / or ""
patcharray/pathrequired outside HTTPOperation list

patch is a spec-2 action.

Patch paths

A path is either a JSON Pointer (RFC 6901) or a BDUI node root plus a pointer:

PathTarget
/children/-RFC 6902 from targetId (default: root widget)
""The targetId node itself
$id.offer-42The widget node with that id
$id.offer-42/state/likedJSON Pointer /state/liked on that node
$root/state/statusRoot widget, then /state/status
$local/state/likedCurrent widget (widget-declared actions only)
$parent/state/titleNearest ancestor widget (widget-declared actions only)

$id.<id> runs until the first /. Ids may contain dots (offer-42); they must not contain /. Duplicate ids in the document fail the patch.

$root, $local, $parent, and $id.x resolve to a widget node, not to its state. State is always /state/.... Binding $id.offer-42.liked is the same value as patch path $id.offer-42/state/liked.

$local and $parent are invalid in SSE and HTTP patches (no current widget). Use $root or $id.*.

targetId applies only to paths that start with / or "". $ paths are absolute.

value and from values resolve like other action JSON ($event, $response, dotted state paths). Do not put a dotted state path in path.

Operations

opFieldsMeaning
addpath, valueInsert a value. Existing object members are replaced. Array indexes insert and shift
removepathDelete an existing value
replacepath, valueOverwrite an existing value. The target must already exist
movefrom, pathRemove at from, then add at path. Array indexes in path are evaluated after the removal
copyfrom, pathDuplicate the value at from onto path
testpath, valueSucceed only when the current value equals value (JSON equality, including type)
mergepath, valueDeep-merge value into the object at path

merge: arrays and scalars replace; object keys recurse; null is a value, not deletion (not RFC 7386). If path is missing and the parent is an object, merge creates that object. If path already exists, it must be an object.

add on an existing object member still replaces (RFC 6902), so it upserts a key when the parent object exists.

Unrecognized operation members are ignored. Unknown op values, missing required fields, a pointer whose parent does not exist, a missing remove/test/from target, a move into its own descendant, a failed test, $local/$parent without a current widget, and duplicate ids fail the action. replace on an object member creates a missing key (and a missing object parent). It still cannot target a missing array index. All operations validate before commit: one invalid operation leaves the complete tree unchanged.

Prefer $id.* plus a short pointer over a long numeric path from root. Sibling indexes shift after add, remove, and move. Copied widgets keep their id; replace it in the same patch when identity must stay unique. Replacing $id.slot-gallery (empty pointer after the id) replaces that node in its parent and should keep the same id.

Mixed state and tree, one commit:

{
  "type": "patch",
  "targetId": "slot-reviews",
  "patch": [
    {
      "op": "add",
      "path": "/children/-",
      "value": { "id": "review-1", "type": "Text", "params": { "text": "Great location" } }
    },
    { "op": "replace", "path": "$id.offer-42/state/liked", "value": true },
    { "op": "merge", "path": "$root/state", "value": { "loading": false } }
  ]
}

add — object member (creates state when the node had none):

{
  "type": "patch",
  "patch": [
    { "op": "add", "path": "$id.offer-42/state", "value": { "liked": false, "likes": 0 } }
  ]
}

add — nested member (parent object must already exist):

{
  "type": "patch",
  "patch": [
    { "op": "add", "path": "$id.offer-42/state/badge", "value": "new" }
  ]
}

add — insert a child at index 0:

{
  "type": "patch",
  "patch": [
    {
      "op": "add",
      "path": "$id.slot-reviews/children/0",
      "value": { "id": "review-banner", "type": "Text", "params": { "text": "Latest reviews" } }
    }
  ]
}

add — append a child (/-, or targetId plus /children/-):

{
  "type": "patch",
  "patch": [
    {
      "op": "add",
      "path": "$id.slot-reviews/children/-",
      "value": { "id": "review-1", "type": "Text", "params": { "text": "Great location" } }
    }
  ]
}

add — existing object member is replaced:

{
  "type": "patch",
  "patch": [
    { "op": "add", "path": "$root/params/enabled", "value": false }
  ]
}

remove — object member:

{
  "type": "patch",
  "patch": [
    { "op": "remove", "path": "$root/state/loading" }
  ]
}

remove — array element (later siblings shift down):

{
  "type": "patch",
  "patch": [
    { "op": "remove", "path": "$id.slot-reviews/children/1" }
  ]
}

replace — existing value (object keys are created when missing; array indexes must already exist):

{
  "type": "patch",
  "patch": [
    { "op": "replace", "path": "$root/state/status", "value": "ready" }
  ]
}

replace — whole child widget:

{
  "type": "patch",
  "patch": [
    {
      "op": "replace",
      "path": "$id.slot-gallery/children/0",
      "value": {
        "id": "hero",
        "type": "Image",
        "params": {
          "url": "https://cdn.example.com/hero.jpg",
          "contentDescription": "Offer"
        }
      }
    }
  ]
}

replace — named node in its parent:

{
  "type": "patch",
  "patch": [
    {
      "op": "replace",
      "path": "$id.slot-gallery",
      "value": {
        "id": "slot-gallery",
        "type": "Image",
        "params": {
          "url": "https://cdn.example.com/hero.jpg",
          "contentDescription": "Offer"
        }
      }
    }
  ]
}

move — object member:

{
  "type": "patch",
  "patch": [
    { "op": "move", "from": "$root/state/draftTitle", "path": "$root/state/title" }
  ]
}

move — array element. After from is removed, path uses the shifted indexes:

{
  "type": "patch",
  "patch": [
    { "op": "move", "from": "$root/children/2", "path": "$root/children/0" }
  ]
}

move — across nodes (no numeric path from root):

{
  "type": "patch",
  "patch": [
    { "op": "move", "from": "$id.review-0", "path": "$id.slot-footer/children/-" }
  ]
}

copy — duplicate a child, then give the copy a unique id:

{
  "type": "patch",
  "patch": [
    { "op": "copy", "from": "$id.slot-reviews/children/0", "path": "$id.slot-reviews/children/-" },
    { "op": "replace", "path": "$id.slot-reviews/children/1/id", "value": "review-1-copy" }
  ]
}

test — equality guard, then mutate. A mismatch or type mismatch ("3" vs 3) fails the complete patch:

{
  "type": "patch",
  "patch": [
    { "op": "test", "path": "$root/state/version", "value": 3 },
    { "op": "replace", "path": "$root/state/status", "value": "ready" }
  ]
}

merge — into a state object:

{
  "type": "patch",
  "patch": [
    { "op": "merge", "path": "$id.offer-42/state", "value": { "liked": true, "likes": 11 } }
  ]
}

merge — into a widget node:

{
  "type": "patch",
  "patch": [
    { "op": "merge", "path": "$id.offer-42", "value": { "params": { "enabled": false } } }
  ]
}

merge / replace — root theme (backend layer; BduiThemePolicy still filters). Rejected tokens are logged and dropped; the patch still succeeds. Missing theme is created by merge:

{
  "type": "patch",
  "patch": [
    {
      "op": "merge",
      "path": "$root/theme",
      "value": {
        "colors": {
          "primary": { "light": "#0057D9", "dark": "#8AB4FF" }
        }
      }
    }
  ]
}

Write from the control event:

{
  "type": "patch",
  "patch": [
    { "op": "add", "path": "$root/state/form/agreed", "value": "$event" }
  ]
}

Several operations in one patch; later operations see earlier ones:

{
  "type": "patch",
  "patch": [
    { "op": "test", "path": "$root/state/status", "value": "loading" },
    { "op": "replace", "path": "$root/state/status", "value": "ready" },
    {
      "op": "add",
      "path": "$id.slot-reviews/children/-",
      "value": { "type": "Text", "params": { "text": "Loaded" } }
    },
    { "op": "remove", "path": "$root/state/loading" }
  ]
}

JSON Pointer escaping after the node root: ~ encodes as ~0, / encodes as ~1.

{
  "type": "patch",
  "patch": [
    { "op": "add", "path": "$root/state/a~1b", "value": true },
    { "op": "replace", "path": "$root/state/c~0d", "value": 1 }
  ]
}

That writes state["a/b"] = true and state["c~d"] = 1.

These fail (nothing commits):

{ "type": "patch", "patch": [{ "op": "add", "path": "$root/state/missing/child", "value": 1 }] }
{ "type": "patch", "patch": [{ "op": "replace", "path": "$root/state/absent", "value": true }] }
{ "type": "patch", "patch": [{ "op": "test", "path": "$root/state/count", "value": "0" }] }

The last example fails when count is the number 0: test compares JSON values, not coerced strings.


sync and async

Both contain:

FieldTypeDefaultMeaning
actionsaction[]requiredChild actions

sync

Runs children strictly in order. Each child—including HTTP, custom handlers, and nested groups—must finish before the next starts. Later actions see earlier commits.

{
  "type": "sync",
  "actions": [
    { "type": "patch", "patch": [{ "op": "add", "path": "$root/state/loading", "value": true }] },
    {
      "type": "http",
      "method": "POST",
      "url": "/v1/form",
      "body": { "fromState": "$root.form" },
      "success": { "apply": "none" },
      "onError": {
        "type": "patch",
        "patch": [{ "op": "add", "path": "$root/state/loading", "value": false }]
      }
    },
    { "type": "patch", "patch": [{ "op": "add", "path": "$root/state/loading", "value": false }] }
  ]
}

First failed child stops remaining children after error handling. Compatibility skips do not stop the group. State observers run once at group completion/failure.

async

Starts every child without waiting for siblings. Individual commits remain serialized.

{
  "type": "async",
  "actions": [
    { "type": "patch", "patch": [{ "op": "add", "path": "$id.offer-42/state/liked", "value": true }] },
    {
      "type": "http",
      "method": "POST",
      "url": "/v1/likes",
      "body": { "fromState": "$id.offer-42" },
      "success": { "apply": "none" },
      "onError": {
        "type": "patch",
        "patch": [{ "op": "add", "path": "$id.offer-42/state/liked", "value": false }]
      }
    },
    { "type": "analytics", "event": "offer_like_started" }
  ]
}

One child failure does not cancel siblings or fail the group. Each child runs its own error handling and observer cycle.

Nested async inside sync is awaited as a group: all parallel children finish before the next sync sibling. A nested sync inside async is one ordered job among the parallel jobs.


Data and logic actions

http

Makes a request through the host's Ktor HttpClient.

FieldTypeDefaultMeaning
methodstringGETGET, POST, PUT, PATCH, DELETE
urlstring/pathrequiredAbsolute or host-relative URL. A path that resolves to an empty string skips the action, so a call can depend on state
headersobject{}Recursively resolved strings; never embed auth secrets in JSON
queryobject{}Recursively resolved query values
bodyJSON/body specabsentLiteral JSON, fromState, or fromEvent
successapply spec{ "apply": "none" }Handling for 2xx
errorapply specabsentOptional handling of non-2xx response body
onErroractionabsentRecovery/reporting after optional error apply
timeoutMsintegerclient defaultMay only tighten host policy

Body from state:

{
  "type": "http",
  "method": "POST",
  "url": "/v1/favorites",
  "headers": { "Content-Type": "application/json" },
  "query": { "source": "$root.analytics.source" },
  "body": { "fromState": "$root.offer" },
  "success": {
    "apply": "patch",
    "from": "$response.ops",
    "action": { "type": "toast", "message": "Saved", "style": "success" }
  },
  "error": {
    "apply": "patch",
    "from": "$response.ops"
  },
  "onError": {
    "type": "toast",
    "message": "$error.message",
    "style": "error"
  }
}

Body from control event:

{
  "type": "http",
  "method": "PUT",
  "url": "/v1/settings/notify",
  "body": { "fromEvent": true },
  "success": { "apply": "none" }
}

Apply spec:

FieldTypeDefaultMeaning
applystringnonenone or patch
fromresponse path$responsePayload within body, taken before mapping
jsMapstringabsentSandboxed mapResponse that reshapes the payload before apply
targetIdstringrootDefault targetId for / paths in the ops
actionactionabsentRuns after successful apply. $response is still the original body

HTTP status outside 200–299 is failure even if the body is valid. The error apply runs first, then onError, then general error observers. A failed jsMap is an action failure (js / timeout / js_limit / js_disabled), not a successful apply of the raw payload.

apply: "patch" takes an operation array (the from slice).

{
  "type": "http",
  "method": "GET",
  "url": "/v1/offers/42/patch",
  "success": {
    "apply": "patch",
    "from": "$response.ops"
  }
}

Mapping a response with JavaScript

Use jsMap when the HTTP body is not already in widget-state shape. It runs after from extraction and before apply.

{
  "type": "http",
  "method": "GET",
  "url": "/v1/profile",
  "success": {
    "apply": "patch",
    "from": "$response.data",
    "jsMap": "function mapResponse(payloadJson, currentJson) { var p = JSON.parse(payloadJson); var root = JSON.parse(currentJson); var prev = root.state && root.state.profile ? root.state.profile : {}; return JSON.stringify([{ op: 'merge', path: '$root/state/profile', value: { name: (p.firstName + ' ' + p.lastName).trim(), avatar: p.photo && p.photo.url, likes: typeof p.likes === 'number' ? p.likes : prev.likes } }]); }"
  }
}

The script must declare one synchronous function:

function mapResponse(payloadJson, currentJson) { return JSON.stringify(mapped); }
ArgumentJSON string of
payloadJsonThe from slice of the HTTP body (default: whole $response)
currentJsonThe root widget document. Missing root is a path failure

The returned JSON is what apply consumes:

applymapResponse must return
patchoperation array
noneignored; do not use jsMap

jsMap uses the same isolate, policy, limits, and commit gate as [jsUpdateState](#jsupdatestate). Kotlin parses the return string; functions, host objects, and promises never enter the store.

Typical uses: flatten nested DTO fields, pick/rename keys, coerce types, keep a local field the server omitted (likes in the example). Do not call HTTP, navigate, or mutate anything except the returned JSON.

If jsPolicy.enabled is false, an apply spec that includes jsMap fails with js_disabled and does not fall back to the unmapped payload. Gate jsMap on X-Bdui-Spec so older clients receive an already-shaped body instead.

ask

Asks AppSana to fill a few state paths the widget declares, from a small piece of state the widget names. AppSana runs one short model call that sees only the instruction, the user's words and the context, and may only set the paths in write. Use it when a value depends on something the widget just loaded: pick the doctor a user named from the list that arrived, or the day they meant from the open days.

FieldTypeDefaultMeaning
querystring/pathrequiredThe user's own words. An empty value skips the action, so a flow can make the call depend on state
instructionstring/path""What the widget's author wants done, for example which of the listed items to choose
contextobject{}Recursively resolved JSON the model may look at (at most askPolicy.maxContextBytes)
writestring[]requiredPatch pointers the answer may set, for example $id.root/state/booking/doctor (written as pointers, not resolved)
ifEmptybooleanfalseOnly write where the target is still missing, null or blank, so a choice the user made is never overridden
freebooleanfalseLet AppSana write a value that does not appear in context (by default it must be copied from it)
timeoutMsintegerpolicyMay only tighten askPolicy.timeoutMs
onErroractionabsentRuns on a failure, timeout or when ask is turned off
{
  "type": "sync",
  "actions": [
    {
      "type": "ask",
      "instruction": "Choose the doctor the user means from the list.",
      "query": "$id.root.doctorQuery",
      "context": { "doctors": "$id.root.doctors" },
      "write": ["$id.root/state/booking/doctor"],
      "ifEmpty": true,
      "onError": { "type": "patch", "patch": [{ "op": "add", "path": "$id.root/state/loading", "value": false }] }
    },
    { "type": "patch", "patch": [{ "op": "add", "path": "$id.root/state/loading", "value": false }] }
  ]
}
  • Only declared paths change. The SDK applies an add or replace of a path in write, and nothing else AppSana sends. A string AppSana writes is data, never a binding (a leading $ is escaped). With no clear answer nothing is written and the action succeeds, so the next action runs.
  • Failures. A server error, a timeout, or a missing or disabled policy fails the action: its own onError runs and a surrounding sync stops, so give the ask an onError that finishes whatever state the widget keeps (a progress indicator).
  • Off by default. BduiConfig.askPolicy = BduiAskPolicy(enabled = true) turns it on. The context leaves the device, so enable it only where users have agreed to that. It needs BduiConfig.appsana.
  • Older SDKs skip an action they do not know, so the next action still runs and the widget shows what it had.

jsUpdateState

Runs a synchronous pure state transform in a sandboxed JavaScript isolate.

FieldTypeDefaultMeaning
pathstate-object path$rootObject passed to and replaced by the script
jsCodestringrequiredOne updateState(stateJson) function declaration
onErroractionabsentHandles disabled/syntax/runtime/timeout/output failure
{
  "type": "jsUpdateState",
  "path": "$root",
  "jsCode": "function updateState(stateJson) { var s = JSON.parse(stateJson); s.canSubmit = !!(s.form && s.form.email && s.form.agreed); return JSON.stringify(s); }",
  "onError": {
    "type": "toast",
    "message": "Could not validate the form",
    "style": "error"
  }
}

Contract (shared with HTTP [jsMap](#mapping-a-response-with-javascript) and observer jsPredicate):

  • one JSON string in, one JSON string out;
  • return the complete replacement object;
  • promises are rejected, never awaited;
  • no top-level side effects;
  • no host bindings, store, HTTP, navigation, DOM, file, clipboard, sockets, timers, or dynamic imports;
  • fresh isolate for every call;
  • Kotlin JSON parsing is the commit gate.

Default/hard limits:

LimitDefaultHard cap
Wall time50 ms250 ms
Source8 KiB32 KiB
Input256 KiB1 MiB
Output256 KiB1 MiB

The watchdog terminates the engine on timeout. Run off the main thread. Web uses a dedicated Worker or QuickJS-Wasm—never page eval.

jsPolicy = BduiJsPolicy(
    enabled = true,
    timeoutMs = 50,
    maxSourceBytes = 8_192,
    maxPayloadBytes = 256 * 1024,
)

enabled = false fails with js_disabled. The isolate is a code sandbox, not a confidentiality boundary; do not expose secrets solely for script consumption.

validate

Checks declarative rules against current state, writes the problems to errorsPath, then runs onValid or onInvalid. Spec 8. The rules are data: nothing supplied by a model is executed, and a regular expression is capped at 200 characters.

FieldTypeDefaultMeaning
rulesarrayrequiredSee below. Missing rules fails the action
errorsPathstring$root/state/errorsPatch path that receives { "<key>": "<message>" }
onValidactionabsentRuns only when every rule passes. The form is never submitted by an invalid run
onInvalidactionabsentRuns when at least one rule fails

A rule has path (a binding such as $root.form.email), an optional key (the field name in the errors object, default the last path segment), and any of: required, minLength, maxLength, min, max, format (email, phone, number, date), pattern, notBefore (an ISO date or a binding to one). message is used for the first failing check; messages overrides per check (required, minLength, format, notBefore, ...). An empty optional field skips its format checks. The first failing rule per key wins. The errors object is replaced on every run, so fixed fields clear.

Show an error by binding it to the field: TextField "error": "$root.errors.email" (non-empty puts the field in error and shows the message).

{
  "type": "validate",
  "rules": [
    { "path": "$root.form.title", "key": "title", "required": true, "minLength": 5, "message": "Give the bug a short title" },
    { "path": "$root.form.email", "key": "email", "format": "email" },
    { "path": "$root.search.ret", "key": "ret", "required": true, "notBefore": "$root.search.depart" }
  ],
  "onValid": { "type": "custom", "name": "app:submit", "args": { "title": "$root.form.title" } }
}

custom

Calls an app-registered, namespaced action handler.

FieldTypeDefaultMeaning
namestringrequiredRegistered namespace, e.g. acme:favoriteOffer
argsobject{}Recursively resolved JSON arguments
{
  "type": "custom",
  "name": "acme:favoriteOffer",
  "args": {
    "offerId": "$root.offer.id",
    "source": "card"
  },
  "onError": {
    "type": "toast",
    "message": "$error.message",
    "style": "error"
  }
}
sealed interface BduiCustomActionResult {
    data object Done : BduiCustomActionResult
    data class Dispatch(val action: BduiAction) : BduiCustomActionResult
    data class Fail(
        val message: String,
        val code: String = "custom",
    ) : BduiCustomActionResult
}

Dispatch re-enters the same action runtime. Handlers do not receive BduiStore. An unknown name is a compatibility skip; a registered handler throwing/returning Fail is an error.

invokeTool

Runs an application Tool registered on BduiConfig.tools, then maps the structured result through BduiConfig.mappers into a patch applied by the existing patch engine. UI onClick, observers, and SSE action events all enter BduiRuntime.dispatch — there is no separate Tool invocation path.

FieldTypeDefaultMeaning
toolstringrequiredRegistered Tool name
argumentsobject{}Recursively resolved JSON arguments
{
  "type": "invokeTool",
  "tool": "get_account_balance",
  "arguments": {
    "accountId": "primary"
  }
}

The Tool itself returns a business result, never patches. If the Tool has mapper source, a mapper converts that result into RFC 6902 ops ((result, context) => Patch[]). Native mappers register with mappers.register; JavaScript mapper source delivered by AppSana registers with mappers.registerJs and runs through the existing BduiJsRuntime (requires jsPolicy.enabled). If mapper source is empty, the SDK emits tool.mapper_missing and asks AppSana to compose widgets from the widget library (built-ins and composite recipes) using the real Tool result. Tools registered with sensitive = true skip that compose step so the result never leaves the device — register a client mapper instead.

Unknown Tool names fail (tool). Invalid input fails (tool_input). Mapper exceptions fail (tool_mapper). Lifecycle events: tool.started, tool.completed, tool.failed, plus mapper events on BduiConfig.toolListener.


Effect actions

Effects do not mutate state and are consumed once. They are not restored after process death.

dialog

Shows an overlay whose content is any widget document. Effects do not enter the view checkpoint; the overlay is not restored after process death.

FieldTypeDefaultMeaning
presentationstringalertalert, bottomSheet, or tooltip
contentwidgetrequiredWidget tree rendered in the overlay
dimColorcolor/pathsee belowScrim behind the overlay (#RRGGBB / #AARRGGBB or $theme.colors.*)
cancelablebooleantrueOutside tap, back, and Esc close the overlay (tooltip: tap outside)
gravitystringCenteralert only. Same values as Box contentAlignment
anchorIdstringrequired tooltiptooltip only. Widget id in this view to attach to. BDUI chooses caret/edge placement

Default dimColor is #80000000 for alert and bottomSheet, and transparent (#00000000) for tooltip.

content is a normal widget envelope (type, modifier, onClick, params, children, optional id / state / observers). $root and $id.* resolve against this view's document. $local / $parent walk the overlay tree. Overlay widget ids must not collide with ids in this view.

Unknown or too-new widgets in content skip as in this view. Missing content, an unknown presentation, a tooltip without anchorId, or an anchorId that is not in this view fails the action. Invalid dimColor logs and uses the default. gravity is ignored for bottomSheet and tooltip. anchorId is ignored for alert and bottomSheet.

Close the overlay with dismiss from a control in content (or cancelable chrome).

alert — centered (or gravity) modal:

{
  "type": "dialog",
  "presentation": "alert",
  "gravity": "Center",
  "dimColor": "#80000000",
  "cancelable": true,
  "content": {
    "type": "Card",
    "params": { "variant": "elevated" },
    "children": [
      { "type": "Text", "params": { "text": "Delete offer?", "variant": "titleMedium" } },
      { "type": "Text", "params": { "text": "This cannot be undone." } },
      {
        "type": "Row",
        "params": { "horizontalArrangement": "End" },
        "children": [
          {
            "type": "Button",
            "onClick": { "type": "dismiss" },
            "children": [{ "type": "Text", "params": { "text": "Cancel" } }]
          },
          {
            "type": "Button",
            "onClick": {
              "type": "sync",
              "actions": [
                {
                  "type": "http",
                  "method": "DELETE",
                  "url": "/v1/offers/42",
                  "success": { "apply": "none" }
                },
                { "type": "dismiss" }
              ]
            },
            "children": [{ "type": "Text", "params": { "text": "Delete" } }]
          }
        ]
      }
    ]
  }
}

bottomSheet — sheet from the bottom edge; gravity is ignored:

{
  "type": "dialog",
  "presentation": "bottomSheet",
  "dimColor": "$theme.colors.outline",
  "content": {
    "type": "Column",
    "modifier": [
      { "name": "fillMaxWidth" },
      { "name": "paddingHorizontal", "value": "16" }
    ],
    "children": [
      { "type": "Text", "params": { "text": "Share offer", "variant": "titleMedium" } },
      {
        "type": "Button",
        "onClick": {
          "type": "sync",
          "actions": [
            { "type": "share", "text": "$root.shareUrl" },
            { "type": "dismiss" }
          ]
        },
        "children": [{ "type": "Text", "params": { "text": "Share link" } }]
      }
    ]
  }
}

tooltip — attach to a widget in this view. BDUI measures that node and points the tooltip at it (anchorId required):

{
  "type": "dialog",
  "presentation": "tooltip",
  "anchorId": "price-info",
  "dimColor": "#00000000",
  "content": {
    "type": "Text",
    "params": { "text": "Price includes utilities", "variant": "bodySmall" }
  }
}

The anchored widget stays in this view's tree:

{
  "type": "Row",
  "children": [
    { "type": "Text", "params": { "text": "$root.price" } },
    {
      "id": "price-info",
      "type": "Button",
      "onClick": {
        "type": "dialog",
        "presentation": "tooltip",
        "anchorId": "price-info",
        "content": {
          "type": "Text",
          "params": { "text": "Price includes utilities" }
        }
      },
      "children": [{ "type": "Text", "params": { "text": "?" } }]
    }
  ]
}

dismiss

Closes the foremost dialog overlay. If none is shown and this view is a pushed BDUI screen, pops that screen with a null result (same as system back). Otherwise the host may apply its own back/pop behavior.

openScreen

Pushes a full-screen BDUI destination onto BduiScreenHost. The new screen is not an embedded BduiView: it covers the app window, has its own store/runtime, and typically uses a Scaffold root.

FieldTypeDefaultMeaning
initialJsonwidget/stringabsentSnapshot for the new screen (inline widget tree, JSON string, or path)
streamUrlstring/pathabsentSSE URL, same contract as BduiView (sseUrl is accepted as an alias)
onResultactionabsentRuns on the caller after the screen pops. $event is the result

At least one of initialJson or streamUrl is required. Missing both, or calling this outside BduiScreenHost, fails (screen). An inline widget tree is not resolved against the caller; $root in that tree belongs to the new screen. A string path such as "$root.detailJson" is resolved on the caller and then used as the snapshot.

openScreen does not wait for the destination to close. Later sync siblings run immediately.

{
  "type": "openScreen",
  "initialJson": {
    "id": "root",
    "type": "Scaffold",
    "state": { "title": "Choose a city" },
    "topBar": {
      "type": "TopAppBar",
      "params": { "title": "$root.title" }
    },
    "children": [
      {
        "type": "Button",
        "onClick": {
          "type": "popScreen",
          "result": { "title": "Almaty" }
        },
        "children": [{ "type": "Text", "params": { "text": "Almaty" } }]
      }
    ]
  },
  "streamUrl": null,
  "onResult": {
    "type": "patch",
    "patch": [
      { "op": "add", "path": "$root/state/picked", "value": "$event.title" }
    ]
  }
}

onResult is optional. System back and cancel pass a null $event.

popScreen

Closes the current pushed BDUI screen and delivers result to the caller's onResult as $event.

FieldTypeDefaultMeaning
resultJSONabsentValue exposed to the caller as $event

Fails (screen) when this view is not a pushed screen or there is no BduiScreenHost.

{
  "type": "popScreen",
  "result": { "title": "Almaty", "id": "ala" }
}

analytics

Reports a named event. Best effort: it never fails and never blocks the surrounding action. Use it for events specific to your product. Taps, screen opens and the start and end of flows are already reported by the SDK (see below), so do not repeat those. Place it next to the real action in a sync.

FieldTypeDefaultMeaning
eventstringrequiredEvent name; blank names are ignored
paramsobjectabsentResolved like any action value ($root., $event.)
{
  "type": "sync",
  "actions": [
    { "type": "analytics", "event": "offer_opened", "params": { "offer": "$root.offerId", "source": "banner" } },
    { "type": "openScreen", "screen": "offer" }
  ]
}

The action hands the event to BduiConfig.analytics (a BduiAnalytics) when the runtime has opted in (see below); otherwise it is only logged. BduiAppsanaAnalytics is the stock sink. It batches events and posts them to the AppSana /api/events endpoint with the application's credentials:

config.analytics = BduiAppsanaAnalytics(config, scope)

Each event is sent with the id of the AppSana response currently on screen (X-Appsana-Request, delivered through BduiAnalytics.onResponse) and the id of the screen that raised it, so AppSana can join a tap to the answer that produced it. Every event is stamped with the phone's own clock (ts, epoch milliseconds, taken when the event happens rather than when the batch is sent) and its UTC offset (tzOffset, minutes, e.g. 300 for UTC+5), so AppSana can show when people act in their local time. Set config.endUserToken = { currentUserRef } so events and requests are tied to a customer; it is sent as X-Appsana-User and must be an opaque reference, never a name, phone or account number.

Events the SDK raises itself

Reporting is opt-in per surface. A runtime reports only when it is given an analyticsScreen name; every other BduiView stays silent, so your own native-style screens (home, settings, checkout) are never tracked. Name only the AppSana-generated surface:

val runtime = BduiRuntime(config, store, darkTheme, scope, analyticsScreen = "personal_feed")
BduiView(initialJson = json, store = store, runtime = runtime)
// or, when BduiView creates the runtime itself:
BduiView(initialJson = json, store = store, analyticsScreen = "personal_feed")

For a named surface the runtime reports these without any JSON. They are raised by the runtime, so they never change how an action runs or fails. The name is sent as the event's screen.

EventRaised whenparams
screen_openedThe surface first shows contentroot
widget_clickedThe user taps a widget's onClickwidget, widgetType, action, personalized
action_startedThat tap runs an action that does work (http, invokeTool, custom, or a sync/async containing one)same as widget_clicked
action_finished…and it succeededsame
action_abandoned…and it failedsame

personalized is always true: only an opted-in surface reports, and that surface shows AppSana-generated UI. The analytics action follows the same rule: on a runtime without analyticsScreen it does nothing. Host code reports anything else through the same sink, for example the text field or microphone: config.analytics?.log("input_started", buildJsonObject { put("mode", "text") }, "personal_feed"). Use mode text or voice.

Event names are lowercase snake_case (a-z 0-9 _ . : -, at most 64 characters). Keep params small (under 2 KB) and free of personal data; AppSana scrubs card numbers, emails and national ids but cannot recognise everything.

← Widgets · Documentation · Next: Themes →

AppSana — экраны под задачу в приложениях, которые у вас уже есть.