WorkflowStores

Stores

The WorkflowStore contract and the built-in adapters

Stores

Durability is delegated to a small WorkflowStore contract; the engine stays storage-agnostic. Two stores ship in the box, and you can implement the contract over Redis, Postgres, a Durable Object, or anything else.

Built-in stores

MemoryStore (default)

In-process, ideal for tests and single-instance apps. Used automatically when you don't pass a store.

import { createRuntime, MemoryStore } from "@visulima/workflow";

const runtime = createRuntime({ store: new MemoryStore() });

UnstorageStore

Durable, edge-friendly persistence over any unstorage driver (Cloudflare KV, D1, Redis, filesystem, memory…).

import { createRuntime, UnstorageStore } from "@visulima/workflow";
import { createStorage } from "unstorage";
import cloudflareKVBindingDriver from "unstorage/drivers/cloudflare-kv-binding";

const runtime = createRuntime({
    store: new UnstorageStore(createStorage({ driver: cloudflareKVBindingDriver({ binding: env.RUNS }) })),
});

A small due-index document tracks wake-at times so sweep does not scan every run. Operations are not transactional; its optional lease is best-effort on non-atomic drivers (see Concurrency).

SqlStore (PostgreSQL / MySQL)

Durable persistence in a SQL database, with a genuinely-atomic cross-process lease (a single conditional UPDATE), so it is safe for many runtime instances against one database. Driver-agnostic: adapt any client (node-postgres, postgres.js, mysql2, pglite…) to the structural SqlClient — a single query(sql, params) => { rows, rowCount }.

import { createRuntime, SqlStore } from "@visulima/workflow";
import { Pool } from "pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const store = new SqlStore(
    { query: (sql, parameters) => pool.query(sql, parameters as unknown[]) }, // pg returns { rows, rowCount }
    { dialect: "postgres" },
);

await store.init(); // create the table (also runs lazily on first use)
const runtime = createRuntime({ store });

For MySQL pass { dialect: "mysql" } and connect with the FOUND_ROWS client flag (so the lease's affected-rows check counts matched, not changed, rows). Run state is stored as JSON text, so no JSON column type is required.

RedisStore

Durable persistence in Redis, with an atomic Lua acquire/release lease and a sorted-set wake index — safe across instances. ioredis satisfies the structural RedisLike directly.

import { createRuntime, RedisStore } from "@visulima/workflow";
import Redis from "ioredis";

const store = new RedisStore(new Redis(process.env.REDIS_URL));
const runtime = createRuntime({ store });

The contract

interface WorkflowStore {
    save(run: StoredRun): Promise<void>;
    load(runId: string): Promise<StoredRun | undefined>;
    delete(runId: string): Promise<void>;
    due(now: number, limit: number): Promise<string[]>; // runs whose wakeAt has passed

    // Optional cross-process lease (omit to rely on in-process locking only):
    acquire?(runId: string, token: string, ttlMs: number): Promise<boolean>;
    release?(runId: string, token: string): Promise<void>;
}
  • due(now, limit) is the poll the sweep entrypoint uses. The contract is deliberately poll-based so it works on plain KV/SQL, but it is shaped so a push-based adapter (e.g. Durable Object alarms) can implement due as a no-op and schedule wake-ups inside save.
  • acquire / release are the optional cross-process lease. Implement them with an atomic primitive (Redis SET NX, a SQL row lock, a Durable Object) for race-free exclusion across instances. See Concurrency & exactly-once.

Writing your own

A minimal store needs only the four required methods. StoredRun carries the JSON-serialisable snapshot plus denormalised status, wakeAt and eventName so you can implement due and signal-routing without parsing the snapshot. Keep save ordering-safe: if your backend isn't transactional, persist the wake index before the run so a crash yields at worst a harmless spurious wake (which resume re-validates and ignores) rather than a lost wake-up.

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