worklists.dev

Timeout is a state, not a silence

On an append-only ledger a state change with no actor must be explicit. A breached deadline is written by an alarm, never by a person; a block belongs to whoever raised it; and escalate is a separate act by a separate party.

One thesis: a timeout is a state the record enters, not a silence, because on an append-only ledger a state change with no actor must be explicit. A deadline that passes without an act leaves nothing for trace to find. So the breach is written — by an alarm, never by a person — and the two neighboring verbs, block and escalate, each belong to a specific party for the same reason.

The silence problem

A receiving task for a pallet of lettuce has a window: dock 12, before 08:00. At 08:00 nothing has happened. In a system with a status column, the task still says pending, and a dashboard somewhere paints it red by comparing now() to due_at at render time. The record itself holds no fact about the breach. Replay the ledger next month and the breach is not in it; it was an artifact of when someone looked.

A fold over acts has to do better than that, because the fold is the record. If the only acts are dispatch and assign, the task folds to pending forever, and this window was missed is a fact that was never stated.

Timeout is an act with no performer

timeout takes the task and the deadline it breached, and appends an act that folds to timed-out. Three things about it are deliberate:

It has no performer. The act is authored by the alarm. Writing a person into who would put a fabricated actor on an evidence ledger — someone who did nothing, recorded as having done something. The actor field says what it is: a server-side clock that ran against the worklist.

It is not terminal. The state machine's own words for it are awaiting escalation. A timed-out task is still open; it has entered a state that says a window passed, and what happens next is a separate act by a separate party.

It records a breach; it never causes one. An alarm that fires before the deadline is refused with timeout-not-due, which is retryable because time fixes it. Writing the breach anyway would put a fact on the ledger that is not true. The early alarm is one of the package's poison fixtures.

It escalates on non-action, never non-delivery. Whether a notification reached a device is unobservable from the server; whether an act was appended is not. So the clock runs against the worklist, and a dead network is not a breach until the window is.

timeout is one of the five verbs that answer in the package worklists.dev today, with query, trace, claim and complete.

Block belongs to its author

Before the window closed, the receiver might have raised a block: the pallet arrived with the wrong label, the dock was occupied, I was handed a short load. block takes the task and a fixed-enum cause, and appends an act that is timestamped, located and authored by the performer.

That authorship is the whole mechanism. A block is that wasn't me / here's what I was handed, and it runs against the gradient of every existing workplace system, where the person closest to the problem has the least ability to put it on the record. So the block is author-owned, private by default, and never editable by a supervisor.

The package goes further than read-only, because read-only is a rule someone regresses. A non-author never receives the block at all. Not a redacted version, not a withheld(...) marker — withheld says there is a value you are not licensed for, which discloses that a block exists, and for this mechanism the existence of the block is part of what is private. The test asserts it the only way it can: a non-author's trace of a blocked task is deep-equal, member for member, to the trace of the same task with the block acts removed. The task still folds to blocked. The block is not a way to disappear; it is a way to be on the record without being exposed by it.

The cause is a fixed enum per deployment, never free text in the releasable form. unblock releases it and retains the block record.

Escalate is a separate party's act

A task that has timed out, or that a performer cannot finish, needs someone with authority to do something. escalate is that request, and it is addressed to a role, never a person — the performer escalates to shift-lead, and who fills that role is resolved at invocation. Letting a performer pick a person would mean rendering a list of people, which is the roster this door does not have.

Worklists holds no authority. escalate compiles to the identity rail's ceremony; the grant or the refusal is never Worklists'. An approval is a task, so the request lands in the role's worklist with the same shape as everything else there, and it settles with the same evidence shape.

The record, read back

trace on the lettuce pallet's receiving task, read by an auditor: dispatched from PO 2026-0007; assigned to dock 12; timed out at 08:00 by the alarm against the 08:00 window; escalated to shift-lead; settled at 08:41 with an event hash. Read by the receiver: the same, plus the block they raised at 07:52 with the cause short load. Read by anyone else: no block, and no sign of one.

Every state change is an act with an honest author — a person, an alarm, or a ceremony. Get started by folding a record with an early alarm and a due one. The refusal on the first is the point.

Get started.

Say what you will dispatch. Access opens the verbs against your own demand, the closing event validated at epcis.dev, and the frontline app under your own name.