UI Framework
Browser-based block orchestration that works with any frontend framework.
Two UI Layers
BOA splits browser-side blocks into two distinct categories based on purity. This separation keeps your logic deterministic and testable while still giving you full access to browser APIs when you need them.
UI-Block Pure
- State derivation, formatting, calculations
- No DOM, no fetch, no side effects
- Deterministic: same input = same output
- Automatically memoized with LRU cache
Examples: deriveCheckoutState, formatPrice, computeDiscount
UI Capability Block Side Effects
fetch(),localStorage, clipboard, browser APIs- Controlled & observable
- Declare
MOCKresponses for offline testing
Examples: fetchCart, readLocalStorage
Framework-Agnostic SDK
The createBoaUI() factory gives you a runtime that registers blocks and workflows, then executes
them. It returns a lightweight object with three methods — no framework lock-in.
import { createBoaUI } from "@boa-framework/ui";
// 1. Create the UI runtime
const boa = createBoaUI();
// 2. Register blocks (pure or capability)
boa.registerBlock(deriveCheckoutStateDef);
boa.registerBlock(fetchCartDef);
// 3. Register a workflow that chains them
boa.registerWorkflow(checkoutWorkflowDef);
// 4. Run it
const result = await boa.run("CheckoutWorkflow", { cartId: 42 });
| Method | Description |
|---|---|
registerBlock(def) |
Register a UI-Block or UI Capability Block definition. |
registerWorkflow(def) |
Register a workflow that chains registered blocks. |
run(name, input) |
Execute a workflow by name with the given input. Returns a promise. |
Works With Every Framework
The SDK is a plain JavaScript module. Call boa.run() from any framework — React, Vue,
Angular, Svelte, or vanilla JS. The blocks and workflows stay the same; only the glue code changes.
React
import { useState, useEffect } from "react";
import { boa } from "./boa-setup";
function Checkout({ cartId }) {
const [state, setState] = useState(null);
useEffect(() => {
boa.run("CheckoutWorkflow", { cartId })
.then(setState);
}, [cartId]);
if (!state) return <p>Loading...</p>;
return (
<div>
<h2>{state.title}</h2>
<p>{state.total}</p>
</div>
);
}
Vue
import { boa } from "./boa-setup";
export default {
props: ["cartId"],
data() {
return { state: null };
},
async mounted() {
this.state = await boa.run(
"CheckoutWorkflow",
{ cartId: this.cartId }
);
},
template: `
<div v-if="state">
<h2>{{ state.title }}</h2>
<p>{{ state.total }}</p>
</div>
<p v-else>Loading...</p>
`
};
Vanilla JS
import { createBoaUI } from "@boa-framework/ui";
const boa = createBoaUI();
// ... register blocks and workflows ...
const result = await boa.run("CheckoutWorkflow", { cartId: 42 });
document.getElementById("title").textContent = result.title;
document.getElementById("total").textContent = result.total;
Key Features
Memoization
Pure blocks are cached with an LRU strategy. Same input returns the cached result instantly — no re-computation, no wasted cycles.
Sandbox
Pure blocks run with deep-frozen input. Global mutation detection catches impure behavior at runtime, preventing hidden side effects.
Observability
Hook into onStepStart, onStepEnd, onStepSkip, and
onStepError callbacks to trace every block execution in the browser.
MOCK Testing
Capability blocks declare mock I/O inline. Run boa test from the CLI to verify UI logic
without spinning up a browser or a server.
Error Handling
When a block fails, an ON_ERROR block catches it and returns a standardized error object your UI
can display directly.
// ON_ERROR blocks return a structured error envelope
{
"userMessage": "Unable to load your cart. Please try again.",
"code": "CART_FETCH_FAILED",
"recoverable": true
}
recoverable is true, your UI can show a retry button. When false,
the workflow halts and the error is surfaced to the user immediately.
UI Block vs Backend Block
Backend blocks and UI blocks share the same manifest format and testing tools, but they execute differently.
Backend blocks use the Universal Runtime Protocol (child process, STDIN/STDOUT). UI blocks are plain ES module
functions loaded via dynamic import().
Backend Block URP
// Reads STDIN JSON, writes STDOUT JSON
// Runs as a child process
async function main() {
const chunks = [];
for await (const chunk of process.stdin)
chunks.push(chunk);
const envelope = JSON.parse(
Buffer.concat(chunks).toString("utf-8")
);
const input = envelope.input;
// Business logic
const output = { total: input.a + input.b };
console.log(JSON.stringify(
{ success: true, output }
));
}
main();
UI Block ES Module
// Plain function, loaded via import()
// Runs in the browser
export default function computeDiscount(input) {
const { subtotal, couponCode } = input;
let discount = 0;
if (couponCode === "SAVE10") {
discount = subtotal * 0.1;
}
return {
discount,
finalTotal: subtotal - discount
};
}
// Same I/O contract, same manifest,
// same FIXTURE tests — different runtime.
| Aspect | Backend Block | UI Block |
|---|---|---|
| Protocol | URP — STDIN/STDOUT JSON | ES module — import() |
| Execution | Child process | In-browser, same thread |
| Languages | Node, Python, Go, Rust, any | JavaScript / TypeScript |
| Manifest | block.boa |
block.boa (identical format) |
| Testing | boa test BlockName@1.0.0 |
boa test BlockName@1.0.0 |
block.boa, test them with boa test, and wire
them into workflows with workflow.boa. The only difference is how the engine invokes them.