Skip to content

UIAP SDK API

FieldValue
StatusDraft
Version0.1
Date2026-03-27
Dependencies[UIAP-CORE], [UIAP-WEB]
EditorsPatrick

UIAP SDK API v0.1 defines the integration and runtime API for Web applications that:

  • publish a PageGraph,
  • register Actions,
  • evaluate Policy locally,
  • emit Signals,
  • operate Frame Bridges,
  • optionally render presentation features such as Overlay/Ghost Cursor.

The SDK is the first-party layer within the app. This is exactly where control belongs, because CSP can restrict external scripts and the Same-Origin Policy limits direct DOM access across origins. For cross-origin communication, postMessage() is the appropriate bridge mechanism. ([MDN Web Docs][3])

The key words MUST, MUST NOT, SHOULD, MAY in this document are to be interpreted as described in RFC 2119 and BCP 14, when and only when they appear in ALL CAPS.

package "@uiap/web-sdk"
package "@uiap/web-sdk/overlay"
package "@uiap/web-sdk/frame-bridge"
  1. ESM / npm / first-party bundle Recommended for production apps.

  2. CDN script Only if script-src permits it.

  3. Frame bridge For cooperating child/parent contexts via postMessage().

  4. External driver adapter Optional for out-of-process automation via WebDriver or WebDriver BiDi. ([MDN Web Docs][3])


interface UIAPConfig {
app: {
id: string;
version: string;
profile: "[email protected]";
locale?: string;
};
transport: UIAPTransport;
observe?: ObserveConfig;
policy?: SDKPolicyConfig | false;
overlay?: OverlayConfig | false;
frames?: FrameConfig | false;
annotations?: AnnotationConfig;
debug?: boolean | "info" | "warn" | "error";
}
interface UIAPClient {
start(): Promise<void>;
stop(): Promise<void>;
destroy(): Promise<void>;
getSnapshot(input?: SnapshotOptions): Promise<PageGraph>;
publishSnapshot(input?: SnapshotOptions): Promise<PageGraph>;
emitSignal(signal: WebSignal): void;
registerAction(descriptor: ActionDescriptor, handler: ActionHandler): Unsubscribe;
unregisterAction(actionId: ActionId): void;
registerElementAdapter(adapter: UIAPElementAdapter): Unsubscribe;
registerPolicyEvaluator(evaluator: SDKPolicyEvaluator): Unsubscribe;
bindElement(node: Element, binding: ElementBinding): Unsubscribe;
bindScope(node: Element, binding: ScopeBinding): Unsubscribe;
setRouteProvider(provider: RouteProvider): void;
createFrameBridge(options: FrameBridgeOptions): FrameBridge;
on<T = unknown>(event: SDKEventName, listener: (payload: T) => void): Unsubscribe;
overlay?: OverlayController;
}
type Unsubscribe = () => void;

interface UIAPTransport {
send(message: UIAPEnvelope): Promise<void> | void;
close?(): Promise<void> | void;
onMessage(listener: (message: UIAPEnvelope) => void): Unsubscribe;
onError?(listener: (error: unknown) => void): Unsubscribe;
}
  • The SDK MUST remain transport-agnostic.
  • A Transport MUST deliver messages in UIAP Core envelopes.
  • The SDK MAY adapt multiple transports as long as exactly one per active session is write-primary.

The SDK SHOULD derive roles, names, descriptions, and states from Web semantics rather than mere pixel magic. Browsers produce an Accessibility Tree with name, role, description, and state; aria-label and aria-labelledby are central sources for Accessible Names. For runtime changes, MutationObserver is the native tool. ([MDN Web Docs][4])

interface ObserveConfig {
root?: Document | ShadowRoot;
includeHidden?: boolean; // default false
includeNonInteractive?: boolean; // default false
throttleMs?: number; // default 100
maxNodes?: number; // default implementation-defined
signals?: WebSignalKind[]; // default supported set
}
interface SnapshotOptions {
scopes?: ScopeId[];
documents?: DocumentId[];
includeHidden?: boolean;
includeNonInteractive?: boolean;
}
  1. The SDK MUST be able to produce at least one snapshot.
  2. The SDK SHOULD derive deltas from DOM/state changes.
  3. The SDK MUST be able to update bounding boxes, focus, and visible interaction states.
  4. The SDK SHOULD detect route, dialog, toast, and validation signals or accept them from the app.

interface ElementBinding {
id: StableId;
scopeId?: ScopeId;
meaning?: string;
name?: string;
defaultAction?: ActionId;
risk?: RiskLevel;
sensitive?: boolean;
success?: SuccessSignal[];
metadata?: Record<string, unknown>;
}
interface ScopeBinding {
id: ScopeId;
kind: ScopeKind;
parentScopeId?: ScopeId;
name?: string;
metadata?: Record<string, unknown>;
}
  • bindElement() and bindScope() SHOULD mirror the corresponding data-uiap-* metadata or register them internally.
  • Explicit bindings MUST take precedence over heuristics.
  • An SDK MAY maintain bindings both via DOM attributes and via an in-memory registry.
data-uiap-id="video.submit"
data-uiap-scope="video.create.form"
data-uiap-meaning="use_case"
data-uiap-action="video.create"
data-uiap-risk="confirm"
data-uiap-sensitive="true"
data-uiap-ignore="true"

interface ActionHandlerContext {
actionHandle: string;
action: ActionDescriptor;
target?: ResolvedTarget;
args: Record<string, unknown>;
snapshot: PageGraph;
policy?: PolicyDecision;
emitSignal(signal: WebSignal): void;
requestConfirmation(input: ActionConfirmationRequestPayload): Promise<"granted" | "denied">;
waitForUser(note: string): Promise<void>;
overlay?: OverlayController;
}
type ActionHandlerResult =
| {
status: "succeeded";
returnValue?: Record<string, unknown>;
sideEffectState?: "none" | "applied" | "unknown";
verification?: VerificationOutcome;
}
| {
status: "failed";
error: RuntimeErrorDescriptor;
sideEffectState?: "none" | "applied" | "unknown";
};
type ActionHandler =
(ctx: ActionHandlerContext) => Promise<ActionHandlerResult>;
  1. registerAction() MUST support domain actions and primitive actions.
  2. For registered domain actions, appAction SHOULD be preferred.
  3. Handlers MUST set sideEffectState correctly.
  4. Non-idempotent handlers SHOULD NOT perform silent retries.

interface SDKPolicyConfig {
mode?: "local-only" | "delegated" | "hybrid"; // default hybrid
document?: PolicyDocument;
}
type SDKPolicyEvaluator =
(context: PolicyContext) => Promise<PolicyDecision> | PolicyDecision;
  • local-only: Decision made exclusively within the SDK
  • delegated: Decision delegated to an external policy counterpart
  • hybrid: Local preflight rules plus optional delegation
  1. The SDK MUST have at least one policy decision before every Action.
  2. A local deny decision MUST be final.
  3. confirm MUST be routed into the Runtime Confirmation flow.
  4. handoff MUST trigger waitForUser() or an equivalent UX.

The SDK SHOULD consider browser state when requireUserActivation is set. navigator.userActivation makes this state queryable; this is precisely why this check belongs locally in the SDK and not in some late backend oracle. ([MDN Web Docs][2])


For complex widgets, design system components, and those creative frontend excesses that look like controls visually but are semantically half art installations, the SDK needs adapters.

interface UIAPElementAdapter {
id: string;
match(node: Element): boolean;
describe(
node: Element,
ctx: {
route?: RouteContext;
scopeId?: ScopeId;
}
): Partial<UIElement> | null;
execute?(
node: Element,
request: ActionRequestPayload,
ctx: ActionHandlerContext
): Promise<ActionHandlerResult | null>;
}
  • Adapters SHOULD run before generic heuristics.
  • Adapters MAY publish additional semantics and actions.
  • Adapters SHOULD be used for custom components and open Shadow Roots.

Open and closed Shadow Roots must be treated differently: open roots can be traversed by script, closed ones cannot. For closed Shadow DOM you therefore need either host-side bindings or explicit adapters on the host, rather than hoping that JavaScript will suddenly learn to read minds. ([MDN Web Docs][5])


interface RouteProvider {
getRoute(): RouteContext;
subscribe?(listener: (route: RouteContext) => void): Unsubscribe;
}
  • The SDK SHOULD prefer an app-specific routeId.
  • If no provider is set, the SDK MAY use URL-based fallbacks.

interface SignalEmitter {
emitSignal(signal: WebSignal): void;
}
  • route.changed
  • toast.shown
  • status.changed
  • validation.changed
  • dialog.opened
  • dialog.closed
  • submission.started
  • submission.finished

The SDK SHOULD additionally offer DOM-side integration points via CustomEvent, because CustomEvent.detail can cleanly carry app data along. ([MDN Web Docs][6])

Recommended events:

  • uiap:ready
  • uiap:snapshot
  • uiap:signal
  • uiap:action-request
  • uiap:action-result
  • uiap:policy-decision

Cross-origin DOM remains restricted by the Same-Origin Policy. When parent and child need to cooperate, the bridge must therefore rely on messaging rather than direct DOM access. window.postMessage() is the native mechanism for this. ([MDN Web Docs][7])

interface FrameConfig {
enabled?: boolean;
channel?: string; // default "uiap"
}
interface FrameBridgeOptions {
mode: "parent" | "child";
targetWindow: Window;
targetOrigin: string;
channel?: string;
}
interface FrameBridge {
connect(): void;
disconnect(): void;
send(message: UIAPEnvelope): void;
}
  1. targetOrigin MUST be set explicitly.
  2. A Bridge MUST validate the incoming origin and channel.
  3. Cross-origin frames without a bridge MUST be treated as opaque.
  4. Bridged frames SHOULD publish their own documentId and optionally a bridgeSessionId.

interface OverlayConfig {
ghostCursor?: boolean;
highlightStyle?: "outline" | "spotlight" | "pulse";
narration?: boolean;
zIndex?: number;
}
interface OverlayController {
showCursor(target: ResolvedTarget, options?: { pace?: "instant" | "humanized" }): void;
highlight(target: ResolvedTarget, options?: { style?: "outline" | "spotlight" | "pulse" }): void;
announce(text: string): void;
clear(): void;
}
  • Overlay is presentation, not truth.
  • Overlay MUST NOT fake success when action.result is missing.
  • ghostCursor SHOULD use a separate visual cursor.

interface ExternalDriverAdapter {
execute(
request: ActionRequestPayload,
target: ResolvedTarget | undefined
): Promise<ActionHandlerResult>;
}

For out-of-process automation, the SDK MAY use a driver adapter built on WebDriver or WebDriver BiDi. The important thing is that the UIAP lifecycle remains the same: same policy, same verification, same error classes. ([W3C][8])


type SDKEventName =
| "ready"
| "snapshot"
| "signal"
| "action:accepted"
| "action:progress"
| "action:result"
| "policy:decision"
| "error";

A conforming @uiap/web-sdk MUST:

  • Offer createUIAP() or an equivalent initialization,
  • Be able to publish snapshots,
  • Support at least registerAction, registerPolicyEvaluator, bindElement, bindScope, emitSignal,
  • Enforce local policy decisions,
  • Correctly handle open/closed shadow boundaries,
  • Correctly mark opaque cross-origin contexts as inaccessible,
  • Emit Action and Policy lifecycle events.

A conforming SDK SHOULD:

  • Publish deltas,
  • Use MutationObserver,
  • Offer CustomEvent hooks,
  • Support postMessage frame bridges,
  • Optionally offer an overlay.

import { createUIAP } from "@uiap/web-sdk";
const uiap = createUIAP({
app: {
id: "videoland",
version: "1.4.2",
profile: "[email protected]",
locale: "de-CH"
},
transport: myTransport,
observe: {
throttleMs: 120,
includeHidden: false,
includeNonInteractive: false
},
policy: {
mode: "hybrid",
document: {
modelVersion: "0.1",
extension: "uiap.policy",
defaults: {
onSafeRisk: "allow",
onConfirmRisk: "confirm",
onBlockedRisk: "handoff",
onUnknownAction: "deny",
onSensitiveRead: "confirm",
onSecretRead: "deny"
},
rules: []
}
},
overlay: {
ghostCursor: true,
highlightStyle: "spotlight",
narration: true
}
});
uiap.bindScope(document.querySelector("[data-form='video-create']")!, {
id: "video.create.form",
kind: "form",
name: "Video erstellen"
});
uiap.bindElement(document.querySelector("#title")!, {
id: "video.title",
scopeId: "video.create.form",
meaning: "title"
});
uiap.bindElement(document.querySelector("#submit")!, {
id: "video.submit",
scopeId: "video.create.form",
defaultAction: "video.create",
risk: "confirm",
success: [
{ kind: "route.changed", pattern: "/videos/:id" },
{ kind: "toast.contains", text: "erstellt" }
]
});
uiap.registerAction(
{
id: "video.create",
kind: "domain",
targetKinds: ["scope"],
executionModes: ["appAction", "semanticUi"],
risk: { level: "confirm", tags: ["external_effect"] }
},
async (ctx) => {
const granted = await ctx.requestConfirmation({
actionHandle: ctx.actionHandle,
actionId: "video.create",
risk: { level: "confirm", tags: ["external_effect"] },
preview: { summary: "Video erstellen" }
});
if (granted !== "granted") {
return {
status: "failed",
sideEffectState: "none",
error: {
code: "confirmation_denied",
message: "Action was not confirmed"
}
};
}
// domain app action
await app.actions.createVideo();
ctx.emitSignal({
signalId: "sig_1",
kind: "toast.shown",
level: "success",
text: "Video erstellt"
});
return {
status: "succeeded",
sideEffectState: "applied"
};
}
);
uiap.registerPolicyEvaluator((context) => {
if (context.dataClasses?.includes("credential")) {
return {
decision: "handoff",
reasonCodes: ["credential_data", "human_actor_required"]
};
}
if (context.risk?.level === "confirm") {
return {
decision: "confirm",
reasonCodes: ["risk_confirm"]
};
}
return {
decision: "allow",
reasonCodes: ["policy_default"]
};
});
await uiap.start();

  • [UIAP-CORE] UIAP Core v0.1
  • [UIAP-WEB] UIAP Web Profile v0.1
  • [RFC2119] Key words for use in RFCs to Indicate Requirement Levels, BCP 14
  • The SDK MUST run in the same origin context as the host application; external script injection MUST NOT be enabled.
  • Transport credentials MUST NOT be hardcoded in client-side code.
  • targetOrigin for Frame Bridges MUST be set explicitly, never "*".
  • Policy evaluators MUST be executed locally before Actions are handed to the transport.
  • Overlay elements MUST NOT obscure real UI elements of the host application or intercept clicks.
VersionDateChanges
0.12026-03-27Initial draft