Getting Started

Set up BOA and create your first block in under 10 minutes.


Prerequisites

Before you begin, make sure you have the following installed on your machine:

  • Node.js v18+ (recommend v22)
  • A terminal (bash, PowerShell, etc.)

Installation

Install the BOA CLI globally from npm with a single command.

Install the CLI

Install @boa-framework/core globally. This gives you the boa command system-wide:

bashnpm install -g @boa-framework/core

Verify the installation

Confirm that the CLI is accessible from any directory:

bashboa --help

You should see a list of available commands including init, run, test, validate, and more.

Tip The boa command is now available globally. You can use it from any project directory. To update to the latest version, run npm update -g @boa-framework/core.

Create Your First Project

With the CLI installed, let’s create a project from scratch and build a working block.

Initialize a new project

Create a new directory and scaffold a BOA project inside it:

bashmkdir MyProject && cd MyProject
boa init

This creates the following structure:

  • project.boa — the full project overview (read this first in every session)
  • blocks.boa — the block registry (one line per block)
  • src/ — source directories for each layer (Primitives, Capabilities, DomainBlocks)
  • workflows/ — directory for workflow definitions

Create a block

Use the CLI to scaffold a new domain block called Greet:

bashboa block create Greet --layer domain --runtime node

This creates src/DomainBlocks/Greet/ containing:

  • block.boa — the block manifest (identity, contract, rules, tests)
  • index.ts — the TypeScript implementation file

Define the block manifest

Open src/DomainBlocks/Greet/block.boa and define the block’s contract, rules, and test fixtures:

block.boaBLOCK Greet 1.0.0
LAYER domain
RUNTIME node
ENTRY index.js
DESC Generates a greeting message for a user.
INTENT User wants to greet someone by name.
RULE Output format is "Hello, {name}!".
RULE If name is empty, use "World".
TAGS greeting, string, demo

IN name:string?
OUT message:string

FIXTURE {"name": "Alice"} -> {"message": "Hello, Alice!"}
FIXTURE {} -> {"message": "Hello, World!"}

ERR ValidationError

Each line declares something specific: INTENT preserves why the block exists, RULE captures business constraints, and FIXTURE defines self-testing input/output pairs.

Implement the block

Open src/DomainBlocks/Greet/index.ts and write the implementation. The block reads a JSON envelope from stdin and writes a JSON result to stdout:

index.tsasync function main() {
  const chunks: Buffer[] = [];
  for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
  const envelope = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
  const { name } = envelope.input;

  const greeting = name ? `Hello, ${name}!` : "Hello, World!";

  console.log(JSON.stringify({
    success: true,
    output: { message: greeting }
  }));
}

main();

Compile TypeScript

Compile the .ts file to .js so the runtime can execute it:

bashnpx tsc --outDir src/DomainBlocks/Greet \
  --target ES2022 \
  --module Node16 \
  --moduleResolution Node16 \
  src/DomainBlocks/Greet/index.ts

Test the block

Run the fixtures you declared in block.boa to verify everything works:

bashboa test Greet@1.0.0

Expected output:

  Greet@1.0.0
    Fixture 1: PASS
    Fixture 2: PASS

  2 fixtures passed, 0 failed
Tip Every block should have at least one FIXTURE declaration. This lets you verify behavior with a single command and catches regressions automatically when you run boa validate.

Create a Workflow

Workflows chain multiple blocks together declaratively. Let’s create a second block and wire them into a workflow.

Create another block

Create an UpperCase block that converts text to uppercase:

bashboa block create UpperCase --layer primitive --runtime node

Define its manifest in src/Primitives/UpperCase/block.boa:

block.boaBLOCK UpperCase 1.0.0
LAYER primitive
RUNTIME node
ENTRY index.js
DESC Converts a text string to uppercase.
INTENT Convert any text to all uppercase letters.
RULE Output is the input text in all uppercase.
RULE If text is empty, return empty string.
TAGS string, transform, uppercase

IN text:string!
OUT result:string

FIXTURE {"text": "hello"} -> {"result": "HELLO"}
FIXTURE {"text": ""} -> {"result": ""}

And implement it in src/Primitives/UpperCase/index.ts:

index.tsasync function main() {
  const chunks: Buffer[] = [];
  for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
  const envelope = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
  const { text } = envelope.input;

  console.log(JSON.stringify({
    success: true,
    output: { result: text.toUpperCase() }
  }));
}

main();

Write the workflow

Create workflows/greet/workflow.boa to chain the two blocks together. The MAP directive wires data from one step’s output to the next step’s input:

workflow.boaWORKFLOW GreetAndShout 1.0.0
DESC Greet a user and convert to uppercase.

STEP greet = Greet@1.0.0
  MAP name <- _initial.name

STEP shout = UpperCase@1.0.0
  MAP text <- greet.message

The workflow engine reads name from the initial input, passes it to Greet, then pipes the greeting message into UpperCase.

Create an input file

Create input.json with the data you want to pass into the workflow:

input.json{
  "name": "Alice"
}
Windows note Always use --input-file instead of piping JSON with echo. Windows shells strip quotes from JSON strings, which causes parsing errors.

Run the workflow

Execute the workflow and see both blocks run in sequence:

bashboa run workflows/greet/workflow.boa --input-file input.json

Expected output:

{
  "result": "HELLO, ALICE!"
}

The initial input {"name": "Alice"} flows through Greet (producing "Hello, Alice!") then through UpperCase (producing "HELLO, ALICE!").

Validate everything

Run the full validation suite to confirm all blocks, fixtures, and workflow wiring are correct:

bashboa validate

Expected output:

  Blocks: 2 registered
  Fixtures: 4 passed, 0 failed
  Workflows: 1 valid
  Status: OK All valid

Create a UI Block

BOA also supports blocks that run in the browser. UI blocks are pure ES module functions — no stdin/stdout, just plain input and output. Let’s create one.

Create the block

Use the CLI to scaffold a UI block:

bashboa block create FormatGreeting --layer ui-block --runtime js

Write the block

UI blocks export a default function that takes an input object and returns an output object. No stdin, no JSON parsing — just a plain function:

index.js// Pure UI block — no side effects
export default function formatGreeting(input) {
  const { name, greeting } = input;
  return {
    html: `<h1>${greeting}</h1><p>Welcome, ${name}.</p>`
  };
}

And define the manifest in block.boa:

block.boaBLOCK FormatGreeting 1.0.0
LAYER ui-block
RUNTIME js
ENTRY index.js
DESC Formats a greeting into displayable HTML.
INTENT Render a greeting message as HTML for the browser.
RULE Output is an HTML string with the greeting and name.
TAGS ui, greeting, html, format

IN name:string!
IN greeting:string!
OUT html:string

FIXTURE {"name":"Alice","greeting":"Hello, Alice!"}
  -> {"html":"<h1>Hello, Alice!</h1><p>Welcome, Alice.</p>"}

Test it

Run the fixtures to verify:

bashboa test FormatGreeting@1.0.0

UI blocks use the same testing infrastructure as backend blocks. The only difference is how the runtime invokes them.

UI blocks vs backend blocks Backend blocks use the Universal Runtime Protocol (stdin/stdout JSON). UI blocks are plain ES module functions loaded via import(). Both share the same block.boa manifest format and the same testing tools.

Next Steps

You now have a working BOA project with blocks, a workflow, and a UI block. Here’s where to go next: