What it is
Because stateledger persists every transition (never mutates in place), you can ask “what was the state of this entity at exactly this moment?” without any extra bookkeeping. The stateAt(at: Date) method walks the transitions table for a subject and returns whichever state was current at the given timestamp.
Basic use
import { invoiceMachine } from "./machines";
const invoice = invoiceMachine.for("inv_0142", { adapter });
// State at some past moment:
const state = await invoice.stateAt(new Date("2026-06-01T14:00:00Z"));
// → "sent" | "paid" | ... | null (if the invoice didn't exist yet)Returns nullwhen the subject had no transitions before the requested timestamp (i.e. it didn’t exist yet as a state-machine entity).
Why this is useful
- • Compliance replays. Regulators or auditors ask: “what state was contract C in on the day it was executed?” Answer with one query, no snapshot table to maintain.
- • Dispute investigation.A customer says: “I got billed even though my subscription was cancelled.” Check
subscription.stateAt(chargeCreatedAt)and settle it. - • Debugging support tickets. User says they saw a “Ship” button when it should have been disabled. Replay the exact state they saw when their session loaded.
- • Reporting. How many invoices were in
sentat midnight on the 31st? Iterate ids and callstateAt.
How the query works
Under the hood, stateAt(t) issues one SQL statement:
SELECT to_state
FROM state_transitions
WHERE machine = $1
AND subject_id = $2
AND transitioned_at <= $3
ORDER BY transitioned_at DESC
LIMIT 1;One index seek. With the shipped schema (indexed by (machine, subject_id, transitioned_at DESC)), this is O(log n) against the transitions table.
Combining with full history
If you want the full trajectory rather than a single point-in-time answer, use readHistory():
const history = await invoice.readHistory();
// [
// { state: "draft", at: 2026-05-01T09:00Z, actor: "user_abc" },
// { state: "sent", at: 2026-05-01T10:30Z, actor: "user_abc" },
// { state: "paid", at: 2026-06-15T14:22Z, actor: "stripe:webhook" },
// ]readHistory() is convenient for admin UIs but reads every row for the subject. For hot paths, prefer stateAt() or readCurrent().Data retention
The transitions table is append-only. For most workloads it grows slowly (one row per state change, not per event) and is safe to keep indefinitely. If you need to prune old data for compliance, delete rows older than N years but keep at least the most recent per subject.