Architecture
BOA organizes software into five distinct layers, each with a clear purpose. This layered model keeps blocks small, isolated, and composable — whether they run on the server or in the browser.
The Five-Layer Model (Backend)
Every backend block lives in exactly one of five layers. Higher layers depend on lower layers, never the reverse. This strict hierarchy prevents circular dependencies and keeps each block’s context small enough for both humans and AI to reason about.
Layer 1 — Primitives
The smallest, purest functions in the system. Math operations, string utilities, data validation, formatting helpers. Primitives have zero business logic and zero side effects. They never change once written, making them safe to reuse across every project in a monorepo.
Examples
Add@1.0.0— adds two numbersMultiply@1.0.0— multiplies two numbersFormatString@1.0.0— template interpolation
Characteristics
- Pure functions — same input always produces same output
- No network calls, no database access, no file I/O
- Highly reusable across projects
- Ideal candidates for memoization
Layer 2 — Capabilities
Capabilities wrap I/O operations behind a clean interface. API clients, database queries, file uploads, email senders, message queue publishers — anything that talks to the outside world. They isolate side effects so that higher layers never deal with raw I/O.
Examples
- REST client — wraps HTTP requests
- Email sender — sends transactional email
- File uploader — stores files in cloud storage
- Database query — runs a parameterized SQL query
Characteristics
- Encapsulates side effects (network, disk, etc.)
- Provides a stable contract for higher layers
- Can be mocked for testing domain blocks
- Never contains business rules
Layer 3 — Domain Blocks
Domain blocks encode narrow, specific business rules. Each one answers a single business question: “What is the order total?”, “How much tax applies?”, “Is this email address valid?”. They are always version-pinned so that business logic is traceable and auditable.
Examples
CalculateOrderValue@1.0.0ComputeTax@1.0.0ValidateEmail@1.0.0ApplyDiscount@1.0.0
Characteristics
- One business rule per block
- May depend on Primitives and Capabilities
- Version-pinned in workflows
- Self-testing via FIXTURE declarations
Layer 4 — Workflows
Workflows are declarative chains of blocks. They contain no logic — only orchestration and data mapping. A workflow declares which blocks to call, in what order, and how to pass data between them. The workflow engine handles execution, error propagation, and context management.
workflow.boa
WORKFLOW OrderProcessing 1.0.0
DESC Calculate value and apply tax for an order.
STEP calcValue = CalculateOrderValue@1.0.0
MAP items <- _initial.items
STEP calcTax = ComputeTax@1.0.0
MAP subtotal <- calcValue.subtotal
Layer 5 — Interfaces
Interfaces are entry points that call workflows. REST controllers, React pages, CLI commands, WebSocket handlers — they accept external input, invoke the appropriate workflow, and return the result. Interfaces never contain business logic; they only translate between the outside world and the workflow layer.
Universal Runtime Protocol (URP)
All backend blocks communicate through the Universal Runtime Protocol. URP is radically simple: a block reads a JSON envelope from STDIN, performs its work, and writes a JSON result to STDOUT. This is what makes BOA polyglot — any language that can read stdin and write stdout can be a block.
Input Envelope
The runtime sends this JSON object to the block’s standard input:
{
"block": "CalculateOrderValue",
"version": "1.0.0",
"input": {
"items": [
{ "price": 29.99, "quantity": 2 },
{ "price": 9.99, "quantity": 1 }
]
}
}
Success Response
When the block completes successfully, it writes this to standard output:
{
"success": true,
"output": {
"subtotal": 69.97
}
}
Error Response
When the block encounters an error, it writes this instead:
{
"success": false,
"error": {
"type": "ValidationError",
"message": "Items array cannot be empty."
}
}
The Three Guarantees
URP enforces three guarantees that make blocks safe to compose:
| Guarantee | What It Means |
|---|---|
| Isolation | Each block runs in its own process. No shared memory, no shared state. One block cannot corrupt another. |
| Contract | Inputs and outputs are declared in block.boa. The runtime validates them before and after
execution. |
| Determinism | Given the same input envelope, a block must produce the same output. Side effects are confined to Capability blocks. |
UI Architecture (Browser)
The backend uses URP (stdin/stdout) because blocks run as separate processes. In the browser, that model doesn’t apply — everything runs in a single JavaScript thread. So the UI framework uses a different, simpler protocol: plain ES module functions.
The UI layer is split into two block types:
UI Blocks
ui-block
Pure logic — state derivation, formatting, calculations, data transformations. No DOM manipulation,
no fetch(), no browser APIs. Because they are pure, they are memoizable and trivially
testable.
UI Capability Blocks
ui-capability
Side effects — fetch(), localStorage, clipboard access, geolocation,
WebSocket connections. They wrap browser APIs behind a stable interface so that UI blocks remain pure.
UI Block Signature
Unlike backend blocks that use URP’s stdin/stdout protocol, UI blocks are plain ES module functions. They receive an input object and return an output object synchronously (or as a Promise for async capability blocks):
ui-block
// Pure UI block — no side effects
export default function formatPrice(input) {
const { amount, currency } = input;
return {
formatted: `${currency} ${amount.toFixed(2)}`
};
}
ui-capability
// Capability block — wraps fetch() side effect
export default async function fetchCart(input) {
const res = await fetch(`/api/cart/${input.cartId}`);
const data = await res.json();
return { cart: data };
}
Block Anatomy
Every block has a block.boa manifest that declares everything about it: identity, layer,
runtime, inputs, outputs, business rules, test fixtures, and dependencies. This manifest is the single source
of truth — both the framework and AI read it to understand what the block does.
block.boa
# Identity
BLOCK CalculateOrderValue 1.0.0
LAYER domain
RUNTIME node
ENTRY index.js
DESC Computes the subtotal from a list of order items.
# AI-native metadata
INTENT Calculate the total value of a shopping cart.
RULE subtotal = sum of (price * quantity) for each item.
RULE Round the result to 2 decimal places.
RULE Return 0 if the items array is empty.
TAGS order, cart, pricing, subtotal
# Contract
IN items:array! # required (!) — list of order items
OUT subtotal:number # the computed subtotal
# Self-testing fixtures
FIXTURE {"items":[{"price":50,"quantity":2}]} -> {"subtotal":100}
FIXTURE {"items":[]} -> {"subtotal":0}
# Error types this block may throw
ERR ValidationError
# Dependencies on other blocks
DEPS Multiply@1.0.0
Each field serves a specific purpose:
| Field | Purpose |
|---|---|
BLOCK |
Block name and semantic version. Workflows pin this exact version. |
LAYER |
Which architectural layer: primitive, capability, domain,
workflow, or interface. |
RUNTIME |
Execution runtime: node, python, go, rust, etc.
|
ENTRY |
The file that the runtime executes (e.g., index.js, main.py). |
DESC |
One-line human-readable description of what the block does. |
INTENT |
The user’s original requirement in their own words. Preserves the why for future AI sessions. |
RULE |
Business rules and constraints, one per line. Structured and parseable by AI. |
TAGS |
Semantic keywords for block discovery and search. |
IN |
Input field with type. ! means required, ? means optional. |
OUT |
Output field with type. Declares the block’s return contract. |
FIXTURE |
Expected input/output pairs for self-testing. Run via boa test or
boa validate. |
ERR |
Error types the block may throw (e.g., ValidationError). |
DEPS |
Other blocks this block depends on, with pinned versions. |
INTENT, RULE, FIXTURE, and TAGS fields exist
specifically for AI. They let an AI agent understand what a block does, why it exists, what constraints it
must obey, and how to test it — all without reading the implementation code.
Data Flow
Workflows pass data between blocks using a simple referencing system. Each step in a workflow produces output that subsequent steps can reference by step ID. The workflow engine manages an internal WorkflowContext that accumulates all step outputs.
How References Work
When a workflow starts, the initial input is available under the special key _initial. Each
step’s output is stored under its step ID. The MAP directive wires data from one
step’s output to another step’s input.
workflow.boa
WORKFLOW OrderProcessing 1.0.0
DESC Calculate value, apply tax, and format the result.
# Step 1: reads from the workflow's initial input
STEP calcValue = CalculateOrderValue@1.0.0
MAP items <- _initial.items # workflow input
# Step 2: reads from Step 1's output
STEP calcTax = ComputeTax@1.0.0
MAP subtotal <- calcValue.subtotal # output of step 1
# Step 3: reads from both initial input and Step 2's output
STEP format = FormatString@1.0.0
MAP template <- _initial.template # workflow input
MAP total <- calcTax.total # output of step 2
Context Accumulation
As the workflow engine executes each step, the context grows:
| After Step | Available References |
|---|---|
| Start | _initial.items, _initial.template |
calcValue |
_initial.*, calcValue.subtotal |
calcTax |
_initial.*, calcValue.*, calcTax.total,
calcTax.tax |
format |
_initial.*, calcValue.*, calcTax.*, format.result
|
_initial or the output of a step that appears before it in the
workflow. Referencing a later step is a validation error that boa validate will catch.
Visual Data Flow
For the OrderProcessing workflow above, data flows like this:
Workflow Input
{ items, template }
|
+----------+----------+
| |
v |
+-------------------+ |
| CalculateOrder | |
| Value@1.0.0 | |
| | |
| items <- _initial | |
| -> subtotal | |
+-------------------+ |
| |
v |
+-------------------+ |
| ComputeTax@1.0.0 | |
| | |
| subtotal <- | |
| calcValue | |
| -> total, tax | |
+-------------------+ |
| |
+----------+----------+
|
v
+-------------------+
| FormatString |
| @1.0.0 |
| |
| template <- |
| _initial |
| total <- calcTax |
| -> result |
+-------------------+
|
v
Workflow Output
{ result }