Add it to an existing project in three steps
Works with any Kotlin Multiplatform project that uses Compose Multiplatform and KSP, or pure Android Jetpack Compose projects.
// components-sdui/build.gradle.kts in fediim-design-system
val anysduiVersion = "1.0.0-alpha01"
commonMain.dependencies {
implementation("dev.fediim:anysdui-annotations:$anysduiVersion")
api("dev.fediim:anysdui-runtime:$anysduiVersion")
}
ksp {
arg("anysdui.catalogId", "urn:fediim:fds")
arg("anysdui.libraryVersion", "0.1.0")
}
dependencies {
add("kspCommonMainMetadata", "dev.fediim:anysdui-processor:$anysduiVersion")
}
// components-sdui/src/commonMain/kotlin/dev/fediim/fds/sdui/SduiActions.kt
import dev.fediim.anysdui.annotations.*
@SduiComponent
@Composable
fun Button(
@SduiUserGesture onClick: () -> Unit,
variant: ButtonVariant,
modifier: Modifier = Modifier,
@SduiDefault("ButtonSize.Medium") size: ButtonSize = ButtonSize.Medium,
@SduiDefault("true") enabled: Boolean = true,
@SduiDefault("null") @SduiChild icon: (@Composable () -> Unit)? = null,
@SduiDefault("null") text: String? = null,
@SduiDefault("{}") @SduiChildren content: @Composable RowScope.() -> Unit = {
if (text != null) Text(text)
},
) { ... }
// app module: implementation("dev.fediim:anysdui-transport-ktor:1.0.0-alpha01")
val catalogId = "urn:fediim:fds:0.1.0"
val transport = remember {
WebSocketTransport(host = "localhost", port = 8081, supportedCatalogIds = listOf(catalogId))
}
val state = remember(transport) {
SurfaceState(surfaceId = "screen", catalogId = catalogId, transport = transport)
}
LaunchedEffect(state, transport) { transport.incoming.collect { state.apply(it) } }
SduiSurface(state = state, renderer = FdsRenderer)
Alpha Release (1.0.0-alpha01). Published to GitHub Packages under group dev.fediim at 1.0.0-alpha01 (and 1.0.0-SNAPSHOT). Pure Android projects without KMP can wrap binary AAR design systems directly.
Alpha release (1.0.0-alpha01)
Following Google's A2UI for Jetpack Compose showcase at Android Dev Summit (28 October 2026), AnySDUI 1.0.0-alpha01 is now available as our first tagged Alpha release under group dev.fediim. It ships a complete, compile-time checked A2UI v1.0 stack for Kotlin Multiplatform and pure Android Jetpack Compose.
- 18 published artifacts under
dev.fediim:anysdui-*:1.0.0-alpha01covering annotations, KSP2 processor, wire protocol, strict validator, Compose runtime, Ktor WebSocket & A2A 0.3/1.0 transports, Kotlin agent SDK, Compose UI test kit, and 8 ready-made catalogs (plus custom KSP catalogs). - Google A2UI v1.0 wire compatibility: checked against upstream commit
ccf5dc20d0with 400 of 409 applicable conformance cases passing in CI. - 6 native targets from one Kotlin codebase: Android (
minSdk 28), Pure Android (binary AAR wrapper modules), iOS (iosArm64,iosSimulatorArm64), Desktop JVM, Web WasmJs, and Web JS. - Design-to-device workflows (RFC-0012): Direct Figma frame push routes, server-side composite expansion, Follow/Explore multi-screen flow navigation (
anysdui.navigate), and HMAC-SHA256 signed zero-install web viewer links.
Google A2UI compatibility & migration
Google's A2UI protocol and its AndroidX Jetpack Compose libraries (androidx.a2ui:*, androidx.a2ui.compose:* and androidx.compose.material3:material3-a2ui) define a declarative JSON standard for agent-generated UIs. Google's renderer targets A2UI v0.9 (its basic catalog id points to v0_9). AnySDUI implements A2UI v1.0 and does not read v0.9 messages, so an agent written for Google's renderer needs to move to v1.0 first. On top of the protocol, AnySDUI removes the two biggest bottlenecks teams face when adopting A2UI in production: manual catalog/renderer glue and single-platform lock-in.
Artifact mapping: Google androidx.a2ui vs AnySDUI
| Role | Google AndroidX (1.0.0-alpha02) | AnySDUI (1.0.0-alpha01) | What AnySDUI adds |
|---|---|---|---|
| Wire model | androidx.a2ui:a2ui-model | dev.fediim:anysdui-protocol | Multiplatform (Android, iOS, JVM, WasmJs, JS); zero UI/network deps |
| Engine & validation | androidx.a2ui:a2ui-engine | dev.fediim:anysdui-validation | STRICT & LENIENT modes, @path/@call/checks, ThemeSpec & function bounds |
| Compose runtime | androidx.a2ui.compose:compose-runtime | dev.fediim:anysdui-runtime | Compose Multiplatform + Pure Android, RenderScope.WithTheme subtree theming and DTCG motion |
| Custom catalog API | androidx.a2ui.compose:compose-ui (manual A2uiComponent / A2uiProperty) | dev.fediim:anysdui-annotations + anysdui-processor (KSP2) | Compile-time generation of both CatalogRenderer and catalog.json from @SduiComponent; build-time @SduiExample validation |
| Basic catalog (18 components) | androidx.compose.material3:material3-a2ui | dev.fediim:anysdui-compose-basic | The upstream A2UI v1.0 basic catalog, with schemas equal to upstream's, on all 6 targets |
| Extended catalogs | Not included | anysdui-compose-material3 (60), material (20), foundation (17), cupertino (28), charts (4), 3d (1), xr (1) | 149 components in total out of the box; mix multiple catalogs on one SurfaceHost |
| Agent SDK & A2A | External / manual | anysdui-agent, anysdui-a2a, anysdui-agent-a2a | Streaming parser, structured validation repair hints, A2A 0.3 & 1.0 client/server, generated Claude/Gemini/ADK skills |
| Component test kit | androidx.a2ui.compose:compose-ui-testing (A2uiTestSurface) | dev.fediim:anysdui-compose-testing | SduiTestController with node stubbing, data model inspection & action assertions |
Conformance and the 9 known differences
AnySDUI runs Google's upstream A2UI v1.0 RC conformance suite (pinned at ccf5dc20d0) in CI with make test-a2ui-conformance. Of 409 applicable cases, 400 pass. The build fails if any other case fails. The 9 known differences are listed, each with its issue:
- Agent function call ids (3 cases): the host generates the id of every
callAgentFunctionand accepts none from the caller, and a handler that throws is answered withINVALID_FUNCTION_CALL. - Bindings need
@path(2 cases): a bare{"path": …}object stays plain data. Only{"@path": …}binds, as upstream's own reserved-keys rules say. - Basic catalog at the pin (2 cases): the pinned basic catalog has no
Button.title, andCheckBoxkeeps its state invalue. - User activation (1 case): a call sent by an agent never counts as a user gesture.
- Unknown catalog error (1 case): an unresolvable catalog id is reported as a top-level
error, not inside the function response.
Migrating a custom component from androidx.a2ui.compose
If you started prototyping with Google's androidx.a2ui.compose, replace manual A2uiComponent and A2uiProperty blocks with @SduiComponent directly on your Composable:
// BEFORE: Google androidx.a2ui.compose (manual schema + manual extraction)
val statusCard = A2uiComponent(
name = "StatusCard",
properties = listOf(
A2uiProperty.string("headline", required = true),
A2uiProperty.boolean("urgent", required = false),
A2uiProperty.action("onDismiss", required = false),
)
) { scope, node ->
StatusCard(
headline = scope.resolveString(node, "headline") ?: "",
urgent = scope.resolveBoolean(node, "urgent") ?: false,
onDismiss = { scope.dispatchAction(node, "onDismiss") },
)
}
// AFTER: AnySDUI (compile-time KSP generates renderer, catalog.json & agent skill)
@SduiUsage("Displays an operational status alert with optional dismiss action.")
@SduiComponent
@Composable
fun StatusCard(
headline: String,
@SduiDefault("false") urgent: Boolean = false,
@SduiUserGesture onDismiss: () -> Unit = {},
) { ... }
Built on Google's A2UI protocol. Designed to go much further.
Google open-sourced the A2UI protocol in December 2025. At Android Dev Summit on 28 October 2026, it showcased A2UI for Jetpack Compose (androidx.a2ui), in alpha since September 2026. That renderer targets A2UI v0.9. AnySDUI implements the newer A2UI v1.0 and passes 400 of 409 applicable upstream conformance cases. It also removes manual component registration and brings your design system to 6 platforms.
// In Google's library, every custom Composable needs manual
// A2uiProperty descriptors, JSON extraction and catalog wiring:
val metricCardComponent = A2uiComponent(
name = "MetricCard",
properties = listOf(
A2uiProperty.string("title", required = true),
A2uiProperty.string("value", required = true),
A2uiProperty.string("trend", required = false),
A2uiProperty.action("onInspect", required = false),
)
) { scope, node ->
val title = scope.resolveString(node, "title") ?: return@A2uiComponent
val value = scope.resolveString(node, "value") ?: return@A2uiComponent
val trend = scope.resolveString(node, "trend") ?: "neutral"
MetricCard(
title = title,
value = value,
trend = Trend.valueOf(trend.uppercase()),
onInspect = { scope.dispatchAction(node, "onInspect") }
)
}
// Then register manually (Android-only):
val catalog = A2uiCatalog(
catalogId = "https://acme.com/catalog/v1",
components = MaterialA2uiBasicCatalogV1Defaults.all + metricCardComponent
)
// In AnySDUI, annotate the Composable you already have.
// KSP generates both CatalogRenderer AND catalog.json at compile time:
@SduiUsage("Displays a KPI metric with trend badge and drill-down action.")
@SduiComponent
@Composable
fun MetricCard(
title: String,
value: String,
@SduiDefault("Trend.NEUTRAL") trend: Trend = Trend.NEUTRAL,
@SduiUserGesture onInspect: () -> Unit = {},
modifier: Modifier = Modifier,
) {
// Your existing Compose Multiplatform or Jetpack Compose UI code
}
// Zero manual property extraction.
// Zero schema drift: changing a Kotlin parameter updates catalog.json,
// strict validator rules, and generated Claude/Gemini/ADK skills in one build.
// Works on Android, Pure Android AARs, iOS, Desktop JVM, Web Wasm & JS.
{
"version": "v1.0",
"createSurface": {
"surfaceId": "booking_surface",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json",
"components": [
{ "id": "root", "component": "Card", "child": "col" },
{ "id": "col", "component": "Column", "children": ["title", "guest_in", "book_btn"] },
{ "id": "title", "component": "Text", "text": "Reserve Table" },
{ "id": "guest_in", "component": "TextField", "label": "Guest name",
"value": { "@path": "/reservation/guestName" },
"checks": [{ "condition": { "@call": "required",
"args": { "value": { "@path": "/reservation/guestName" } } },
"message": "Name is required" }] },
{ "id": "book_btn", "component": "Button", "variant": "primary", "child": "btn_txt",
"action": { "event": { "name": "confirmBooking" } } },
{ "id": "btn_txt", "component": "Text", "text": "Confirm" }
],
"dataModel": { "reservation": { "guestName": "Alex Rivera" } }
}
}
{
"version": "v1.0",
"updateComponents": {
"surfaceId": "booking_surface",
"components": [
{ "id": "occupancy_chart", "component": "LineChart",
"catalogId": "urn:anysdui:compose-charts:1.0.0",
"label": "Table occupancy this week",
"xLabels": ["Mon", "Tue", "Wed", "Thu", "Fri"],
"series": { "@path": "/metrics/occupancySeries" } },
{ "id": "venue_preview", "component": "Model3D",
"catalogId": "urn:anysdui:compose-3d:1.0.0",
"src": "https://cdn.acme.com/models/dining-hall.glb",
"autoRotate": true, "cameraElevation": 20 },
{ "id": "next_step_btn", "component": "Button",
"catalogId": "urn:fediim:fds:0.1.0",
"variant": "primary", "text": "Choose a seat",
"onClick": { "event": {
"name": "anysdui.navigate",
"context": { "targetFrameId": "seating_map", "mode": "explore" }
} } }
]
}
}
// AnySDUI's SurfaceHost renders standard Google A2UI Basic agents
// alongside Material 3 (60 components), Charts, 3D, and your own catalog:
val host = remember(transport) {
SurfaceHost(
transport = transport,
registrations = listOf(
// 1. Standard A2UI v1.0 Basic Catalog (18 components)
CatalogRegistration(ComposeBasicRenderer, CatalogValidator.fromJsonString(basicCatalogJson)),
// 2. Extended Material 3 Catalog (60 components)
CatalogRegistration(ComposeMaterial3Renderer, CatalogValidator.fromJsonString(material3CatalogJson)),
// 3. Vico Charts & SceneView 3D Catalogs
CatalogRegistration(ComposeChartsRenderer, CatalogValidator.fromJsonString(chartsCatalogJson)),
CatalogRegistration(Compose3dRenderer, CatalogValidator.fromJsonString(model3dCatalogJson)),
// 4. A KSP-generated company design system (fediim, urn:fediim:fds)
CatalogRegistration(FdsRenderer, CatalogValidator.fromJsonString(fdsCatalogJson)),
),
validationMode = ValidationMode.STRICT,
themePolicy = ThemePolicy.MERGE,
)
}
SduiSurface(host = host)
// Google's A2UI requires separate renderers per ecosystem: // - Android: androidx.a2ui.compose (Kotlin) // - iOS: SwiftUI renderer (Planned) // - Web: Lit / Angular / React (TypeScript) // - Cross-platform: Flutter GenUI (Dart) // // With AnySDUI, one annotated Kotlin @Composable compiles to: // ✓ Android (minSdk 28) // ✓ Pure Android (wraps closed binary Jetpack Compose AARs) // ✓ iOS (iosArm64 & iosSimulatorArm64 via Compose Multiplatform) // ✓ Desktop (JVM macOS, Windows, Linux) // ✓ Web Wasm (wasmJs browser target) // ✓ Web JS (js(IR) browser target) // // Plus pure-Kotlin anysdui-protocol, anysdui-validation & anysdui-agent // modules with zero UI or network dependencies for any backend or agent.
| Capability | Google A2UI (androidx.a2ui 1.0.0-alpha02) |
AnySDUI (dev.fediim.anysdui 1.0.0-alpha01) |
|---|---|---|
| A2UI wire protocol | v0.9 Basic catalog id points to v0_9 |
Yes · 400/409 Pinned to upstream v1.0 RC (ccf5dc20d0) with @path, @call & checks verified in CI; v0.9 is not supported |
| Basic Catalog (18 components) | Yes androidx.compose.material3:material3-a2ui |
Yes dev.fediim:anysdui-compose-basic (for agents that write A2UI v1.0) |
| Custom design system onboarding | Manual glue Hand-written A2uiComponent, A2uiProperty schemas & state extractors |
Compile-time KSP Add @SduiComponent to existing @Composable functions; processor generates renderer & catalog.json |
| Compile-time example & drift checks | No No build-time validation of example payloads | Yes Renderer & schema come from the same signature; @SduiExample payloads are validated during build |
| Platforms from one Compose codebase | Android only Requires separate Flutter (Dart), Web (Lit/React/Angular) or SwiftUI codebases | 6 targets Android, Pure Android (binary AAR wrappers), iOS, Desktop JVM, Web WasmJs & Web JS |
| Built-in production catalogs | 1 catalog Basic Catalog (18 components) | 8 catalogs · 149 components Basic (18), Material 3 (60), Material 2 (20), Foundation (17), Cupertino (28), Charts (4), 3D (1), Android XR (1) + custom |
| Multi-catalog composition | Merged map Single merged catalog per surface | Per-node namespace SurfaceHost routes each node by its own catalogId with zero name collisions |
| Runtime theming & W3C tokens | Basic Surface properties & host MaterialTheme | Full + Subtree Runtime setTheme/setThemeMode, WithTheme subtree overrides, ThemePolicy & W3C DTCG 2025.10 importer |
| Motion & reduced-motion policy | Manual Hand-coded per component | Declarative DTCG Tokenized durations, easings, springs, automatic state & list enter/exit animations + accessibility MotionPolicy |
| Multi-screen flows & Figma push | No Single-surface agent rendering | RFC-0012 built-in Direct Figma frame push, composite expansion, Follow/Explore anysdui.navigate flows & HMAC-signed web share links |
| Agent SDK, skills & UI test kit | Client engine a2ui-engine runtime validation and compose-ui-testing (A2uiTestSurface) |
End-to-end Multiplatform anysdui-agent SDK with repair hints, auto-generated Claude/Gemini/ADK skills, A2A 1.0 & anysdui-compose-testing |
Tech stack
AnySDUI is a set of Kotlin Multiplatform libraries. The client side is Compose Multiplatform. The protocol, validation and agent libraries are plain Kotlin with no UI or network framework, so a server or an agent can use them alone.
| Layer | Technology | Version on main |
|---|---|---|
| Language | Kotlin Multiplatform | 2.4.0 |
| UI | Compose Multiplatform (plus pure Android Jetpack Compose wrappers) | 1.12.0 |
| Code generation | KSP2 processor, KotlinPoet | KSP 2.3.10 |
| Serialization | kotlinx-serialization-json | 1.11.0 |
| Concurrency | kotlinx-coroutines | 1.11.0 |
| Networking | Ktor client, only in the transport, A2A and image or model loading modules | 3.5.2 |
| 3D | SceneView, behind the opt-in 3D catalog | 4.35.0 |
| Charts | Vico, behind the opt-in charts catalog | 3.3.1 |
| Component testing | Compose ui-test, in anysdui-compose-testing | 1.12.0 |
| Android | Android Gradle Plugin, minSdk 28 (API 28 verification floor) | AGP 9.4.0 |
| JDK | 21, or 22 for a Desktop build that includes the 3D catalog | 21 |
| Sample server | Ktor server, Koog for AI writers, Exposed + Flyway for database | Ktor 3.5.2 · Koog 1.2.0 |
Targets
Standards
- A2UI v1.0, pinned to upstream release-candidate commit
ccf5dc20d0with@pathand@callsyntax, checked against upstream's core conformance suite in CI (400 of 409 applicable cases pass; the other 9 are documented differences). - A2A 0.3 and 1.0 for talking to agents, and the server can itself be an A2A 1.0 agent.
- JSON Pointer (RFC 6901) for data bindings, and JSON Schema for catalogs.
- W3C Design Tokens (DTCG 2025.10) for importing themes (colors, typography, dimensions, and motion).
- RFC-0012: Figma screens to devices push pipeline, composite expansion, Follow/Explore flow navigation, and signed share links.
Architecture
Each module depends only on the ones above it. Static guards in CI enforce the boundaries, for example that the protocol module never imports a UI or networking framework.
catalog.json and the Kotlin renderer.SurfaceState, SurfaceHost, SduiSurface, data model, motion animation, subtree theming (WithTheme), and the transport interface.WebSocketTransport on the Ktor client.ui-test.Life of a session
createSurface: a flat list of component nodes with ids, plus an initial data model. Children are lists of ids.action with its context, or anysdui.navigate to change flow frames.updateDataModel at a JSON Pointer path. Only composables that read that path recompose.Messages & routes
| Protocol | Direction | Messages & Endpoints |
|---|---|---|
| WebSocket (A2UI) | Server to client | createSurface, updateComponents, updateDataModel, deleteSurface, callRendererFunction, agentFunctionResponse |
| WebSocket (A2UI) | Client to server | action (including anysdui.navigate for flows), callAgentFunction, rendererFunctionResponse, error |
| Push API (RFC-0012) | Plugin to server | POST /playground/push (atomic flow push), PUT /playground/push/images/{sha256}, POST /playground/push/presenter |
| Share links | Browser to server | GET /view/{viewId} (web viewer), POST /playground/links, POST /playground/links/regenerate |
{
"version": "v1.0",
"createSurface": {
"surfaceId": "surf_counter",
"catalogId": "urn:anysdui:sample-designsystem:1.0.0",
"metadata": { "extensions": { "anysdui_theme": { "mode": "dark" } } },
"components": [
{ "id": "root_card", "component": "DsCard", "content": ["counter_text", "increment_btn"] },
{ "id": "counter_text", "component": "DsText", "text": { "@path": "/state/count" } },
{ "id": "increment_btn", "component": "DsButton",
"onClick": { "event": { "name": "increment" } }, "content": ["btn_label"] },
{ "id": "btn_label", "component": "DsText", "text": "Increment Count" }
],
"dataModel": { "state": { "count": 0 } }
}
}
Artifacts
Every library is published under the group dev.fediim with one shared version. Take only what your side of the wire needs.
| Artifact | What it is | Who needs it |
|---|---|---|
anysdui-annotations | @SduiComponent and the other annotations | Design system authors |
anysdui-processor | KSP processor that generates renderers and catalog.json | Design system authors |
anysdui-runtime | SduiSurface, SurfaceHost, data model, rendering, subtree theming | Apps (KMP and pure Android) |
anysdui-transport-ktor | WebSocketTransport | Apps that connect over WebSocket |
anysdui-a2a | A2A 0.3 and 1.0 client and wire models | Apps that render an A2A agent |
anysdui-protocol | Message types and serializers | Servers, agents |
anysdui-validation | Catalog model and validators, without Compose | Servers, agents |
anysdui-agent | Kotlin agent SDK | Agents written in Kotlin |
anysdui-agent-a2a | A2A packaging and transport adapter for the agent SDK | Agents connecting over A2A |
anysdui-compose-foundation | Compose Foundation catalog (16 components) | Apps, optional |
anysdui-compose-material | Material 2 catalog (20 components) | Apps, optional |
anysdui-compose-material3 | Material 3 catalog (60 components: core, content, navigation, and overlays) | Apps, optional |
anysdui-compose-basic | The A2UI v1.0 basic catalog, all 18 components | Apps that render standard A2UI agents |
anysdui-compose-charts | Charts catalog (LineChart, BarChart, PieChart, Sparkline via Vico 3.3.1) | Apps, optional |
anysdui-compose-cupertino | iOS-style catalog (Switch, SegmentedControl, Slider, Button, ActivityIndicator, AlertDialog) | Apps, optional |
anysdui-compose-3d | Model3D catalog | Apps, optional |
anysdui-compose-testing | Test kit for catalog components on Compose ui-test | Catalog authors, apps (test scope only) |
The sample server, the playground, the theme importer (with motion token import) and the demo apps live in the repository and are not published.
Installation
Prerequisites
- A Kotlin Multiplatform module with the Compose Multiplatform, Compose compiler and KSP plugins.
- JDK 21 or newer. Xcode on macOS for the iOS targets.
- For GitHub Packages, a GitHub token with the
read:packagesscope.
1. Add a repository
The libraries are not on Maven Central. Use GitHub Packages, or build them into your local Maven repository.
dependencyResolutionManagement {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/fediim-dev/anysdui-kmp")
credentials(PasswordCredentials::class)
}
google()
mavenCentral()
}
}
// Credentials come from the environment. Never commit them.
// ORG_GRADLE_PROJECT_GitHubPackagesUsername = your GitHub user name
// ORG_GRADLE_PROJECT_GitHubPackagesPassword = a token with read:packages
// First, in a checkout of anysdui-kmp (on macOS, to get the iOS targets):
// ./gradlew publishToMavenLocal
dependencyResolutionManagement {
repositories {
mavenLocal()
google()
mavenCentral()
}
}
2. Add the dependencies
val anysduiVersion = "1.0.0-alpha01"
kotlin {
sourceSets {
commonMain.dependencies {
// A ready-made catalog. It brings anysdui-runtime with it.
implementation("dev.fediim:anysdui-compose-foundation:$anysduiVersion")
// WebSocketTransport lives in its own artifact.
implementation("dev.fediim:anysdui-transport-ktor:$anysduiVersion")
// Only when you annotate your own components:
implementation("dev.fediim:anysdui-annotations:$anysduiVersion")
}
}
}
dependencies {
add("kspCommonMainMetadata", "dev.fediim:anysdui-processor:$anysduiVersion")
}
Android-only projects (Pure Jetpack Compose)
Projects that are not Kotlin Multiplatform and wrap a binary design system (an AAR you cannot annotate) consume AnySDUI through a wrapper module. AGP 9 compiles Kotlin itself and runs KSP directly on the debug variant.
plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.ksp)
}
val anysduiVersion = "1.0.0-alpha01"
android {
namespace = "com.acme.sdui"
compileSdk = 35
defaultConfig { minSdk = 28 }
}
ksp {
arg("anysdui.catalogId", "urn:acme:sdui")
arg("anysdui.libraryVersion", anysduiVersion)
}
dependencies {
implementation(project(":components-sdui"))
implementation("dev.fediim:anysdui-annotations:$anysduiVersion")
api("dev.fediim:anysdui-runtime:$anysduiVersion")
ksp("dev.fediim:anysdui-processor:$anysduiVersion")
}
Let an agent do it. The repository ships an integrating-anysdui skill. Copy that folder into your project's agent skills folder and a coding agent can do the wiring on this page for you.
Create your own catalog
A catalog is the list of components a server may use, with a schema for each. You do not write it by hand. You annotate composables in your design system module and the processor generates it.
1. Wire the design system module
kotlin {
sourceSets {
commonMain {
// KSP runs on common metadata; its output must be added by hand.
kotlin.srcDir(layout.buildDirectory.dir("generated/ksp/metadata/commonMain/kotlin"))
dependencies {
implementation("dev.fediim:anysdui-annotations:$anysduiVersion")
api("dev.fediim:anysdui-runtime:$anysduiVersion") // the generated renderer implements CatalogRenderer
}
}
}
}
ksp {
arg("anysdui.catalogId", "urn:fediim:fds")
arg("anysdui.libraryVersion", "0.1.0")
}
dependencies {
add("kspCommonMainMetadata", "dev.fediim:anysdui-processor:$anysduiVersion")
}
// Every target compilation must wait for the common KSP pass.
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {
if (name != "kspCommonMainKotlinMetadata") dependsOn("kspCommonMainKotlinMetadata")
}
The id a client and server exchange is <catalogId>:<libraryVersion>, here urn:fediim:fds:0.1.0.
2. Annotate components
import dev.fediim.anysdui.annotations.*
@SduiComponent
@Composable
fun Button(
@SduiUserGesture onClick: () -> Unit,
variant: ButtonVariant,
modifier: Modifier = Modifier,
@SduiDefault("ButtonSize.Medium") size: ButtonSize = ButtonSize.Medium,
@SduiDefault("true") enabled: Boolean = true,
@SduiDefault("null") @SduiChild icon: (@Composable () -> Unit)? = null,
@SduiDefault("null") text: String? = null,
@SduiDefault("{}") @SduiChildren content: @Composable RowScope.() -> Unit = {
if (text != null) Text(text)
},
) { ... }
| Annotation | What it does |
|---|---|
@SduiComponent | Exposes a public @Composable. The name defaults to the function name. KDoc becomes the description agents read. |
@SduiDefault("expr") | Required on every optional parameter except a Modifier. Injected into the generated renderer; resolves simple and nested (Inner.X) types. |
@SduiChildren, @SduiChild | A composable slot that takes a list of children, or exactly one child. |
@SduiUserGesture | Marks a callback fired only by the user's own input. Its action may then run functions that need a user activation, such as openUrl. |
@SduiRes | The parameter takes a theme token (colour, typography, icon) instead of a literal value. |
@SduiTheme | Registers an object of tokens. Emits schemas into anysdui_types.<slug>.json and catalog.bundle.json. |
@SduiReadyWhenResolved | The component is not drawn until this value has arrived. The surface shows its loading slot instead. |
@SduiCheckable | The component accepts validation checks and has an error state. |
@SduiSchema | Replaces a parameter's generated schema with JSON you write, for shapes Kotlin types cannot express. |
@SduiUsage, @SduiExample | Tell an agent when to pick the component and show a valid message that uses it. |
@SduiPreferred | Marks preferred components for agents when multiple catalog components match a role. |
@SduiCatalog | Catalog-level metadata, as an alternative to the KSP options. |
3. Declare theme tokens
// Tokens read the live theme, so they follow every fediim palette
@SduiTheme(ResType.COLOR)
object FdsSduiColors {
val bgCard: Color
@Composable get() = FediimTheme.colors.bgCard
val textPrimary: Color
@Composable get() = FediimTheme.colors.textPrimary
// ...
}
@SduiTheme(ResType.TYPOGRAPHY)
object FdsSduiTypography {
val headline: TextStyle
@Composable get() = FediimTheme.typography.headline
val body: TextStyle
@Composable get() = FediimTheme.typography.body
// ...
}
A component asks for a token with @SduiRes. The foundation catalog's BasicText does it like this:
@SduiComponent
@Composable
fun BasicText(
text: String,
@SduiRes(ResType.TYPOGRAPHY)
@SduiDefault("androidx.compose.ui.text.TextStyle.Default")
style: TextStyle = TextStyle.Default,
@SduiRes(ResType.COLOR)
@SduiDefault("androidx.compose.ui.graphics.Color.Unspecified")
color: Color = Color.Unspecified,
modifier: Modifier = Modifier,
) { ... }
4. Two-way inputs need no annotation
A component with a value parameter and an onValueChange of the same type is bound to the data model in both directions. Supported types are String, Boolean, Float, Double and List<String>. Input is stored only when the message binds value to a path, such as {"@path": "/inputs/name"}. A literal value is shown read-only.
5. Teach agents your catalog
Examples and instructions travel inside the catalog, so a model can produce valid screens without extra prompting.
@SduiExample(
name = "greeting",
json = """[
{"version": "v1.0", "createSurface": {"surfaceId": "greeting",
"catalogId": "urn:anysdui:sample-designsystem:1.0.0"}},
{"version": "v1.0", "updateComponents": {"surfaceId": "greeting", "components": [
{"id": "root", "component": "DsText", "text": "Hello"}
]}}
]"""
)
@SduiComponent
@Composable
fun DsText(text: String) { ... }
// build.gradle.kts: layout rules and conventions for the whole catalog
ksp {
arg("anysdui.instructions",
providers.fileContents(layout.projectDirectory.file("catalog-instructions.md")).asText.get())
}
The processor checks every example's structure at build time. An invalid example fails the build and names the exact JSON Pointer.
6. What the build generates
| Output | Location under build/generated/ksp/metadata/commonMain/ |
|---|---|
| Renderer | kotlin/<package>/anysdui/generated/<Slug>Renderer.kt, an object that implements CatalogRenderer. For urn:fediim:fds it is FdsRenderer. |
| Catalog | resources/catalog.json, an A2UI v1.0 catalog definition |
| Extension types | resources/anysdui_types.<slug>.json: modifiers and colour, typography, icon and shape references |
| Examples | resources/examples/<name>.json |
Ship the catalog yourself. catalog.json is not packaged into any artifact. Copy or publish it to wherever your server or agent validates and prompts from, and refresh that copy whenever a component signature changes.
Keep the design system clean
If you do not want AnySDUI imports in your core library, put the annotated wrappers in a separate bridge module that depends on it. The fediim design system does this with its components-sdui module.
Render in an app
One catalog
val catalogId = "urn:fediim:fds:0.1.0"
val transport = remember {
WebSocketTransport(host = "localhost", port = 8081, supportedCatalogIds = listOf(catalogId)) // path defaults to /a2ui
}
val state = remember(transport) {
SurfaceState(surfaceId = "screen", catalogId = catalogId, transport = transport)
}
LaunchedEffect(state, transport) { transport.incoming.collect { state.apply(it) } }
SduiSurface(state = state, renderer = FdsRenderer)
Several catalogs on one surface
Register each renderer together with its validator. A component resolves its catalog by its own catalogId, otherwise the surface default. There is no fallback by name, so two catalogs may both declare Row.
val host = remember(transport) {
SurfaceHost(
transport = transport,
registrations = listOf(
CatalogRegistration(FdsRenderer, CatalogValidator.fromJsonString(fdsCatalogJson)),
CatalogRegistration(ComposeFoundationRenderer, CatalogValidator.fromJsonString(foundationCatalogJson)),
),
validationMode = ValidationMode.STRICT,
)
}
// The host does not read the transport itself; without this loop the surface stays empty.
LaunchedEffect(host, transport) { transport.incoming.collect { host.apply(it) } }
SduiSurface(host = host)
Loading and error slots
SduiSurface(
state = state,
renderer = FdsRenderer,
errorComponent = { node, error -> MyErrorTile(node.id, error) },
loadingComponent = { componentId -> MySkeleton() },
)
Intercept actions
An interceptor sees each action before it is sent. It returns the action, a changed copy, or null to consume it. Use it for analytics, confirmation dialogs, or closing a sheet locally.
val host = SurfaceHost(
transport = transport,
actionInterceptors = listOf(ActionInterceptor { action -> analytics.track(action.name); action }),
)
Without a server
The runtime includes a FixtureTransport that replays a list of messages. Use it for previews, tests and offline demos.
Subtree theming
A container component can theme a subtree of components separately from the surface theme: scope.WithTheme(theme) { themed -> themed.RenderChild(childId) } merges the wrapper theme over the outer theme (tokens, mode, overrides) for all children below it.
@Composable
fun MyDarkCard(scope: RenderScope, node: SduiNode) {
val darkCardTheme = ThemeSpec(mode = ThemeMode.DARK)
scope.WithTheme(darkCardTheme) { themed ->
themed.RenderChildren(node, "children")
}
}
Testing catalog components
dev.fediim:anysdui-compose-testing tests catalog components on a real Compose surface inside runComposeUiTest. An SduiTestController creates surfaces, applies updates, stubs components by id or component name, and asserts on actions and host errors.
@OptIn(ExperimentalTestApi::class)
@Test
fun testButtonClick() = runComposeUiTest {
val controller = SduiTestController(listOf(CatalogRegistration(FdsRenderer, CatalogValidator.fromJsonString(fdsCatalogJson))))
controller.createSurface(
components = listOf(rootBox, buttonNode),
dataModel = buildJsonObject { put("count", 0) },
)
setContent { SduiTestSurface(controller) }
onNodeWithText("Increment").performClick()
assertEquals("incrementAction", controller.actions.single().name)
controller.close()
}
Drive from a server
Any server that sends the messages above over a WebSocket can drive a surface. For a JVM server, anysdui-protocol gives you the message types and anysdui-validation checks a message against a catalog before you send it. The repository contains a reference Ktor server; it is a sample, not a published library.
class MyScenario(private val catalog: Catalog) : ScenarioHandler {
override val requiredCatalogIds: List<String> = listOf(catalog.catalogId)
override suspend fun onConnect(session: SduiSession) {
session.send(catalog.createSurface("my_dynamic_screen", sendDataModel = false) {
component("DsText", id = "welcome_text") { prop("text", "Welcome to AnySDUI") }
})
}
override suspend fun onAction(session: SduiSession, action: Action) {
when (action.name) {
"buttonClicked" -> {
val userName = action.context["/inputs/userName"]?.jsonPrimitive?.content ?: ""
session.send(UpdateDataModel(
surfaceId = action.surfaceId,
path = "/status/message",
value = JsonPrimitive("Hello, $userName!"),
))
}
}
}
}
fun Application.module() {
sduiModule(scenarioHandler = MyScenario(catalog))
}
Multi-screen flows & navigation (RFC-0012)
Rooms hold complete multi-screen flows with presenter tracking and independent browsing. A client sends the standard anysdui.navigate action with context targetFrameId or mode ("follow" or "explore"). In Follow mode, paired devices follow live editor and presenter changes. In Explore mode, a user browses the flow independently without interrupting other devices.
Push API and signed share links
External tools like the Figma plugin push entire flows in one atomic call to POST /playground/push using server-issued push tokens (Authorization: Bearer asp_…). The server expands composite components, uploads images with SHA-256 deduplication, and can issue HMAC-SHA256 signed share links (/view/{viewId}?exp=…&sig=…) that let stakeholders view and navigate flows in any browser with zero installation.
Render an A2A agent instead
With anysdui-a2a, the same host renders any agent that speaks A2A 0.3 or 1.0 and emits A2UI.
val session = A2aAgentConnection("https://agent.example.com", catalogIds, engine).connect()
val transport = A2aTransport(session, dataModel = ::sendDataModelSurfaces)
val host = SurfaceHost(registrations, transport,
validationMode = ValidationMode.STRICT, limits = SurfaceHostLimits.A2A) // one host per session
transport.incoming.collect { host.apply(it) }
You pass the Ktor engine. It must not follow redirects, keep cookies or add credentials of its own.
Agents
Kotlin agent SDK
dev.fediim:anysdui-agent sits between your model client and the renderer. It does not call a model and has no transport. It gives you three things: the catalogs narrowed to what the renderer supports, a prompt snippet for your system prompt, and a parser that returns only messages the renderer would accept.
// Once per agent: immutable and safe to share between sessions.
val catalog: RcCatalog = InMemoryCatalogProvider(Json.parseToJsonElement(catalogJson) as JsonObject).load()
val generator = A2uiGenerator(catalogs = listOf(CatalogConfig(catalog)))
// Once per session with one renderer. Keep it for every later turn.
val session: A2uiRequestProcessor =
generator.createProcessor(A2uiRendererCapabilities.fromJson(capabilitiesJson, AgentLimits()))
// Each turn
val systemPrompt = "You are a helpful assistant.\n\n" + session.promptSnippet
val parts: List<ResponsePart> = session.parseResponse(callModel(systemPrompt))
// TextPart = prose for the user. A2uiPart = checked messages; send them to the renderer in order.
parseResponsethrowsA2uiParseExceptionorA2uiValidationExceptioninstead of returning a partial result. The validation error carries hints you can feed back to the model for a repair turn.- Streaming is supported: feed chunks as they arrive and receive a part when its block closes.
- It runs on Android, JVM, JS, WasmJs and iOS, and brings neither Compose nor Ktor.
Agent skills
The repository holds one generated skill per catalog under contract/skills/, plus a shared a2ui-core. Copy a2ui-core and the folder for each catalog you target, keeping the folder names.
| Framework | Where the folders go |
|---|---|
| Claude Code | .claude/skills/ |
| Gemini CLI | .gemini/skills/ or .agents/skills/ |
| ADK (Python) | Load each with load_skill_from_dir and pass them to SkillToolset |
The server as an agent
The reference server can serve A2A 1.0 itself, with bearer-token authentication and a mandatory daily spending cap. Its AI helper writes layouts with Claude, Gemini, OpenAI (using the Responses API with reasoning effort selection), or local models via Ollama. It validates each result and repairs errors before anything reaches a device. The repository includes an end-to-end design-system chatbot tutorial and sample.
Theming & motion
The host app sets a base theme and decides how far an agent may change it.
val host = SurfaceHost(
registrations = registrations,
hostTheme = ThemeSpec(
colors = mapOf("primary" to ColorToken(light = "#6750A4", dark = "#D0BCFF")),
motion = ThemeMotion(
durations = mapOf("short" to 150, "medium" to 300),
easings = mapOf("standard" to CubicBezierEasing(0.2f, 0f, 0f, 1f)),
),
),
themePolicy = ThemePolicy.MERGE, // the default; also HOST_ONLY, AGENT_WINS
)
- A
ThemeSpecholdsmode, token maps (colors,typography,fontFamilies,shapes,spacing,elevation),motiontokens, state-layer opacities and per-component overrides withdefaults,variantsandstates. - Motion tokens:
durations,easings,springs, andtransitionsdefine how state transitions and child enter/exit in lists animate. - State transitions: The generated renderer automatically animates color, Dp and Float parameter changes across states (e.g. hovered, pressed, focused) using
scope.animatedColor,animatedDp, andanimatedFloat. - Enter and exit: Children of
ColumnandRowanimate when appearing or disappearing according to component enter/exit tokens. - Reduced motion: The runtime detects system accessibility settings and enforces a
MotionPolicy(spatial movements snap immediately while color transitions adjust gracefully). - Subtree theming:
RenderScope.WithTheme(theme)lets a container component override tokens, component definitions and mode (e.g. rendering a dark card inside a light surface) for its subtree without affecting the rest of the surface. - Third-party catalogs declare their own token sets in
anysdui_types.<slug>.jsonandcatalog.bundle.json; built-in token sets are matched by exact catalog id. - Only colours carry separate light and dark values. Any token can alias another with
{"ref": "primary"}. - An agent's first theme rides in
createSurface.metadata.extensions.anysdui_theme. Later it callssetThemeorsetThemeMode. - The app can change a surface directly with
SurfaceState.updateThemeandupdateMode; the policy does not restrict those.
Import design tokens
# W3C Design Tokens: modes come as two token sets; --dark is optional. ./gradlew :anysdui-theme-import:run --args="dtcg --light tokens/light.tokens.json --dark tokens/dark.tokens.json --mapping tokens/mapping.json --out build/theme.json"
The result is a ThemeSpec JSON file, validated with the same rules the runtime applies. Exports from Figma, Tokens Studio and Style Dictionary are supported, including duration, cubicBezier and transition motion tokens.
Security and limits
A server-driven screen is untrusted input. The runtime treats it that way.
| Area | What the runtime does |
|---|---|
| Validation | Every message is checked against the catalog that will render it. STRICT rejects what does not match; LENIENT drops the unusable part and draws the rest. Renderer and validator are registered together, so they cannot disagree. |
| User activation | Functions such as openUrl run only from a callback marked @SduiUserGesture, never from composition, a timer or a load callback. |
| Images | Image URLs load only from origins the app allows. The list is empty by default. |
| 3D models | Origin allowlist, a fail-closed size manifest on the server, a byte ceiling and redirect pinning on the client. |
| Themes | At most 65,536 characters, 256 tokens per kind, 8 alias hops, 64 component entries. Font families come from an allowlist, never from URLs. |
| Charts & Cupertino | Bounds on data arrays (up to 12 series, 500 points, 50 pie slices; at most 6 segments), numbers clamped to finite ranges, string labels cut to 64 chars. |
| Push API & images | Push routes require bearer tokens (asp_…). Uploaded images verified by magic bytes (PNG, JPEG, WebP) with SHA-256 integrity, size ceilings, and served with Content-Security-Policy: sandbox. |
| Composite expansion | Composites nest at most 4 deep, with cycle detection, linear graph walking, and a 100 composite instance cap per push. |
| Share links | HMAC-SHA256 signed, time-limited (up to 30 days), no server-side secret leakage, pinned room caps, and viewer limits (up to 256 per room). |
| A2A | Bounded event, line, body and message sizes while reading. Only the configured origin is fetched. Plain http needs an explicit loopback flag. |
| Agent-only functions | setTheme, setThemeMode and the model3d_* camera functions can be called by the agent, not from a component. |
Run the repository
To try the playground, the sample server and the demo apps, clone fediim-dev/anysdui-kmp. A root Makefile wraps every task; make help lists them and make doctor reports the tools it finds.
| Command | What it does |
|---|---|
make run-server | Starts the server on port 8081 |
make run-web-wasm | Starts the web app and the visual playground editor |
make run-desktop | Runs the desktop companion app |
make run-android | Installs the Android debug app |
make build-ios-sim | Builds the iOS simulator framework; then run iosApp from Xcode |
make test-ci | Runs the test tasks CI runs |
make test-a2ui-conformance | Runs upstream's A2UI v1.0 conformance cases |
make verify | Guards, CI tests and conformance together |
To pair a device with the playground, point a client at ws://localhost:8081/a2ui?scenario=playground&pairingCode=<code>, using the code shown in the editor's top bar.
Known limitations & platform status
- Pre-1.0. No binary-compatibility promise between releases yet.
- Not on Maven Central. Published to GitHub Packages (group
dev.fediim); downloads need a token withread:packagesscope. - Package names. Renamed to
dev.fediim.anysduiacross all modules and published artifacts. - Android floor. Requires Android
minSdk 28(API 28 is the runtime verification floor). - Material 3.
anysdui-compose-material3implements 60 components across content, navigation and overlay tiers, and depends on Material3 1.12.0-alpha03. - Media.
VideoandAudioPlayerin the basic catalog draw labelled placeholders. - Desktop 3D.
Model3Ddraws a placeholder on Desktop (Foundation, Basic, Material 2, Material 3, Cupertino, and Charts catalogs run natively on all 6 targets with no placeholders). - Native iOS / SwiftUI spike. A native SwiftUI renderer was evaluated in #1283 and parked; Compose Multiplatform remains the single cross-platform rendering engine for full fidelity.
- A2UI coverage. 400 of 409 applicable upstream v1.0 RC conformance cases pass in CI with
@pathand@callsupport; the other 9 are documented differences, each with its issue. A2UI 0.9.x and compact output formats are not supported. - iOS publishing. The iOS artifacts can only be built on macOS.