Computations and Mutations
derive and query give you reasoning. Most real ontologies need two more things: pure functions over the data (a lease’s days-until-expiry, the total rent owed) and operations that change the world (signing the lease, expiring it). Argon has dedicated item kinds for both.
compute is the function form: input → output, no side effects, fully evaluable as an expression. mutation is the state-changing form: imperative body, audit-trailed events, with bitemporal valid-time and transaction-time semantics modeled at the language surface and at the kernel API level. (ox mutate accepts --principal, --standpoint, --valid-at, and --idempotency-key; the in-process runtime applies them against the current knowledge store, while a fully durable bitemporal log lives in the kernel API tier — see Part 4.)
This chapter teaches both. The lease-tutorial examples below model parties as UFO kind / role concepts — those metatypes are user-declared via the substrate of Chapter 2.1, not built into the language; computations and mutations themselves are vocabulary-neutral.
compute — pure functions
The simplest compute is one expression:
pub compute annual_rent(l: Lease) -> Money =
l.monthly_rent * 12
Read in English: “the function annual_rent, given a Lease, returns twelve times its monthly rent.”
A few mechanics:
computeis a hard-reserved keyword. Likederiveandmutation, the name introduces a kind of item, not a type.-> Moneyis the return type. Argon will not infer it; computes always declare their return type explicitly. This is intentional — the return type is part of the function’s contract.= expris the body. A single expression. No statements, no early returns, no side effects.
The body uses ordinary arithmetic over the primitive types (Int, Decimal, Money) and the operators in Appendix B. Date arithmetic — durations between dates, .days-style accessors, calendar-aware arithmetic — lives in domain packages today; the language core ships only the primitive types.
Three forms
compute admits three body shapes, each useful in different situations.
Form 1 — single expression
pub compute total_rent(l: Lease) -> Money =
l.monthly_rent * months_between(l.start_date, l.end_date)
The most common form. Reads like a definition; lifts trivially into rule bodies; participates in the meta-property calculus and the tier classifier.
Form 2 — block with a body expression
When the computation needs intermediate bindings:
pub compute deposit_after_deductions(d: SecurityDeposit) -> Money {
body {
let allowed = sum(x.amount for x in d.deductions where x.is_allowed)
let disallowed = sum(x.amount for x in d.deductions where not x.is_allowed)
d.held_amount - allowed
}
}
The body { let ... ; let ... ; <expr> } block carries let-statements (intermediate bindings, never side-effects) followed by a trailing expression that is the computed value. Form 2 looks imperative but evaluates as a pure expression — the engine inlines the lets and reads the trailing expression as the result.
Use Form 2 when an intermediate name makes the computation readable. When you find yourself writing five lets in a row, think about whether the intermediate values deserve to be their own computes — small composable functions read better than big block bodies.
Form 3 — Rust FFI
Some functions cannot be expressed in Argon — cryptographic primitives, regex matching, OS clock access. For those:
pub compute hash_id(input: String) -> String {
impl rust("crypto_helpers::sha256_hex")
}
The impl rust("path::to::function") clause delegates to a Rust function bundled with your toolchain. Form-3 computes are opaque to the reasoner — the type-checker still validates the signature, but the body is dispatched through the runtime’s FFI surface. Use them sparingly; every Form-3 binding is a place the reasoner cannot see.
Tip: if a Form-3 compute crops up in a hot rule body, look for a way to refactor: either lift the rust call to a precomputed field (an asserted fact rather than a derived one), or rewrite the rule to keep the FFI off the inner loop.
Compositional bodies
Compute bodies admit the full expression vocabulary: arithmetic, if/else, match, aggregates, list literals, function calls. Tier-classification composes by taking the maximum tier across components, so if and match do not introduce new tiers — they propagate the tier of their branches.
Status note:
if-condition type rule. The type rule forif-conditions (that they must beBool-typed) is part of the upcoming Appendix B operator audit. Today’s elaborator accepts non-Boolconditions likeif s.rank { … }wheres.rank: Nat; tracked at sharpe-dev/orca-mvp#412.
pub compute lease_status(l: Lease) -> String =
if l.end_date <= today() {
"expired"
} else if l.start_date > today() {
"pending"
} else {
"active"
}
pub compute lease_summary(l: Lease) -> String =
match l {
ResidentialLease(r) => "Residential lease, " + r.bedrooms + " bedroom",
CommercialLease(c) => "Commercial lease, use: " + c.permitted_use,
_ => "General lease",
}
Aggregates lift naturally:
pub compute total_collected(l: Lease) -> Money =
sum(p.amount for p in l.payments where p.cleared)
The for var in path where atom clause filters the iteration. The filter atom is the same predicate vocabulary as rule bodies.
mutation — operations that change the world
Where compute produces a value, mutation changes the model. A mutation can update fields, retract derived facts, emit typed events into the bitemporal log, and return a value.
pub mutation sign_lease(l: Lease, signed_at: Date) -> Lease {
require {
l.start_date < l.end_date
}
do { }
emit LeaseSigned(l, signed_at)
return l
}
Five clauses, all but do optional:
| Clause | Cardinality | Purpose |
|---|---|---|
require { body } | optional, once | Precondition — body must hold for the mutation to fire. |
retract { stmts } | optional, once | Facts to remove from the knowledge base. |
do { stmts } | mandatory, once | Field updates and intermediate lets. |
emit <expr> | repeats | Events to publish into the axiom-event log. |
return <expr> | optional, once | Return value (matches the declared -> T). |
The recommended ordering is the table above. The parser is order-permissive — you can write emit before do — but the convention reads better in order.
A worked example using all five clauses. The retract clause removes facts from the knowledge base before the new state asserts; emit publishes the supersession event into the bitemporal log:
pub mutation correct_rent(l: Lease, new_rent: Money) -> Lease {
require {
new_rent > 0,
l.monthly_rent != new_rent
}
retract {
MonthlyRent(l, l.monthly_rent)
}
do {
l.monthly_rent = new_rent
}
emit RentCorrected(l, new_rent, today())
return l
}
Variants: _catalog/mutation-retract-clause/{minimal,composition,bitemporal,idiom}/. The catalog’s idiom/ variant pairs retract with a follow-up emit of a supersession axiom — the canonical pattern for keeping the bitemporal log audit-honest when a value is corrected.
Migration note: the team is exploring removing the
dokeyword: a mutation body would just be raw statements, withlets and field updates appearing directly. Theemitclauses stay where they are. Today’sdo { }block continues to compile; the migration will land additively. See Appendix D.
Field updates
The do { } block is where state actually changes:
pub mutation pay_rent(l: Lease, amount: Money) {
require {
amount > 0,
amount <= l.monthly_rent
}
do {
let new_balance = l.balance + amount
l.balance = new_balance
}
emit PaymentReceived(l, amount, today())
}
Two statement forms inside do { }:
let name [: Type] = expr— bind an intermediate value (not a state change).<field-path> = expr— set a field value (state change).
There is no compound assignment (+= or similar) yet. The Phase-B agenda has compound-assignment as an open ergonomics item; for now, write the equivalent: l.balance = l.balance + amount.
Events
emit Expr publishes the value of Expr into the bitemporal axiom-event log. The engine wraps the emission with three pieces of metadata: a valid time (when the event becomes true in the world), a transaction time (when the system learned of it), and a principal (which agent caused the emission, supplied via the --principal flag on ox mutate).
Events are first-class typed values. A user-defined event:
pub kind PaymentReceived {
lease: Lease,
amount: Money,
received_at: Date,
}
Then emit PaymentReceived(l, amount, today()) constructs an instance and publishes it. The runtime’s bitemporal storage treats it as an append-only fact.
Migration note: events are moving toward being a first-class language primitive — a dedicated
eventitem kind that ties consequences in rules and mutations through the same declared event types. Today, declare the event as akindwith the relevant fields; the migration will introduceeventdeclarations as a richer alternative. See Appendix D.
Bitemporal axes — what the runtime gives you
Every event in the log carries two times. Concretely:
- Valid time — the time at which the event is true in the world. By default,
today()at the moment of emission, but you can override withox mutate --valid-at #2026-01-01#. - Transaction time — the time at which the system learned of the event. Always the wall clock at emission; never overridable.
Queries can dial in either axis:
$ ox query active_leases --as-of-valid #2026-04-01#
What was active on April 1?
$ ox query active_leases --as-of-tx #2026-04-15#
What did we know was active, as of the database state on April 15?
--as-of-valid is “go back in time in the world.” --as-of-tx is “go back in time in the database.” They are independent: you can ask “as of April 15 in the database, what was active on April 1 in the world?” by combining them.
This is built in. You do not write any of it.
Forks
Sometimes you want to reason in a what-if world without mutating the canonical one. Argon’s runtime gives you forks:
$ ox mutate sign_lease --fork experimental --principal user-1 --arg l=lease-001
Mutation applied to fork=experimental.
$ ox query active_leases --fork experimental
...sees the signed lease in the experimental fork only.
A fork is a structural-shared branch of the world. Mutations against a fork do not flow back to MAIN unless you explicitly promote them. We will see more of this in Chapter 5.4; here the point is just that mutations are fork-aware.
Reading the consequences
ox mutate --explain shows the mutation’s effects:
$ ox mutate sign_lease --principal user-1 --arg l=lease-001 --arg signed_at=#2026-01-15# --explain
Preconditions:
✓ l.start_date < l.end_date
Field updates:
(none)
Events emitted:
LeaseSigned(lease-001, #2026-01-15#)
valid_time = #2026-01-15#
tx_time = #2026-04-24T10:32:11Z#
principal = user-1
Returned: lease-001
--dry-run runs the mutation without committing it — useful for testing whether preconditions hold and what events would land.
Edge cases worth knowing
computeForm-2 cannot emit events. That is the line betweencomputeandmutation: computes are referentially transparent.requireis a precondition, not a guard. Ifrequire’s body does not hold, the mutation does not fire and an error is reported. It is not “skip silently.”mutationitems must bepubto be runtime-dispatchable. The CLI dispatches by qualified name; non-pubmutations are file-scoped and unreachable from outside.--principalis mandatory onox mutate. The runtime refuses to apply a mutation without an attribution. Set it; pass it; track it. The audit trail is the point.- Event types should be declared as concepts. Inline
emitof an ad-hoc tuple loses you typed-querying later. Declare akindfor every event you emit; it pays back the keystrokes immediately.
Putting it in the running example
Add src/computes.ar:
use lease::*
pub compute annual_rent(l: Lease) -> Money =
l.monthly_rent * 12
Add src/mutations.ar:
use lease::*
pub kind LeaseSigned {
lease: Lease,
signed_at: Date,
}
pub kind LeaseExpired {
lease: Lease,
effective_at: Date,
}
pub mutation sign_lease(l: Lease, signed_at: Date) -> Lease {
require {
l.start_date < l.end_date
}
do { }
emit LeaseSigned(l, signed_at)
return l
}
pub mutation expire_lease(l: Lease, effective_at: Date) -> Lease {
do { }
emit LeaseExpired(l, effective_at)
return l
}
Update prelude.ar:
pub use metatypes::*
pub use party::*
pub use lease::*
pub use rules::*
pub use queries::*
pub use computes::*
pub use mutations::*
Then:
$ ox check
Checking lease-tutorial v0.1.0
Finished in 0.12s
The model now derives, queries, computes, and mutates. We have all three computational modes wired up.
Summary
compute is for pure functions; mutation is for state changes. compute has three body forms — single expression, block, and Rust FFI — picked by what the function does. mutation has five clauses — require, retract, do, emit, return — and operates against a bitemporal, fork-aware, audit-trailed runtime. The lease tutorial now signs and expires leases. Next we tighten the type system around the model.