Tutorials
Hands-on projects that walk you through building real applications with BOA. Each tutorial has two tracks — pick the one that fits you.
Track A — Non-Developers
You describe what you want in plain English. An AI assistant (Claude, ChatGPT, Copilot, etc.) generates the entire project for you. You just run it.
Track B — AI Agents & Developers
You want to understand the architecture: how requirements decompose into blocks, how blocks wire together, and why the framework is designed this way.
All Tutorials
Tip Calculator
Calculate tips and split the bill between friends. Covers project setup, creating blocks, chaining them into a workflow, and running the result.
Going Polyglot
Extend the Tip Calculator with a Python block. Mix Node.js and Python in the same workflow — learn why the Universal Runtime Protocol makes language irrelevant.
More coming soon
Advanced tutorials covering UI blocks, capability blocks with mocks, error handling, parallel steps, and sub-workflows.
Tutorial 1: Tip Calculator
You’re at a restaurant with friends. You want a tool that takes the bill amount, tip percentage, and number of people — then tells you exactly how much each person pays. Let’s build it.
What you’ll learn
- How to create a BOA project from scratch
- How blocks encapsulate single responsibilities
- How workflows chain blocks together with data mapping
- How to test and validate everything with one command
What you’ll build
Two blocks and one workflow:
project.boa ← project overview
blocks.boa ← block registry
src/
DomainBlocks/
CalculateTip/
block.boa ← manifest + rules + tests
index.ts ← implementation
Primitives/
SplitBill/
block.boa
index.ts
workflows/
tip-calculator/
workflow.boa ← chains blocks together
input.json ← sample input
The data flows like this:
data flowInput: { billAmount: 85.50, tipPercent: 18, people: 3 }
CalculateTip → tipAmount: 15.39, totalWithTip: 100.89
|
SplitBill → perPerson: 33.63
Output: Each person pays $33.63
Install the BOA CLI
Open a terminal and run this command. It gives you the boa tool that runs your
projects:
bashnpm install -g @boa-framework/core
Verify it worked:
bashboa --help
Create a project folder
Create a new folder and initialize it as a BOA project:
bashmkdir TipCalculator && cd TipCalculator
boa init
This sets up the basic project structure. You now have a project.boa,
blocks.boa, and src/ directory ready to go.
Give your AI assistant this prompt
Open this project folder in your AI coding assistant (Claude Code, Cursor, GitHub Copilot, etc.) and give it this prompt:
“I want a tip calculator. It takes a bill amount, a tip percentage, and the number of people. It should calculate the tip, add it to the bill, then split the total evenly. Tell me how much each person pays. Round everything to 2 decimal places. Use Node.js.”
The AI will read your project’s CLAUDE.md (or equivalent instructions), understand
the BOA framework, and generate all the blocks, manifests, and workflows automatically.
What the AI generates for you
Behind the scenes, the AI creates two blocks and one workflow. You don’t need to understand the code — just know that it broke your request into small, testable pieces:
- CalculateTip — takes the bill and tip percentage, calculates the tip amount and total
- SplitBill — takes the total and number of people, calculates per-person share
- TipCalculator workflow — connects them: bill flows into CalculateTip, the total flows into SplitBill
Test that everything works
Run the validation command to make sure all the pieces are correct:
bashboa validate
You should see something like:
Blocks: 2 registered
Fixtures: 4 passed, 0 failed
Workflows: 1 valid
Status: OK All valid
All green. Every block tested itself automatically using the test cases the AI included.
Run it with real numbers
Create a file called input.json in your workflows folder with the numbers you want to
calculate:
input.json{
"billAmount": 85.50,
"tipPercent": 18,
"people": 3
}
Now run the workflow:
bashboa run workflows/tip-calculator/workflow.boa --input-file workflows/tip-calculator/input.json
Result
{
"perPerson": 33.63
}
Each of the 3 people pays $33.63 (bill $85.50 + 18% tip = $100.89, split 3 ways).
Change the numbers and run again
Edit input.json with different values and run the same command. Try a dinner for 5 people
with a 20% tip, or a coffee for 2 with 15%. The workflow handles any input.
Want to add a new feature? Just tell your AI assistant:
“Add a minimum tip rule: if the tip percentage is less than 10%, round it up to 10%.”
The AI will modify the right block, update the test cases, and revalidate — all without breaking the existing workflow.
Step 1: Requirement Decomposition
The user said: “Calculate tips and split the bill.”
This decomposes into two distinct responsibilities:
| Responsibility | Layer | Block | Why this layer? |
|---|---|---|---|
| Apply tip percentage to a bill | domain | CalculateTip | Business rule: how tips are calculated |
| Divide a number by N people | primitive | SplitBill | Generic math — reusable anywhere |
Why two blocks, not one?
A single “calculateTipAndSplit” function would work, but it violates the single responsibility principle. Splitting the bill is generic math (primitive layer) — it has no knowledge of tips. CalculateTip is a business rule (domain layer) — it knows what a “tip percentage” means. Keeping them separate means SplitBill can be reused in any project that needs to divide amounts.
Step 2: Project Scaffolding
bashmkdir TipCalculator && cd TipCalculator
boa init
boa block create CalculateTip --layer domain --runtime node
boa block create SplitBill --layer primitive --runtime node
This generates the folder structure, project.boa, blocks.boa, and stub files for
each block.
Step 3: CalculateTip Block (Domain Layer)
This block encapsulates the business rule: “a tip is a percentage of the bill.”
block.boa
block.boaBLOCK CalculateTip 1.0.0
LAYER domain
RUNTIME node
ENTRY index.js
DESC Calculates tip amount and total from a bill.
INTENT User wants to calculate how much tip
to leave on a restaurant bill.
RULE tipAmount = billAmount * (tipPercent / 100).
RULE totalWithTip = billAmount + tipAmount.
RULE Round both outputs to 2 decimal places.
TAGS tip, restaurant, billing, math
IN billAmount:number!
IN tipPercent:number!
OUT tipAmount:number
OUT totalWithTip:number
FIXTURE {"billAmount":100,"tipPercent":20}
-> {"tipAmount":20,"totalWithTip":120}
FIXTURE {"billAmount":85.50,"tipPercent":18}
-> {"tipAmount":15.39,"totalWithTip":100.89}
ERR ValidationError
index.ts
TypeScriptasync 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 { billAmount, tipPercent } =
envelope.input;
if (typeof billAmount !== "number" ||
typeof tipPercent !== "number") {
console.log(JSON.stringify({
success: false,
error: {
type: "ValidationError",
message: "Inputs must be numbers"
}
}));
return;
}
const tipAmount =
Math.round(
billAmount * (tipPercent / 100) * 100
) / 100;
const totalWithTip =
Math.round(
(billAmount + tipAmount) * 100
) / 100;
console.log(JSON.stringify({
success: true,
output: { tipAmount, totalWithTip }
}));
}
main();
INTENT preserves the user’s original words — so future AI sessions know
why this block exists. RULE lines are structured constraints an AI can parse and
verify. FIXTURE lines are self-testing — run boa test CalculateTip@1.0.0 and
the framework feeds the input through the block and compares against the expected output.
Step 4: SplitBill Block (Primitive Layer)
This block is pure math with no business context. It lives in the primitive layer because
dividing a number by N is a generic operation.
block.boa
block.boaBLOCK SplitBill 1.0.0
LAYER primitive
RUNTIME node
ENTRY index.js
DESC Divides a total evenly among people.
INTENT Split a total amount equally
among a group of people.
RULE perPerson = total / people.
RULE Round up to 2 decimal places (ceiling).
RULE people must be at least 1.
TAGS math, split, divide, billing
IN total:number!
IN people:number!
OUT perPerson:number
FIXTURE {"total":100,"people":4}
-> {"perPerson":25}
FIXTURE {"total":100.89,"people":3}
-> {"perPerson":33.63}
ERR ValidationError
index.ts
TypeScriptasync 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 { total, people } =
envelope.input;
if (typeof total !== "number" ||
typeof people !== "number" ||
people < 1) {
console.log(JSON.stringify({
success: false,
error: {
type: "ValidationError",
message:
"total must be a number, people >= 1"
}
}));
return;
}
const perPerson =
Math.round(
(total / people) * 100
) / 100;
console.log(JSON.stringify({
success: true,
output: { perPerson }
}));
}
main();
Primitive vs Domain — the key distinction
SplitBill knows nothing about tips, restaurants, or billing context. It divides a number by another number. This means it can be reused in any future project that needs to split an amount — rent splitting, expense sharing, etc. Domain blocks like CalculateTip encode business-specific rules that only make sense in a particular context.
Step 5: Register Blocks
Every block must be registered in blocks.boa so the framework can locate them at runtime:
blocks.boaCalculateTip 1.0.0 -> src/DomainBlocks/CalculateTip
SplitBill 1.0.0 -> src/Primitives/SplitBill
The format is: Name Version -> Path. The CLI uses this to resolve
CalculateTip@1.0.0 to src/DomainBlocks/CalculateTip/ when executing workflows.
Step 6: Compile and Test
Compile both TypeScript files to JavaScript:
bashnpx tsc --outDir src/DomainBlocks/CalculateTip --target ES2022 --module Node16 --moduleResolution Node16 src/DomainBlocks/CalculateTip/index.ts
npx tsc --outDir src/Primitives/SplitBill --target ES2022 --module Node16 --moduleResolution Node16 src/Primitives/SplitBill/index.ts
Test each block against its fixtures:
bashboa test CalculateTip@1.0.0
boa test SplitBill@1.0.0
Expected output:
CalculateTip@1.0.0
Fixture 1: PASS
Fixture 2: PASS
2 fixtures passed, 0 failed
SplitBill@1.0.0
Fixture 1: PASS
Fixture 2: PASS
2 fixtures passed, 0 failed
FIXTURE declarations in block.boa are the tests. No separate test files, no
test framework, no configuration. The framework reads the fixtures, feeds the input through the block, and
compares the output. If you change the block, the fixtures catch regressions immediately.
Step 7: Wire the Workflow
Create workflows/tip-calculator/workflow.boa to chain the two blocks:
workflow.boaWORKFLOW TipCalculator 1.0.0
DESC Calculate tip and split the bill among people.
STEP calc = CalculateTip@1.0.0
MAP billAmount <- _initial.billAmount
MAP tipPercent <- _initial.tipPercent
STEP split = SplitBill@1.0.0
MAP total <- calc.totalWithTip
MAP people <- _initial.people
How data flows
| Expression | Source | Destination |
|---|---|---|
_initial.billAmount |
Workflow input JSON | CalculateTip's billAmount field |
_initial.tipPercent |
Workflow input JSON | CalculateTip's tipPercent field |
calc.totalWithTip |
CalculateTip's output | SplitBill's total field |
_initial.people |
Workflow input JSON | SplitBill's people field |
Workflows contain zero logic
The workflow file is purely declarative — it only describes which blocks run and how data passes between them. There are no if-statements, no loops, no functions. This is by design: workflows are the wiring diagram, blocks are the logic. An AI agent can read this file and instantly understand the entire data flow without parsing any code.
Step 8: Update project.boa
The project overview file gives any AI (or human) a complete mental model of the project in one read:
project.boaPROJECT TipCalculator 1.0.0
DESC Calculates tips and splits bills among people.
DOMAIN Billing
CalculateTip: Computes tip amount and total with tip
SplitBill: Divides a total evenly among people
FLOW TipCalculator: CalculateTip -> SplitBill
project.boa to get the full mental model — every domain, every block, every workflow
— in a few lines. No need to crawl directories or parse dozens of files.
Step 9: Run and Validate
Create workflows/tip-calculator/input.json:
input.json{
"billAmount": 85.50,
"tipPercent": 18,
"people": 3
}
Run the workflow:
bashboa run workflows/tip-calculator/workflow.boa --input-file workflows/tip-calculator/input.json
Expected output:
{
"perPerson": 33.63
}
Validate the entire project:
bashboa validate
Blocks: 2 registered
Fixtures: 4 passed, 0 failed
Workflows: 1 valid
Status: OK All valid
Understanding the Universal Runtime Protocol (URP)
Every backend block communicates via a simple JSON contract over STDIN/STDOUT:
What the framework sends (STDIN)
JSON{
"block": "CalculateTip",
"version": "1.0.0",
"input": {
"billAmount": 85.50,
"tipPercent": 18
}
}
What the block returns (STDOUT)
JSON{
"success": true,
"output": {
"tipAmount": 15.39,
"totalWithTip": 100.89
}
}
This is why BOA is polyglot — any language that can read STDIN and write STDOUT JSON works. The framework spawns the block as a child process, pipes the envelope in, and reads the result out. Node.js, Python, Go, Rust — it all works the same way.
Why This Architecture Matters for AI
BOA exists to solve the “large codebase + AI context limit” problem. Here’s why each design decision helps:
| Design Decision | AI Benefit |
|---|---|
| Blocks are < 200 lines | Any single block fits entirely in an AI’s context window |
| Explicit IN/OUT contracts | AI knows exactly what a block accepts and returns without reading code |
| FIXTURE declarations | AI can verify its changes are correct by running boa test |
| INTENT keyword | Future AI sessions know why a block was created, not just what it does |
| RULE keyword | Business rules are structured text, not buried in code comments |
| Version pinning | Workflows reference exact versions, preventing accidental breakage |
| project.boa overview | One file gives the AI the complete mental model of the entire project |
| Isolation between blocks | Modifying one block cannot break another — low blast radius |