Batch work keeps its progress
An action is all-or-nothing: its writes all commit, or none do (All-or-nothing actions). If one action emailed 500 owners and failed on the 300th, its database writes would roll back, but 299 emails would already be sent. So a schedule that covers many items doesn't do the work itself. It finds the items and emits one event per item. Each item is then handled by its own action and retried on its own, and one bad item never blocks or undoes the rest.
In this example, overdue_todos is a named query
returning at most 500 todos not yet queued, and send_reminder is the
subscriber that handles each event:
// domains/planning/actions/queue_chore_census.jadpoaction queue_chore_census() -> Unit { var cutoff: ReminderCutoff = ReminderCutoff(clock.now) var todos: List<Todo> = overdue_todos(cutoff) for todo in todos { attempt Todo.update(todo) { set: { reminder_queued_at: clock.now } } emit ReminderDue { todo_id: todo.id } }}// domains/planning/subscribers/send_reminder.jadposubscriber send_reminder { on: ReminderDue run: send_reminder_email(event.todo_id)}
A query returning a list declares no failures, so its call takes no
attempt. A database outage is a fault, and
the next run tries again. The update can fail, because the todo may have
changed since it was read, so it takes attempt: the failure ends the
run, nothing commits, and the next run tries again. The query's limit
bounds each run, and the next run picks up anything left over. Marking
each todo in the same transaction stops the next run from queueing it
twice.
This shape suits a bounded set found by a query. Work over a set with no bound, such as every row of an uploaded file, a large export or every page of a provider's list, is a batch.
Markdown for agents: /docs/batch-work-keeps-its-progress.md.Built from Jadpo main at a707efb.