# A task is the causal parent of its events

> worklists.dev — 2026-08-20
> Written for ED-3 — the in-house supply-chain platform team — the platform engineer who owns the WMS integration and has been asked why the event ledger cannot answer "why".

One thesis: **an event says what happened; it has never said what it was for, and a task is the
object that supplies the *for*.** A task is the causal parent of the events that satisfy it. Once
that parent exists, the record reads *this was scanned because this task said to*, and the join
between the two is the event's own hash rather than a column somebody filled in afterward.

## What an event can and cannot say

Take a bag of lettuce moving through a DC. An EPCIS event records that a case containing it was
observed at dock 12 at 06:14, business step `receiving`, disposition `in_progress`, read point
`urn:epc:id:sgln:...`. That is a complete and honest statement. It is what the [event
engine](https://epcis.dev) is for, and it answers *what, where, when, and which*.

Ask it *why* and it goes quiet. Was that scan a receiving count against purchase order 2026-0007?
A cycle count a supervisor ordered because the lot looked short? An accident while someone was
reaching for the case beside it? The event dictionary has no field for intent, and it should not —
a field for intent on an event is a field anyone can fill with anything.

Every integration team meets this at the same place: the auditor or the customer asks *show me that
the receiving count for this PO was done*, and the team has a ledger of scans that is consistent
with the count having been done and equally consistent with it not having been.

## The parent is a task

worklists.dev adds one object above the event. A task is dispatched from a demand — the PO that
arrived at [transactions.dev](https://transactions.dev), an exception that fired, a plan, a schedule
— and it names what will satisfy it: an action verb (the `bizStep`, from the [barcode
layer](https://barcoding.dev)'s reading of the mark), a subject, a place, a window.

Then the scan happens. The event lands on the spine exactly as it would have without any task. And
`complete` takes that event's hash and settles the task with it. Nothing about the event changes;
the task now points at it, and the pointer is content-addressed.

So the causal arrow runs one way, and it is readable in both directions:

- From the task: *here is the event that satisfied me*, by hash.
- From the event, through `trace`: *here is the task that asked for me*, by lineage.

With the action verb supplying the step and the badge supplying the performer, a complete event
composes from scans with no application in the loop. The app that rendered the worklist did not
author the evidence; the spine did.

## Why a hash and not a foreign key

The obvious design is a `task_id` column on the event, or an `event_id` column on the task. Both
are mutable, and both put intent where it can be edited. A hash cannot be repointed without the
repointing being visible, because the closing act is appended to the task's record and the record is
append-only. A task has no `update`. A mistaken close is corrected at the *event* grain with
`epcis.dev.correct`, which emits a new event with an `errorDeclaration` — the task record stays
exactly as it was, and the correction is itself on the ledger.

That is what makes *this was scanned because this task said to* a statement a third party can
check rather than a statement your application asserts.

## What this looks like in the package

The five verbs that answer in code today ship as the package `worklists.dev`: `query`, `trace`,
`claim`, `complete`, `timeout`. There is no `Task` type with a status you can assign. There is an
`ActionRecord`, and `foldTask()` returns the projection over it:

```ts
statusOf(record.actsOf("task-1"));
// "pending" | "claimed" | "in-progress" | "blocked"
// | "escalated" | "timed-out" | "settled" | ...
```

`trace` returns the lineage: the document or plan that dispatched the task, its assignments, its
blocks (to their author only), and its closing evidence. It composes to `epcis.dev.trace` for the
evidence hop rather than walking the ledger itself, which is the same act at a different grain.

## What it means for the integration

If you already emit EPCIS events from your WMS, you keep doing that. Worklists does not become a
second write door; `complete` returns a capture intent for `epcis.dev.capture` and the package's
own test suite fails on any `fetch` in its source. What you add is the parent: dispatch tasks from
the demand you already have, and close them with the hashes you already produce.

The question *was the receiving count for PO 2026-0007 done?* then has an answer that is a task, a
hash, and an event — a real *who*, at a real place, inside the window — instead of a query over
scans that might mean anything.

Get started with the package, dispatch one task against one PO, and close it with a real event
hash. The bag of lettuce will not notice. The auditor will.

---
Get started: https://worklists.dev/get-access/ · All posts: https://worklists.dev/blog/ · Machine face: https://worklists.dev/llms.txt
