Developer docs menu

Formula language

The small, sandboxed expression language for pricing rules a form cannot express: syntax, functions, variables, hook points and a cookbook.

This page is the reference for Quotometry’s formula language, which you use for the few pricing rules that a number in a form cannot express: “double the setup charge under five pieces”, “add a handling fee that scales with the bounding box”, “send anything over 400 mm tall to review”.

Formulas are written in the configuration editor, type-checked when you save, and compiled once when the configuration is published. A configuration that refers to a variable that does not exist cannot be published, so the mistake is caught at your desk, not in front of a buyer.

Before you begin

  • A plan that includes formulas.
  • A draft configuration to edit, and a part to try it on in the sample-quote panel.

The language

A subset of CEL and nothing more. There are no loops, no I/O, no clock and no randomness. An expression is at most 4 KiB, nests at most 32 levels deep, and runs on a budget of 10 000 operations; a formula that would not finish is refused rather than run.

  • Arithmetic + - * / %, comparison < <= > >= == !=, logic && || ! and the ternary cond ? a : b.
  • Membership: sel.material in ["petg", "asa"]. in is an operator, not a function.
  • Numbers are exact, and equality on them is exact. Strings have no escape sequences.
  • Division by zero, NaN and infinity are errors, not values.

Functions

FunctionReturns
min(a, b, …), max(a, b, …)The smallest or largest argument.
abs(x)The absolute value.
ceil(x), floor(x)x rounded up or down to a whole number.
round(x[, places])x rounded to a whole number, or to places decimal places.
sqrt(x), ln(x), exp(x), pow(a, b)Square root, natural logarithm, e to the power x, a to the power b.
clamp(x, low, high)x, limited to the range from low to high.
interp(x, table.name)Linear interpolation between the rows of a table you defined in the configuration.
tier(x, table.name)Step lookup: the value of the last row whose threshold x has reached.

Variables

Every variable below is available to every line-level formula:

GroupVariables
Quantityqty
Geometrypart.volume_cm3 part.area_cm2 part.bbox.x part.bbox.y part.bbox.z (mm) part.bbox_volume_cm3 part.hull_volume_cm3 part.complexity part.mass_g
Estimateest.material_g est.support_g est.machine_h est.labour_h est.batches est.parts_per_batch est.confidence
Costcost.material cost.machine cost.labour cost.subtotal
Priceprice.before: the computed unit price, in the hooks that adjust it

Each configuration adds its own:

  • sel.<option id>: what the buyer chose. A dropdown is text, a toggle is a boolean, and a count or a slider is a number, so sel.infill > 40 and sel.material == "petg" type-check only where they make sense.
  • const.<name> and table.<name>: your own constants and lookup tables.

cost.* variables are costs, not prices. They exist so you can write a rule about your own economics; nothing computed from them reaches a buyer except as part of the final price.

Where formulas hook in

HookTypeWhat it does
adjust_unit_pricenumberMultiplies the computed unit price. 1 changes nothing.
override_unit_pricenumberReplaces the unit price outright. Use it sparingly: everything before it still runs.
extra_lines.<id>numberAdds a priced line with that id, for the whole line rather than per piece. The buyer sees it if your visibility settings allow.
Option chargesnumberA charge computed instead of a fixed amount.
Review rulesbooleantrue sends the line to a person at the shop.

Cookbook

A setup surcharge that fades out with quantity

adjust_unit_price:  qty < 5 ? 1.35 : qty < 20 ? 1.15 : 1.0

A handling fee that depends on the part’s footprint

extra_lines.handling:  max(2.50, part.bbox.x * part.bbox.y / 10000 * 3.00)

Charge more for a material you dislike running

adjust_unit_price:  sel.material in ["tpu", "pc"] ? 1.25 : 1.0

A discount table you can edit without touching the formula

Define a table volume_discount with the rows (10, 0.95), (50, 0.90) and (200, 0.85), then:

adjust_unit_price:  tier(qty, table.volume_discount)

tier steps: 49 pieces get 0.95 and 50 get 0.90. Use interp when you want the value to slide smoothly between rows instead.

Send tall, thin parts to a person

review rule:  part.bbox.z > 400 && part.bbox.x < 60 && part.bbox.y < 60

Recover a fixed cost per batch rather than per part

An extra line is an amount for the whole line, so charge the batch cost once per batch:

extra_lines.batch_setup:  est.batches * 8.00

Refuse to price a part automatically when the estimate is uncertain

review rule:  est.confidence < 0.6

Test a formula before anyone sees it

The configuration editor’s sample-quote panel prices a real part against your draft on every change. On plans that include it, the backtest re-prices up to your last 500 quote lines with the draft and shows which ones move and by how much. Write the formula, watch the number, then publish.

Next steps