← Blog

Kumi: Calculation Logic in Ruby That Also Runs in JavaScript

Many of my projects are webshops and billing systems. The same problem comes up in every one of them: the price logic lives on the server, but the customer wants to see the total in the browser while they change quantities. So the rules get written twice, once in Ruby and once in JavaScript, and sooner or later the two versions disagree.

Kumi by André Muta is a Ruby gem that takes this problem seriously. It is a declarative DSL for calculation logic, such as tax rules, pricing and scoring, that compiles to plain Ruby and plain JavaScript from one schema. I have been trying it out, and I really like what I see.

What Kumi does

You describe the shape of your input data and the values you want computed. You don’t write loops or decide the evaluation order. The compiler works out the dependencies, checks types and generates straight-line code without dependencies for each target.

Two ideas make it different from writing plain Ruby methods:

Install it with:

gem install kumi

It needs Ruby 3.1 or newer and is MIT licensed. The current version is 0.1.0 from June 2026, and the README describes the project as experimental: the public API may still change.

An example: a Swiss invoice

To try it, I wrote a small invoice. Items with a quantity of 10 or more get a 10% bulk discount, Swiss VAT of 8.1% is added, and the total is rounded to 5 Rappen, as usual for cash amounts in Switzerland.

require "kumi"

module Invoice
  extend Kumi::Schema

  schema do
    input do
      array :items do
        hash :item do
          integer :quantity
          decimal :unit_price
        end
      end
    end

    trait :bulk, input.items.item.quantity >= 10

    let :gross, input.items.item.quantity * input.items.item.unit_price
    value :line_totals, select(bulk, gross * 0.9, gross)
    value :subtotal, fn(:sum, line_totals)
    value :vat, subtotal * 0.081
    # no round() yet: scale to 5 Rappen, add 0.5, truncate
    value :total, fn(:to_integer, (subtotal + vat) * 20 + 0.5) / 20.0
  end
end

result = Invoice.from(items: [
  { quantity: 2,  unit_price: 50.0 },
  { quantity: 12, unit_price: 10.0 }
])

result[:line_totals] # => [100.0, 108.0]
result[:subtotal]    # => 208.0
result[:vat]         # => 16.848
result[:total]       # => 224.85

A few things to notice:

The same logic in JavaScript

This is the part that interests me most for webshops. One call writes a JavaScript module:

Invoice.write_source("invoice.mjs", platform: :javascript)

Every value, let and trait becomes an exported function. This is the generated _total, unchanged:

export function _total(input) {
  let t88 = input["items"];
  let acc95 = 0;
  for (let items_i90 = 0; items_i90 < t88.length; items_i90++) {
    let items_el89 = t88[items_i90];
    let t91 = items_el89["quantity"];
    let t92 = 10;
    let t41 = t91 >= t92;
    let t93 = items_el89["unit_price"];
    let t48 = t91 * t93;
    let t94 = 0.9;
    let t51 = t48 * t94;
    let t59 = t41 ? t51 : t48;
    acc95 += t59;
  }
  let t96 = 0.081;
  let t87 = acc95 * t96;
  let t28 = acc95 + t87;
  let t97 = 20;
  let t30 = t28 * t97;
  let t98 = 0.5;
  let t32 = t30 + t98;
  let t33 = __core_to_integer(t32);
  let t99 = 20.0;
  let t35 = t33 / t99;
  return t35;
}

This is exactly what I want from generated code: a single loop that does the discount, the sum, the VAT and the rounding, with no library and no interpreter behind it. You can read it, step through it in the debugger and ship it as it is. I ran the module with Node.js on the same input and got exactly the same results as in Ruby: [100, 108], 208, 16.848 and 224.85.

Kumi in ruby.wasm

I work a lot with ruby.wasm, for example for Ruby Koans in the browser, so I wanted to know whether Kumi itself also runs inside Ruby compiled to WebAssembly. It does, with a few lines of setup:

# before require "kumi"
ENV["KUMI_PASS_BUDGET_MS"] = "0"   # no threads in WebAssembly: no compile time limit
require "tmpdir"
Dir.mkdir("/tmp") unless Dir.exist?("/tmp")
def Dir.tmpdir = "/tmp"            # a place for Kumi's code cache
require "kumi"

Kumi guards each compiler pass with a time limit based on Timeout, which starts a thread, and ruby.wasm has no threads. The environment variable switches the limit off. Kumi also writes the generated Ruby code to a cache directory before loading it, and in the browser’s in-memory file system Ruby doesn’t find a temporary directory on its own.

With this setup, the invoice example above gives exactly the same results in ruby.wasm, down to the 224.85. I tested it with ruby.wasm 2.10.1 (Ruby 4.0) in Node.js, using the same in-memory file system that ruby.wasm uses in the browser. Kumi is pure Ruby, and its dependencies zeitwerk and mutex_m load without any changes.

So you have two ways to bring your rules to the browser: export them to JavaScript, which is the lightest option, or, if your frontend already runs Ruby in WebAssembly, define and run Kumi schemas right there.

Mistakes are caught early

Kumi analyzes conditions when the schema is defined. If you declare that an age is between 0 and 150 and then write a condition that can never be true, loading the schema fails:

schema do
  input { integer :age, domain: 0..150 }
  trait :impossible_age, input.age == 200
end
# Kumi::Core::Errors::SemanticError: conjunction `impossible_age` is impossible

In business rules, a condition like this is usually a typo or a misunderstanding of the requirements. Kumi tells you the moment the code loads, instead of you finding out months later that a discount never applied. That alone makes it worth a look.

Composing rules

Schemas can import other schemas with import :tax, from: TaxPolicy. A rule that only knows about a single amount can then be applied to every item of an order, and the compiler inlines it into the generated code. The schema imports documentation shows how this works.

Good to know

Try it in the browser

The quickest way to get a feeling for Kumi is the interactive playground. It has a language introduction and larger examples such as a US tax calculator and a payroll run over 10,000 employees.

Kumi fits perfectly wherever the same calculation has to run on the server and in the browser: price calculators, offer configurators, shipping costs. One schema, two languages, identical results. I’m looking forward to using it in my own projects and to seeing where it goes next.