NotificationWorkflows

Workflows

Code-first, durable multi-step notifications built on @visulima/workflow

Workflows

@visulima/notification/workflow turns the send primitive into durable, code-first workflows: a sequence of channel sends with delays and conditional logic that survives process restarts. It is a thin layer over @visulima/workflowcreateNotificationWorkflow returns a plain WorkflowDefinition, so it plugs straight into a workflow runtime.

@visulima/workflow is an optional peer; install it alongside:

npm install @visulima/notification @visulima/workflow

Defining a workflow

import { createNotification } from "@visulima/notification";
import { createNotificationWorkflow } from "@visulima/notification/workflow";
import { slackProvider } from "@visulima/notification/providers/slack";
import { fcmProvider } from "@visulima/notification/providers/fcm";
import { createRuntime } from "@visulima/workflow";

const notify = createNotification({
    chat: slackProvider({ token: "xoxb-…", defaultChannel: "C123" }),
    push: fcmProvider({ projectId: "app", getAccessToken: getToken }),
});

const onComment = createNotificationWorkflow<{ subscriberId: string; author: string }>(notify, {
    id: "comment-posted",
    run: async ({ step, payload }) => {
        // Each channel step delivers exactly once and records its receipt.
        await step.push("ping", () => ({ to: [payload.subscriberId], title: "New comment", body: payload.author }));

        // Durably pause — the process can exit and resume later.
        await step.delay("cool-off", { amount: 1, unit: "hours" });

        // Conditionally skip a step.
        await step.chat("recap", () => ({ text: `${payload.author} commented` }), {
            skip: ({ payload }) => payload.author === "bot",
        });
    },
});

Running it

A notification workflow is an ordinary WorkflowDefinition — drive it with a workflow runtime:

const runtime = createRuntime({ workflows: [onComment] });

await runtime.trigger(onComment, { subscriberId: "u1", author: "ada" });

// Resume delayed runs on a timer (cron job, Cloudflare alarm, …):
await runtime.sweep();

See the workflow engine docs for durability, stores (memory / unstorage / SQL / Redis) and the cross-process lease.

The step object

StepWhat it does
step.email / sms / push / chat / inApp / webhookResolve a channel payload and deliver it through the bound facade exactly once; returns the Receipt.
step.delay(id, duration)Durably pause (ms, { amount, unit }, or { cron }).
step.custom(id, fn)Escape hatch to a raw engine step for non-send side effects.

Each channel step takes (id, resolve, options?). resolve returns the channel payload (it receives { payload }); options.skip conditionally skips the send. Because sends go through the engine's step, they are recorded and not re-sent on replay.

Delivery failures don't throw by default. The facade returns a FailureReceipt (it doesn't throw), so a failed send is recorded as a completed step and not retried — check receipt.successful, or pass { throwOnFailure: true } to turn a failure into a thrown error that fails the run and re-runs the step on the next resume/sweep:

await step.email("critical", () => payload, { throwOnFailure: true });

Replay safety: only work inside step.* runs exactly once. Bare side effects in the run body re-execute on every replay — see the workflow durability docs.

Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues