Going Polyglot

Extend the Tip Calculator with a Python block. Mix Node.js and Python in the same workflow — the framework handles everything.

Intermediate Node.js + Python Builds on Part 1 ~10 minutes
Prerequisites This tutorial builds on the Tip Calculator (Part 1). You should have a working project with 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.

Same approach as Part 1 You describe the change in plain English. The AI assistant adds the new block in Python, updates the workflow, and everything just works.

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.

What changes from Part 1 We’re adding a single Python block and re-wiring the workflow. The existing Node.js blocks are untouched. This demonstrates the core polyglot principle: the Universal Runtime Protocol (URP) makes language irrelevant at the integration layer.

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
No compilation for Python Unlike Node.js blocks where you compile .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

TipCalculator/
  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
Adding more languages The same pattern works for any language: Go, Rust, Java, Ruby, shell scripts. If it can read STDIN and write JSON to STDOUT, it’s a valid BOA block. Set RUNTIME and ENTRY in the manifest, and the framework handles execution.