workflow-recipient-list: When Notification Fan-Out Becomes a Live, Durable Debugger
A Next.js reference implementation that shows how the Vercel Workflow SDK turns multi-channel delivery into a replayable, observable system instead of a pile of queues and retries.
- The repo’s real product is not notification delivery, but a debugger that makes durable fan-out legible in real time.
- `Promise.allSettled()` and `FatalError` turn failure into policy, so one bad recipient does not poison the whole workflow and permanent errors stop wasting retries.
- The code workbench is the differentiator, because it links live workflow events to exact source lines instead of burying them in logs or queue workers.
- The project argues that reliability, observability, and application code can live in the same Next.js mental model.
The first surprise is not that this app can send to Slack, email, and PagerDuty. It is that it can explain each branch while it runs. `workflow-recipient-list` treats durable execution like something you should be able to read, not just operate.
The notification system that explains itself
That changes the shape of the problem. A recipient list is usually where teams bolt together queues, retries, cron jobs, and a separate dashboard. This repo collapses that stack into one workflow and one UI, so the delivery logic and the debugging surface stay in the same mental model.
Workflow Development Kit brings durability, reliability, and observability to async JavaScript so you can build apps and AI agents that suspend, resume, and maintain state with ease.
Why recipient lists are harder than they look
A recipient list sounds simple until you ship it. Channels have different SLAs, different failure modes, and different business rules. Some are optional, some are mandatory, and some should retry forever while others should fail fast.
| Approach | Durability | Visibility | Failure handling | Developer burden |
|---|---|---|---|---|
| Cron jobs plus queues | Possible, but assembled from parts | Spread across logs, workers, and dashboards | Usually custom and inconsistent | High |
| Manual orchestration and persistence | Strong if you build it correctly | Depends on your tooling | Explicit, but hand-rolled | Very high |
| Vercel Workflow SDK | Built into the workflow model | Live events plus code-linked UI | Branch-aware with retries and fatal exits | Lower |
That is why the pattern matters. Fan-out is where distributed systems stop being elegant and start becoming operational. The repo’s answer is to make each recipient branch durable on its own, instead of letting one failure collapse the whole delivery.
The workflow engine is the product
In `workflows/recipient-list.ts`, the architecture is the argument. The `"use workflow"` directive defines the durable boundary, while `"use step"` wraps each delivery so the runtime can checkpoint progress and resume after interruption.
"use workflow";
export async function recipientList(input: RecipientListInput) {
const recipients = await getRecipients(input);
const results = await Promise.allSettled(
recipients.map((recipient) =>
deliverToRecipient(recipient, input.message)
)
);
return summarize(results);
}
async function deliverToRecipient(recipient: Recipient, message: string) {
"use step";
return sendChannel(recipient.channel, message);
}
`Promise.allSettled()` is the quiet hero here. It lets the workflow fan out in parallel without turning the whole run into an all-or-nothing bet. If Email fails, Slack can still succeed, and the final summary can tell you exactly what happened.
That is where `FatalError` matters. It tells the runtime that some failures are permanent, not temporary. That distinction is expensive to get wrong, because indiscriminate retries waste time, money, and operator trust.
The function suspends for 7 days without consuming any resources, then resumes exactly where it left off.
From runtime events to highlighted lines of code
The workbench is the sharpest part of the repo. The API route exposes a readable stream for a workflow run, the frontend reducer folds those events into local state, and the code viewer lights up the line that corresponds to the branch that just executed.
That turns debugging into a spatial task. You are not hunting through logs to infer what happened. You are looking at the same workflow through three synchronized views: trigger, timeline, and source.
What this replaces, and what it still cannot
| Model | Strength | Weakness | Best fit |
|---|---|---|---|
| Cron plus queues | Familiar and widely portable | Visibility is fragmented | Simple background jobs |
| Manual workflow systems | Maximum control | Lots of plumbing and glue code | Teams with custom platform needs |
| Vercel Workflow SDK | Fast path to durable, inspectable workflows | Tighter platform bet and younger ecosystem | Product teams that want durability inside app code |
So the tradeoff is clear. This repo is not pretending to be a universal workflow philosophy. It is a focused answer to a painful class of problems, and it is strongest when you want reliability and debug visibility without building a platform team around them.
The bigger bet behind the demo
The broader idea is bigger than notifications. It is that durable execution can feel like normal application code, with a UI that speaks the same language as the source. If that thesis holds, the most valuable thing here is not a sender, a queue, or even a retry policy. It is the fact that the system can explain itself while you are still reading the code.





