The code snippets can be hard to read on LinkedIn. The full, runnable code is on the automation branch of em-building-blocks.
Picture an insurer where a batch of policy owners is charged twice for the same monthly premium. No alarm goes off. No job fails. The logs look clean. The first sign of trouble is a stack of complaints, weeks later, and then a long afternoon of reconstructing which charges went out when, and why.
In The work nobody clicks I argued that the most dangerous work in a business system is the work no user triggers. Nobody presses a button to collect a premium, send a lapse notice or feed a reinsurer. It happens because time passes or because another system says something. When it goes wrong, it rarely fails loudly. It just stays undone, or gets done twice.
One slice, end to end showed the code for a state change. Read models are cheap. Coupling isn't. showed the code for state views. This article does the same for automation, with premium collection in life and health insurance as the example. Plain JavaScript, PostgreSQL and an event store with Dynamic Consistency Boundaries (DCB). Every claim about the code below is backed by a test in the repository.
The shape every automation shares
Every automation here has the same four pieces.
A todo list. It's a read model, built from events like any other. An event opens a row, and the event recorded by the command closes or moves it. For premiums, one table holds one row per policy and period, and a status column says which lane the row is in:
// open (the processor picks it up), requested (waiting for the provider),
// needs_attention (a person works it), collected (done).
CollectionRunStarted: (d, _e, c) =>
c.query(`INSERT INTO premiums_to_collect (collection_id, policy_id, period, amount, due_at)
SELECT 'collection-' || policy_id || '-' || $1, policy_id, $1, premium, $2
FROM policies_in_force ON CONFLICT DO NOTHING`, [d.period, d.dueAt]),
Full code: automation/PremiumCollection/PremiumsToCollect.js, L7-L8 and L34-L39
A processor loop. It reads the open rows and issues one command per row. The same loop serves every automation:
export async function workThrough(rows, issue) {
const results = [];
for (const row of rows) {
try {
results.push({ row, ok: true, event: await issue(row) });
} catch (error) {
results.push({ row, ok: false, error: error.message }); // skip it, keep going
}
}
return results;
}
Full code: automation/processor.js
A todo list is not a queue. One bad row never blocks the others, and the test for escalation checks exactly that: while one policy keeps failing, the other is charged.
A command. The processor doesn't decide anything. It sends StartCollectionRun, RequestPremiumCollection or RecordAttemptFailed, and the command handler decides from past events, as in any state change slice.
Idempotency. Processors run again, run in parallel and crash halfway. So every handler appends with a DCB condition: "only if nothing relevant happened since I read." Projections write with ON CONFLICT DO NOTHING. Running anything twice is safe.
Kind 1: the todo processor, or why nobody is charged twice
Back to the opener. Two things stop the double charge. The collection id is fixed, built from the policy and the period:
export const collectionIdFor = (policyId, period) => `collection-${policyId}-${period}`;
Full code: automation/PremiumCollection/PremiumsToCollect.js
And the request is only recorded while the collection is still open:
if (state.status !== 'open') throw new Error(`PremiumCollection${state.status === 'requested' ? 'AlreadyRequested' : 'NotOpen'}`);
const [event] = await store.append(
{ type: 'PremiumCollectionRequested', data: { collectionId, policyId, period, amount, attempt: state.attempt + 1, /* … */ },
tags: [`collection:${collectionId}`, `policy:${policyId}`] },
{ failIfEventsMatch: query, after: position }, // two processors racing: only one append wins
);
Full code: automation/PremiumCollection/RequestPremiumCollection.js
The processor records the request first and only then calls the payment provider, passing the collection id as the provider's idempotency key. The tests run the processor twice in a row, and three processors at the same time. Each time, every policy is charged once. A new period is a new id, so next month's premium is never mistaken for a duplicate.
What's special here: the rule "never twice in flight, never after it's paid" lives in the command handler, not in the processor. The processor stays dumb, and that's the point.
Kind 2: the clock-driven run that catches up
Picture an insurer where the monthly collection run simply didn't happen. The scheduler host was being patched that night. Nothing failed, so nothing alerted. Days later, finance notices that the premium income is short.
In the clock-driven slice, each run is a row with a due_at, and the processor takes a clock instead of reading the system time:
export async function processCollectionRuns({ pool, store, clock }) {
const now = clock();
const rows = await listDueCollectionRuns(pool, now);
return workThrough(rows, (row) => startCollectionRun(store, { period: row.period, now }));
}
Full code: automation/CollectionRuns/CollectionRunProcessor.js
The handler rejects a run that isn't due yet (CollectionRunNotDueYet) and a run that has already started. The test leaves the processor down on the day the run is due. Three days later the row is still on the list, still due, and the first tick starts it. A missed run doesn't vanish. It waits.
What's special: time is just another input. Tests pass a fixed clock, production passes () => new Date().
Kind 3: translating what the outside world says
Picture an insurer whose reinsurance bordereau feed went dark the month a new product code was launched. The mapping didn't know the code, the records were dropped, and nobody heard about it until the reinsurer asked.
The same risk sits at the payment provider's webhook. Its events are not the system's language, so they get translated at the door, inbound only:
export function translate(payload) {
const collectionId = payload.idempotency_key; // we sent our id, the provider echoes it back
if (payload.type === 'charge.succeeded') return { kind: 'collected', collectionId };
if (payload.type === 'charge.failed') return { kind: 'failed', collectionId, reason: payload.failure_code };
throw new Error(`UnknownProviderEvent:${payload.type}`);
}
Full code: automation/PaymentProviderResult/TranslatePaymentResult.js
An event type the translator doesn't know is rejected loudly, not dropped. The handler then records PremiumCollected or PremiumCollectionAttemptFailed. It deduplicates on the provider's event id, so the same webhook delivered twice is recorded once, and a result for a collection that was never requested is rejected.
What's special: the outside vocabulary stops here. Nothing behind the translator ever reads a provider payload.
Kind 4: retry and escalation
Picture an insurer where a lapse notice was never sent. The address on file was malformed, the job skipped the record without a word, and the customer lost cover without being told.
Skipping a bad row is fine. Forgetting it isn't. Every failed attempt is an event that puts the row back in the open lane, and the next run tries again. The third failure also records an escalation:
export const MAX_ATTEMPTS = 3;
if (state.attempt >= MAX_ATTEMPTS) {
events.push({ type: 'PremiumCollectionEscalated', data: { collectionId, attempts: state.attempt, reason }, tags });
}
Full code: automation/PremiumCollection/CollectionState.js, L3 and L28-L30
That moves the row to needs_attention, a lane a person works from and the processor never reads. The tests prove that two failures keep the row in the normal lane, the third moves it, and a fourth run makes no fourth call.
There is one more gap: the process dies after recording the request but before the provider answers. A clock-driven sweep treats a request with no provider result after a window (60 minutes by default) as a failed attempt with reason NoProviderResult. The next run charges again with the same collection id, so a provider that did charge the first time deduplicates it. These resends count toward the three attempts on purpose. Three attempts with no answer means a person should look.
To be precise about what this code does not do: there is no wait between retries beyond the next run, and there is no command yet to send an item from the needs-attention lane back for another try. The lane is there. What a person does with it is the next slice.
Business monitoring without new infrastructure
None of this helps if nobody looks. The dashboard is a read-only query over the todo tables the processors already use:
const attention = await pool.query(
`SELECT collection_id, due_at, attempt, last_reason FROM premiums_to_collect
WHERE status = 'needs_attention' ORDER BY due_at, collection_id`);
Full code: automation/CollectionDashboard/CollectionDashboard.js
Three lists come back: runs not started, premiums not collected (flagged overdue after a grace period) and the needs-attention lane. No new table, no metrics pipeline, no extra service. The tests show a missed run as overdue, collected premiums gone from the list, and escalated premiums in their own lane rather than the normal one.
This is the question operations actually asks: what should have happened by now and hasn't?
Run it yourself
The full code for this article, with every test mentioned above, is on the automation branch of em-building-blocks. The main branch has every building block so far side by side: state change, state view and now automation.
The same failure, elsewhere
- A claim is approved, but the money never leaves.
- An underwriting evidence chase stalls after the first reminder.
- A sanctions list update arrives, but it's never applied.
- Regulatory notices stop going out after a template change.
Different departments, same pattern. The work never fails visibly. It just stays undone. A todo list makes undone work a row you can query, count and hand to a person. That's what an automation slice is for.
Originally published on LinkedIn on 3 Nov 2026: https://www.linkedin.com/pulse/work-nobody-sees-automation-slices-code-gabriel-n-schenker-nk4ve/