Guarantees
What is atomic, what runs once, what is live, and what you design for yourself.
runtime.tx and runtime.query; today the same guarantees apply to await runtime.db() with db.transact and db.query.This page is the contract. If your code relies on something not listed here, it relies on luck.
At a glance
| Guaranteed | Not guaranteed | |
|---|---|---|
One runtime.tx | all its operations apply, or none do | isolation from concurrent reads |
| One action | input and output validated; runs once per request id; caller recorded | that its separate tx calls are atomic together |
.unique() | enforced by the database on every write | — |
| Permissions | every read and write of a production action runs under the caller's rules | — |
| Live queries | every commit, by any writer, refreshes affected subscribers | ordering between two different queries |
| Workflow steps | a finished step never runs again | that a running step runs only once |
A tx is the transaction
await runtime.tx((tx) => [
tx.accounts_account[from].update({ balance: fromBalance - amount }),
tx.accounts_account[to].update({ balance: toBalance + amount }),
]);Both updates commit together or not at all. Two runtime.tx calls are two transactions: if the second fails, the first stays. Inside one tx, if two operations set the same attribute of the same object, the last one wins.
Reading does not lock
runtime.query reads committed data; it does not hold it. Between your read and your tx, another action can change it. A check-then-write is only correct if the database can enforce the condition itself:
// ❌ two concurrent calls can both see "none" and both create
const { orders_customer } = await runtime.query({
orders_customer: { $: { where: { email: input.email } } },
});
if (orders_customer.length === 0) {
await runtime.tx((tx) => tx.orders_customer[id()].update({ email: input.email, name: input.name }));
}
// ✅ `email` is .unique(); this creates or updates the one customer
await runtime.tx((tx) =>
tx.orders_customer[lookup("email", input.email)].update({ name: input.name }),
);For counters and balances, prefer appending over overwriting: record each movement as its own object and derive the total with a query.
tx that commits only if what you read has not changed — are designed, not built.An action runs once per request
| Same request id, same input | the first execution is returned; your code does not run again |
| Same request id, different input | REQUEST_CONFLICT |
| Your code throws | the execution fails with action_failed; committed tx calls stay |
| Your code returns an invalid output | the execution fails with execution_output_invalid; committed tx calls stay |
| The process running it dies | the execution is marked failed, not re-run |
A failed execution may have done part of its work. Keep each action's writes in one tx, and make a retry with a new request id harmless with caller-chosen ids or unique attributes.
Permissions apply inside actions
Reads and writes inside an action run with the caller's identity, in development and in production: a user's action cannot read or write what that user could not. A write the rules forbid fails the execution with action_failed; writes committed before it stay. Only a service key bypasses rules. See Permissions.
Live queries follow commits
Subscriptions refresh from the database's change log. A commit by an action, a workflow step or any other writer refreshes every affected query, in every tab, for every user who may see the result.
Workflows
A finished step is saved and never runs again. A step that was running when the process stopped runs again, so steps run at least once. A run keeps the identity it was admitted with and the release it started on. See Workflows.
Stale runners cannot write
Each execution attempt carries a fence. If an attempt is superseded — its 30-second lease expired and another took over — writes from the old one are rejected.