A step-by-step tutorial that builds up a composable system incrementally. Each step adds one key concept from the Composable Runtime model. All of the configuration files for this example are in the configs sub-directory.
- Rust and
rustup target add wasm32-unknown-unknown - cargo-component
- wkg
- Node.js (for examples 4–7, which use a mock translate API)
- The
composableCLI (cargo install composable-runtime) - The
composable-http-serverCLI (cargo install composable-http-server) for examples 6 and 7
cd examples/7cs
./build.sh # compiles the wasm components
./run.sh <1-7> # runs the specified example|
A single Wasm Component, declared by name with a path to its Of course, this initial version of our greeter simply returns: "Hello, World!" |
[component.greeter]
uri = "./lib/simple-greeter.wasm"$ ./run.sh 1
"Hello, World!"
|
A second component is added and injected into the first as an import. The runtime composes them at startup. Their interfaces are explicit and strict: they cannot provide any functionality other than what they declare as exports, and they cannot call out to anything other than their imports. Thus, neither of these components yet has any way to interact with external systems... no files, no network, no I/O, not even clocks or random number generators. But the greeter can now delegate to a translator with self-contained logic, passing 'haw-US' as the locale. This version of the translator is intentionally simplistic, translating just the word "Hello" across a handful of language codes: de, es, fr, haw, or nl. |
[component.greeter]
uri = "./lib/translating-greeter.wasm"
imports = ["translator"]
[component.translator]
uri = "./lib/simple-translator.wasm"$ ./run.sh 2
"Aloha, World!"
|
The The greeter now reads its locale from configuration instead of hardcoding it. |
[component.greeter]
uri = "./lib/configured-greeter.wasm"
imports = ["translator"]
config.locale = "es-ES"
[component.translator]
uri = "./lib/simple-translator.wasm"$ ./run.sh 3
"Hola, World!"
|
Host capabilities provide the interfaces a component needs to interact with the external world. For example, the The translator is now replaced with one that calls an external HTTP API. The |
[component.greeter]
uri = "./lib/configured-greeter.wasm"
imports = ["translator"]
config.locale = "fr-FR"
[component.translator]
uri = "./lib/capable-translator.wasm"
imports = ["http-client"]
[capability.http-client]
type = "wasi:http"$ ./run.sh 4
"Bonjour, le Monde!"
|
The The interceptor generated from logging-advice now wraps the translator and logs before (with args) and after (with the return value) each invocation of the translate function. |
[component.greeter]
uri = "./lib/configured-greeter.wasm"
imports = ["translator"]
config.locale = "de-DE"
[component.translator]
uri = "./lib/capable-translator.wasm"
imports = ["http-client"]
interceptors = ["logger"]
[component.logger]
uri = "./lib/logging-advice.wasm"
imports = ["logging-stdout"]
[capability.http-client]
type = "wasi:http"$ ./run.sh 5
... [interceptor]: Before translate(text: "Hello, World!", locale: "de-DE")
... [interceptor]: After translate -> "Hallo, Welt!"
"Hallo, Welt!"
|
An HTTP server accepts incoming requests and consults its configured routes. Each route indicates whether to invoke a specific component + function (where path segments can map to arg names) or to publish to a channel, as shown in this example. The greeter subscribes to the "names" channel and is invoked when messages arrive. Rather than calling |
[server.api]
type = "http"
port = 8080
[server.api.route.hello]
path = "/hello"
channel = "names"
[component.greeter]
uri = "./lib/configured-greeter.wasm"
imports = ["translator"]
config.locale = "nl-NL"
subscription = "names"
[component.translator]
uri = "./lib/capable-translator.wasm"
imports = ["http-client"]
interceptors = ["logger"]
[component.logger]
uri = "./lib/logging-advice.wasm"
imports = ["logging-stdout"]
[capability.http-client]
type = "wasi:http"$ ./run.sh 6
POST /hello with body 'World':
The HTTP server logs the result asynchronously:
... [interceptor]: Before translate(text: "Hello, World!", locale: "nl-NL")
... [interceptor]: After translate -> "Hallo, Wereld!"
... composable_runtime::messaging::activator: invocation complete component=greeter function=greet result="Hallo, Wereld!"
|
Finally, the single config file is split into domain, env, and ops files. Each could be owned by a different team or role based on responsibilities. But more importantly, this decouples the configuration of domain components from the capabilities and infrastructure that may vary across environments. Notice the greeter can now be configured with a |
domain.toml:
[component.greeter]
uri = "./lib/configured-greeter.wasm"
imports = ["translator"]
config.locale = "${process:env:LOCALE|en-US}"
subscription = "names"
[component.translator]
uri = "./lib/capable-translator.wasm"
imports = ["http-client"]
interceptors = ["logger"]env.toml:
[server.api]
type = "http"
port = 8080
[server.api.route.hello]
path = "/hello"
channel = "names"
[capability.http-client]
type = "wasi:http"ops.toml:
[component.logger]
uri = "./lib/logging-advice.wasm"
imports = ["logging-stdout"]And finally, the shared infra.toml used for examples 4-7:
[component.logging-stdout]
uri = "./lib/wasi-logging-to-stdout.wasm"
imports = ["wasip2"]
[capability.wasip2]
type = "wasi:p2"$ LOCALE=fr ./run.sh 7
POST /hello with body 'World':
... [interceptor]: Before translate(text: "Hello, World!", locale: "fr")
... [interceptor]: After translate -> "Bonjour, le Monde!"
... composable_runtime::messaging::activator: invocation complete component=greeter function=greet result="Bonjour, le Monde!"
As the completed example shows, the Composable Runtime enables environment-aware late-binding and a collaborative deployment model. That is possible because Wasm Components:
- have strict and explicit interface-based contracts
- are stored as architecture-agnostic artifacts that can be shared via OCI registries
- can be generated, composed, and instantiated with negligible latency
A domain team can change component wiring without modifying infrastructure. An ops team can add cross-cutting interceptors without modifying domain logic. A platform team can curate and configure the capabilities that will be available wherever the components land.
Note
The ability to support collaboration in highly dynamic environments will be increasingly important as AI agents become collaborators across each of these roles with varying degrees of autonomy.
The Composable Runtime also defines an extensibility model that will support additional host capabilities, interceptors, and servers. The existing functionality has been built upon that same model.
For more detail on specific concepts (messaging, interceptors, extensibility, etc), explore the other examples.