Going Polyglot
Extend the Tip Calculator with a Python block. Mix Node.js and Python in the same workflow — the framework handles everything.
CalculateTip and SplitBill blocks before starting. You also
need Python 3 installed on your machine.
The Idea
Your Tip Calculator works, but now the restaurant wants to add tax. Different jurisdictions have different tax
rates. You’ll add a ComputeTax block — written in Python —
that calculates tax on the bill before tipping.
The updated data flow:
data flowInput: { billAmount: 85.50, taxRate: 0.08, tipPercent: 18, people: 3 }
ComputeTax (Python) → tax: 6.84, billWithTax: 92.34
|
CalculateTip (Node.js) → tipAmount: 16.62, totalWithTip: 108.96
|
SplitBill (Node.js) → perPerson: 36.32
Output: Each person pays $36.32
Three blocks, two languages, one workflow. The framework doesn’t care what language a block is written in — it just pipes JSON through STDIN/STDOUT.
Open your project
Open the TipCalculator project from Part 1 in your AI coding assistant.
Describe what you want
Give your AI assistant this prompt:
“I want to add tax calculation to the tip calculator. The tax should be calculated before the tip. Take a tax rate as input (like 0.08 for 8%), apply it to the bill amount, then calculate the tip on the bill-plus-tax total. Write the tax block in Python instead of Node.js.”
The AI reads your existing project.boa and blocks.boa, understands
what’s already there, and adds the new piece without breaking anything.
What the AI generates
The AI creates one new block and updates the workflow:
- ComputeTax (Python) — multiplies the bill by the tax rate, adds it to the bill
- Updated workflow — ComputeTax runs first, its output feeds into CalculateTip, then SplitBill
The existing Node.js blocks stay exactly the same. The new Python block slots in seamlessly.
Validate everything still works
bashboa validate
Blocks: 3 registered
Fixtures: 6 passed, 0 failed
Workflows: 1 valid
Status: OK All valid
Three blocks now (was two). All six test cases pass, including the two new ones for ComputeTax.
Run with tax included
Update your input.json with a tax rate:
input.json{
"billAmount": 85.50,
"taxRate": 0.08,
"tipPercent": 18,
"people": 3
}
bashboa run workflows/tip-calculator/workflow.boa --input-file workflows/tip-calculator/input.json
Result
{
"perPerson": 36.32
}
Bill $85.50 + 8% tax ($6.84) = $92.34. Plus 18% tip ($16.62) = $108.96. Split 3 ways = $36.32 each.
The polyglot magic
Notice you didn’t have to do anything special for the Python block. You didn’t configure a
runtime, set up interop, or write glue code. BOA runs each block as a separate process using its
declared runtime. Node.js blocks run with node, Python blocks run with
python. They all speak the same JSON protocol.
This means you (or your AI) can pick the best language for each task without any integration overhead.
Step 1: Create the Python Block
Scaffold a new domain block with the Python runtime:
bashboa block create ComputeTax --layer domain --runtime python
This creates src/DomainBlocks/ComputeTax/ with stubs for block.boa and
main.py.
Step 2: Define the Block Manifest
block.boa
block.boaBLOCK ComputeTax 1.0.0
LAYER domain
RUNTIME python
ENTRY main.py
DESC Computes tax and adds it to the bill.
INTENT Calculate sales tax on a restaurant
bill before computing the tip.
RULE tax = billAmount * taxRate.
RULE billWithTax = billAmount + tax.
RULE Round both to 2 decimal places.
RULE taxRate must be between 0 and 1.
TAGS tax, billing, python, domain
IN billAmount:number!
IN taxRate:number!
OUT tax:number
OUT billWithTax:number
FIXTURE {"billAmount":100,"taxRate":0.08}
-> {"tax":8.0,"billWithTax":108.0}
FIXTURE {"billAmount":85.50,"taxRate":0.08}
-> {"tax":6.84,"billWithTax":92.34}
ERR ValidationError
main.py
Pythonimport sys
import json
def main():
raw = sys.stdin.read()
envelope = json.loads(raw)
inp = envelope["input"]
bill = inp["billAmount"]
rate = inp["taxRate"]
if not isinstance(bill, (int, float)) \
or not isinstance(rate, (int, float)) \
or rate < 0 or rate > 1:
json.dump({
"success": False,
"error": {
"type": "ValidationError",
"message":
"Invalid inputs"
}
}, sys.stdout)
return
tax = round(bill * rate, 2)
total = round(bill + tax, 2)
json.dump({
"success": True,
"output": {
"tax": tax,
"billWithTax": total
}
}, sys.stdout)
if __name__ == "__main__":
main()
Same protocol, different language
Compare this Python block to the Node.js blocks from Part 1. The structure is identical: read JSON from
STDIN, extract input, process, write JSON to STDOUT. The URP contract doesn’t mention
any language — it only says “JSON in, JSON out.” This is what makes polyglot possible
without adapters, bridges, or FFI.
Step 3: URP Side-by-Side Comparison
Here’s the same protocol implemented in both languages:
Node.js (CalculateTip)
TypeScript// 1. Read STDIN
const chunks: Buffer[] = [];
for await (const chunk of process.stdin)
chunks.push(chunk as Buffer);
// 2. Parse envelope
const envelope = JSON.parse(
Buffer.concat(chunks)
.toString("utf-8")
);
const input = envelope.input;
// 3. Process
const result = /* ... */;
// 4. Write STDOUT
console.log(JSON.stringify({
success: true,
output: result
}));
Python (ComputeTax)
Python# 1. Read STDIN
raw = sys.stdin.read()
# 2. Parse envelope
envelope = json.loads(raw)
inp = envelope["input"]
# 3. Process
result = # ...
# 4. Write STDOUT
json.dump({
"success": True,
"output": result
}, sys.stdout)
Four steps in any language: read, parse, process, write. That’s the entire integration contract.
Step 4: Register and Test
Add the new block to blocks.boa:
blocks.boaCalculateTip 1.0.0 -> src/DomainBlocks/CalculateTip
SplitBill 1.0.0 -> src/Primitives/SplitBill
ComputeTax 1.0.0 -> src/DomainBlocks/ComputeTax
Test the Python block (no compilation step needed — Python is interpreted):
bashboa test ComputeTax@1.0.0
ComputeTax@1.0.0
Fixture 1: PASS
Fixture 2: PASS
2 fixtures passed, 0 failed
.ts to .js, Python blocks run directly.
The framework reads RUNTIME python from the manifest and invokes python main.py
instead of node index.js. Each language uses its native toolchain.
Step 5: Rewire the Workflow
Update workflows/tip-calculator/workflow.boa to insert the tax step before tip
calculation:
workflow.boaWORKFLOW TipCalculator 2.0.0
DESC Calculate tax, tip, and split the bill among people.
# Step 1: Compute tax (Python)
STEP tax = ComputeTax@1.0.0
MAP billAmount <- _initial.billAmount
MAP taxRate <- _initial.taxRate
# Step 2: Calculate tip on bill+tax (Node.js)
STEP calc = CalculateTip@1.0.0
MAP billAmount <- tax.billWithTax
MAP tipPercent <- _initial.tipPercent
# Step 3: Split the final total (Node.js)
STEP split = SplitBill@1.0.0
MAP total <- calc.totalWithTip
MAP people <- _initial.people
What changed
| Part 1 (v1.0.0) | Part 2 (v2.0.0) |
|---|---|
CalculateTip reads _initial.billAmount |
ComputeTax reads _initial.billAmount |
| — | CalculateTip now reads tax.billWithTax (bill + tax) |
| 2 steps, 1 language | 3 steps, 2 languages |
Existing blocks are untouched
Neither CalculateTip nor SplitBill were modified. We only changed the workflow
wiring. CalculateTip still takes billAmount and tipPercent — it
doesn’t know or care that the billAmount now includes tax. This is the power of
explicit contracts: as long as the input types match, blocks are interchangeable.
Step 6: Update project.boa
project.boaPROJECT TipCalculator 2.0.0
DESC Calculates tax, tips, and splits bills among people.
DOMAIN Billing
ComputeTax: Calculates sales tax on a bill (Python)
CalculateTip: Computes tip amount and total with tip (Node.js)
SplitBill: Divides a total evenly among people (Node.js)
FLOW TipCalculator: ComputeTax -> CalculateTip -> SplitBill
Step 7: Run and Validate
Update input.json with the new taxRate field:
input.json{
"billAmount": 85.50,
"taxRate": 0.08,
"tipPercent": 18,
"people": 3
}
bashboa run workflows/tip-calculator/workflow.boa --input-file workflows/tip-calculator/input.json
# Step execution trace:
# 1. ComputeTax (python) → tax: 6.84, billWithTax: 92.34
# 2. CalculateTip (node) → tipAmount: 16.62, totalWithTip: 108.96
# 3. SplitBill (node) → perPerson: 36.32
{
"perPerson": 36.32
}
bashboa validate
Blocks: 3 registered
Fixtures: 6 passed, 0 failed
Workflows: 1 valid
Status: OK All valid
Updated Project Structure
project.boa ← v2.0.0 now
blocks.boa ← 3 blocks registered
src/
DomainBlocks/
CalculateTip/ ← Node.js (unchanged)
block.boa
index.ts / index.js
ComputeTax/ ← Python (NEW)
block.boa
main.py
Primitives/
SplitBill/ ← Node.js (unchanged)
block.boa
index.ts / index.js
workflows/
tip-calculator/
workflow.boa ← v2.0.0, 3 steps now
input.json ← added taxRate field
Why Polyglot Matters
| Benefit | Explanation |
|---|---|
| Best tool for the job | Use Python for data science, Node.js for APIs, Go for performance — in the same project |
| Team flexibility | A Python team and a Node.js team can contribute blocks to the same workflow |
| Zero integration code | No REST adapters, no message queues, no gRPC stubs — just STDIN/STDOUT JSON |
| AI-friendly | AI agents can generate blocks in whatever language best fits the task. The workflow doesn’t change. |
| Independent deployment | Replace a Node.js block with a Rust rewrite — same manifest, same contract, zero workflow changes |
RUNTIME and ENTRY in the
manifest, and the framework handles execution.