Skip to content

Audit trail and events

Every command writes audit events in the same transaction as the change itself: what happened, who did it (actorId, or null for tick), when, and the details, such as the comment or which approvers were removed and why. The audit trail is append-only.

const history = await rungs.history(request.id);
for (const event of history) console.log(event.seq, event.at, event.actorId, event.type);
// 1 … u_req request.submitted
// 2 … u_req step.activated
// 3 … u_req task.assigned
// 4 … u_mgr task.approved
// …

Runs in CI:examples/docs-snippets/src/concepts/audit.ts

After a command commits, Rungs delivers domain events to onEvent, for example to send an email when a task is assigned:

const rungs = createRungs({
store: memoryStore(),
clock: () => new Date(),
ids: () => crypto.randomUUID(),
// After each command commits, at least once and in order per request. Make it idempotent:
// `requestId` and `seq` identify an event.
onEvent: async (event) => {
if (event.type === "task.assigned") await email(event);
},
});

Runs in CI:examples/docs-snippets/src/concepts/audit.ts

Events are stored in an outbox in the same transaction, so none is lost when a process stops. They are delivered at least once, in order per request; a failed delivery is retried with backoff and holds back only that request’s later events. A process without onEvent, such as a web server, leaves them for a worker that runs rungs.dispatchPending() (EC-58).

Because an event can arrive twice, make handlers idempotent: requestId with seq identifies an event.