What the language guarantees
- Programs always finish
- No floating point: money is exact 128-bit arithmetic, and overflow is an error, never a silent rounding
- Allocation is exact: the parts always sum to the whole
- The same inputs always give the same result
- A permission is checked before a command's body runs
Every example below is real Cambium and passes the language's checker as written.
Example 1: a package and its settings
A package names itself, its version and the packages it relies on:
[package]
name = "partner_payouts"
version = "1.4.0"
kind = "package"
[requires]
ledger = "1"
sales = "1"Then it declares what an organisation may tune — each setting with its type, range and default:
enum PayoutMode { Manual, Automatic }
config payout {
rate: ratio { range: 0.01..=0.3, step: 0.001, default: 0.05 },
wait: duration { range: 1d..=30d, default: 3d },
minimum: ratio { range: 0..=1000, default: 50 },
mode: PayoutMode { default: Manual },
} ensure mode is Manual or wait >= 7d
pub config referral {
inviter_points: ratio { range: 0..=10000, default: 500 },
friend_discount: ratio { range: 0..=0.5, default: 0.1 },
window: duration { default: 180d },
}From these lines the platform builds the settings page with its limits, the right to edit it, input checks and a change log with author and date. The last line is a rule across fields: automatic payouts must wait at least a week.
Example 2: B2C loyalty — cashback, bonuses, limits
Cashback by receipt size, a bonus for reviews with a cap of five a month, and a limit on paying with points:
config redeem {
share_max: ratio { range: 0..=1, default: 0.3 },
}
/// Cashback by receipt size: the first true line wins.
rule tiered_cashback on sales::receipt.closed as receipt {
params {
bonus_from: ratio { default: 200 },
bonus_points: ratio { default: 500 },
high_from: ratio { default: 100 },
high_rate: ratio { range: 0..=0.5, default: 0.1 },
mid_from: ratio { default: 50 },
mid_rate: ratio { default: 0.05 },
min_total: ratio { default: 15 },
base_rate: ratio { default: 0.03 },
}
when receipt.total >= params.bonus_from => points::bonus(receipt.member, params.bonus_points, lot: LotSpec {
pool: Base,
expires: None,
});
when receipt.total > params.high_from => points::rate(receipt, params.high_rate);
when receipt.total >= params.mid_from => points::rate(receipt, params.mid_rate);
when receipt.total < params.min_total => skip;
else => points::rate(receipt, params.base_rate);
}
/// A verified review with a photo earns 50 points, at most 5 times a month.
rule review_bonus on reviews::published as published where published.verified and published.has_photo {
params {
points: ratio { default: 50 },
max_per_month: u64 { default: 5 },
}
progress per published.member within calendar_month { n: u64 }
when progress.n < params.max_per_month => points::bonus(published.member, params.points, lot: LotSpec {
pool: Promo,
expires: None,
}), progress.n.inc(1);
else => skip;
}
/// Points may pay for at most 30 % of a receipt.
policy points::redeem(r: sales::Receipt, amount: ratio) -> bool {
amount <= r.total * config.redeem.share_max
}Lines are checked top to bottom and the first true one wins. The monthly cap is counted per member and starts again each calendar month. Every number is a setting that marketing changes without a programmer.
Example 3: B2B terms — volume tiers and credit
A quarterly volume scale paid as a credit note, a credit limit that grows after a year of on-time payments, and a block on offers while a dealer is over the limit:
query quarter_volume(d: Id<b2b::Dealer>, q: period) -> ratio {
b2b::invoice.filter(|i| i.dealer == d and i.closed_at in q).sum(|i| i.total)
}
/// Volume scale: from 500 000 a quarter 4 %, from 1 000 000 6 %, paid as a credit note.
rule retro_scale every quarter for d in b2b::dealer {
params {
tier1_from: ratio { default: 500_000 },
tier1: ratio { range: 0..=0.2, default: 0.04 },
tier2_from: ratio { default: 1_000_000 },
tier2: ratio { range: 0..=0.2, default: 0.06 },
}
let volume = quarter_volume(d.id, period.previous_quarter());
when volume >= params.tier2_from => ledger::credit_note(d.account, volume * params.tier2, key: (RetroScale, d.id, period.previous_quarter()));
when volume >= params.tier1_from => ledger::credit_note(d.account, volume * params.tier1, key: (RetroScale, d.id, period.previous_quarter()));
}
/// A full year of NET 30 invoices paid on time raises the credit limit by 25 %.
rule flawless_net30 every year for d in b2b::dealer {
params {
raise: ratio { range: 0..=1, default: 0.25 },
}
when b2b::invoice.filter(|i| i.dealer == d.id and i.due_at in period.previous_year()).all(|i| i.paid_at is Some(p) and p <= i.due_at)
=> b2b::dealer.set_credit_limit(d.id, d.credit_limit * (1 + params.raise), key: (CreditRaise, d.id, period.previous_year()));
}
/// Over the credit limit: no discounts and no bonuses until the balance is back.
policy offers_allowed(d: b2b::Dealer) -> bool {
b2b::balance(d.id) <= d.credit_limit
}Each credit note carries a key — the dealer and the quarter — so a repeated run never pays twice.
Example 4: a bonus pool
A decision table sets each partner's weight by tier and volume; the first matching row wins. A query computes the pool: paid orders in the period times the share that was in force at the start of that period.
/// A partner's weight in the pool: tier and personal volume for the period.
decision weight(tier: Tier, volume: ratio) -> u8 hit first {
| tier | volume | => weight |
| crm::Tier.Platinum | >= 10_000 | => 3 |
| crm::Tier.Gold | >= 5_000 | => 2 |
| crm::Tier.Gold | _ | => 1 |
| _ | >= 1_000 | => 1 |
| _ | _ | => 0 |
}
/// The pool: paid turnover of the period times the share in force when the period began.
pub query pool_total(p: period) -> ratio = (sales::Order where status is Paid and paid_at in p sum(total)) * config.pool.as_of(p.start).share;The command that pays the pool out requires a permission, runs at most once per period and splits the pool to the cent — the parts always add up to the pool:
/// Pays the pool out to the cent: the parts always add up to the pool, in one batch of transfers.
pub cmd distribute(p: period) -> Result<Id<ledger::Batch>, ledger::TransferError> permission bonus.distribute idempotent by p {
let members = participants(p);
let parts = allocate(pool_total(p), members.map(|m| m.w), 2);
ledger::batch(members.zip(parts).map(|(m, amt)| ledger::leg(ledger::Party.Kind {
holder: "bonus_pool",
kind: "pool",
}, ledger::Party.Account { account: m.bonus_account }, ledger::amount(amt, ledger::Asset.Points {
pool: "bonus",
}), bonus.pool { period: p, member: m.id })))
}Example 5: how a status may change
A status machine lists the allowed moves between states. For each move it says how the move may happen: on an event, or by a right — a person moves it by hand, for example with a button in the interface. The choice is made per move. A condition (ensure) can guard a move of either kind.
machine Order.status {
Draft -> Placed permission order.place
ensure lines.len() > 0 else (err.empty { order: id });
Placed -> Paid on payments::payment.received as p
where p.order == id;
Paid -> Shipped permission order.ship { emit order.shipped { order: id }; }
Paid -> Refunded permission order.refund;
Placed -> Cancelled permission order.cancel;
}- Draft → Placed — by hand, with the place right, and only if the order has lines; otherwise the move is refused with a reason naming the order
- Placed → Paid — on an event: the payment for this order is received
- Paid → Shipped — by hand, with the ship right; the move also announces the shipment to everyone who listens
- Paid → Refunded and Placed → Cancelled — by hand, each with its own right
The same works for any entity. Here _ means «from any state»: a member can be banned or suspended whatever their status, but only someone with the right can bring a suspended member back:
machine Member.status {
_ -> Banned permission admin.ban; // from any state
_ -> Suspended permission admin.suspend;
Suspended -> Active permission admin.resume;
}A manual move names only the target state. The machine finds the allowed move from the current state, checks the right and the condition, and only then changes the status:
pub cmd ship(o: Id<Order>) -> Result<(), err.empty> permission order.ship idempotent by o {
Order.move(o, Shipped)?; // manual move: target state; the machine finds the edge
Ok(())
}What is not declared cannot happen at all, whatever rights a user holds: no right turns a shipped order back into a draft, because that move does not exist. Writing the status field directly is refused as well — every change goes through the machine. Every move also raises a status-changed event with the old and the new state, for reports, notifications and audit.
Example 6: permissions and session trust
Rights are declared once per package, in groups. Each right also states the lowest session trust it needs, from anonymous up to a qualified signature:
/// Rights in groups; each names the lowest session trust it needs.
permissions {
payout level strong {
read level identified,
edit,
approve implies read,
},
all_regions,
director level proven,
}
config payout {
approve_limit: Amount<usd> { default: 10_000 },
}
entity Payout {
region: Region,
status: PayoutStatus,
amount: Amount<usd> write permission payout.edit,
note: string,
}
/// Approval is a manual edge: a right and a condition; above the limit the director must join.
machine Payout.status {
Draft -> Approved permission if amount < config.payout.approve_limit { payout.approve } else { payout.approve and director }
ensure amount > 0 usd and note != "" else (err.payout.state);
}
/// Row policy: a user sees only the payouts of their own regions.
policy payout.read(p: Payout, actor: Actor) -> bool {
actor.regions.contains(p.region) or actor.has(all_regions)
}
pub query payouts_due() permission payout.read = Payout where status is Draft sum(amount);- Reading payouts needs an identified session; approving needs a second factor; the director right needs identity proven with a document
- Whoever may approve may also read
- The amount can be changed only with the edit right
- Above the configured limit, approval needs the director as well
- The row policy limits every query to the user's regions: the total above has no filter of its own and still returns only what this user may see
- Approval is a manual move: a person with the right moves the payout to Approved, and only if the amount is positive and a note explains it. The same move could instead be bound to an event — that is a choice made per move
Session trust is a separate axis from rights. Lacking the right is a final «no». Lacking trust is not: the answer comes with a ready form — «confirm with a stronger factor» — in the same response. An organisation may raise the required level, never lower it.
Where Cambium is used
- Partner-network volume, payouts and bonus pools
- Loyalty programmes, B2B terms and credit
- Account roll-ups, bank files and licence expiry
- Order workflows and row-level access
- Warehouse processes
Short programs are pleasant, but line counts are the weakest argument. The real difference is the guarantees: exact money, programs that finish, and the same answer every time.