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 MOCK responses for offline testing

Examples: fetchCart, readLocalStorage

Why the split? Pure UI-Blocks can be tested without a browser, memoized automatically, and reasoned about in isolation. Capability blocks isolate side effects so they can be mocked in CI and observed in production.

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 errors When 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
Same tools, different runtime You define both types of blocks with block.boa, test them with boa test, and wire them into workflows with workflow.boa. The only difference is how the engine invokes them.