npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

little-durable

v0.1.4

Published

A lightweight durable workflow runtime for TypeScript.

Readme

Little Durable - A durable execution runtime for TypeScript in just 33 kB compressed

npm npm unpacked size CI TypeScript Documentation License: MIT Slack

This is an extremely light-weight (33 kB compressed!), runtime and storage agnostic, malleable, Durable Workflow runtime.

Little Durable is BYOCP (bring your own control plane, yes I just made that up), BYOC, and BYOS(torage).

This project was build entirely with the TDD + AI approach. Everything started with tests, and everything is heavily unit tested.

Installation

Little Durable requires Node.js 20 or newer. Install it with Zod:

npm install little-durable zod

See the example project to see a real working implementation of little-durable.

Why Does this Exist?

I built this because I wanted to run durable functions on Sandboxes. This meant coupling the state of the filesystem with the durable journal.

Existing solutions were super heavy-weight and made assumptions on how the workflows were being hosted. For example, most Durable Workflow systems assume you run everything on a small number of nodes and assume each invocation is non-isolated.

This is not the case for running durability in a serverless/cloud function environment.

So I made this!

Some key features:

  • Insanely lightweight: 32.6 kB compressed npm tarball with only 2 runtime dependencies (ms and ulid)
  • Storage agnostic: Journal can be Postgres, File System, Durable Object etc...
  • Runtime agnostic: Runs anywhere you can import this npm package
  • Type safety: Type safety enforced everywhere with Zod enforcing serialization safety in the Journal interactions.
import { mkdtemp } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"

import { FileJournalStore, Runtime, defineWorkflow, sleep, step } from "little-durable"
import { z } from "zod"

// 1 line runtime init
const runtime = new Runtime({ journalStore: new FileJournalStore(await mkdtemp(join(tmpdir(), "little-durable-test-"))) })

// Build your workflow
const WelcomeWorkflow = defineWorkflow({
    name: "welcome-customer",
    input: z.object({
        recipient: z.string(),
        name: z.string()
    }),
    run: async input => {
        const message = await step({
            name: "prepare-message",
            input: {
                name: input.name
            },
            run: async ({ name }) => {
                return `Welcome, ${name}!`
            }
        })

        await sleep("1 day")

        await step({
            name: "send-message",
            input: {
                recipient: input.recipient,
                message
            },
            run: async ({ recipient, message }) => {
                return { delivered: true }
            }
        })
    }
})

// run it
const events = runtime.start(WelcomeWorkflow, {
    runId: "run-123",
    input: {
        // this is type safe!
        recipient: "[email protected]",
        name: "Ada"
    }
})

for await (const event of events) {
    console.log(event)

    if (event.type === "runtime.suspended") {
        // Reach out to your control plane and schedule the run to resume.
        console.log("Workflow suspended", event.suspension)
    }
}

Steps support opt-in retries for caller-defined transient errors.

We also have some convenience methods to see the state of a run.

const run = await runtime.getRun({ runId: "run-123" })
// { runId: "run-123", workflowName: "welcome-customer", startedAt: "..." }

const suspension = await runtime.getSuspension({ runId: "run-123" })
// { waitId: "wait_01...", request: { type: "hook", name: "timer", payload: { wakeAt: "..." } } }
// or undefined when no unresolved wait exists

This is the bare bones of a durable runtime. From here, you can chose where to store the journal by simply implementing an interface and plugging it in. (See fileJournalStore.ts for an example implementation)

export interface JournalStore {
    list(params: ListJournalEventsParams): Promise<readonly JournalEvent[]>
    listByType(params: ListJournalEventsByTypeParams): Promise<readonly JournalEvent[]>
    get(params: GetJournalEventParams): Promise<JournalEvent | undefined>
    append(params: AppendJournalEventParams): Promise<JournalEvent>
    popStep(params: PopJournalStepParams): Promise<void>
}

It doesn't care where you run it! Run it on a hosted k8s pod, run it on Workers, sandboxes etc..

We make it really easy to plug into an external control plane

// Control plane reaches out via HTTP, Grpc, CLI etc...
const input = req.input
const runId = req.runId
const workflowName = req.workflowName

// resolve workflow, your code here
const workflow = fetchWorkflow(workflowName)

// Start a workflow
const events = runtime.start(workflow, {
    runId,
    input: {
        // this is type safe!
        recipient: "[email protected]",
        name: "Ada"
    }
})

for await (const event of events) publishRuntimeEvent(event)

// Resume from a sleep
const waitId = req.waitId

const resumedEvents = runtime.resumeTimer(workflow, {
    runId,
    waitId
})

for await (const event of resumedEvents) publishRuntimeEvent(event)

The hook system is also extremely malleable. Very easy to add Slack/email Human in the loop steps and plug into an integration system like Composio.

const ApprovalHook = defineHook({
    name: "approval",
    request: z.object({
        message: z.string()
    }),
    resolution: z.object({
        approved: z.boolean(),
        approvedBy: z.string()
    })
})

const WelcomeWorkflow = defineWorkflow({
    name: "welcome-customer",
    input: z.object({
        recipient: z.string(),
        name: z.string()
    }),
    run: async input => {
        console.log("Pre approval")

        const approved = await waitFor(ApprovalHook, {
            // Type will match resolution zod object above!
            message: "Deploy to production?"
        })

        console.log("Post approval:", approved)
    }
})

let suspension
for await (const event of runtime.start(WelcomeWorkflow, {
    runId: "run-123",
    input: {
        recipient: "[email protected]",
        name: "Ada"
    }
})) {
    if (event.type === "runtime.suspended") suspension = event.suspension
}

if (suspension) {
    for await (const event of runtime.resumeHook(ApprovalHook, {
        workflow: WelcomeWorkflow,
        runId: "run-123",
        waitId: suspension.waitId,
        resolution: {
            approved: true,
            approvedBy: "Ada"
        }
    })) {
        console.log(event)
    }
}

In fact, we implement sleep() with a small wrapper around defineHook(). A good example to check if you want some more custom hooks.

At Terse, we use this internally to power our Durable functions. We use the FileJournalStore to store the journal on the filesystem. On sandbox suspension, it gets picked up on the snapshot.

You can make your own JournalStore very easily. Store the journal in Postgres, Durable Object etc... as long as you can connect to it, it will work!

Given how malleable and lightweight this project is, you can use it as a base to build your own Durable Workflow API as we did in Terse. That is the beauty of this.

What do we Support?

Here is a list of the table-stake durability feature that are currently in:

  • Starting, resuming, retrying a workflow
  • Journaling steps
  • Step() support for defining durable steps
  • Passing in Workflow context and reading it from the workflow
  • Pinning Date() and seeded Random number generate for idempotent replays. (uses runId for seeding)
  • Creating custom hooks for suspending and resuming with external data

Documentation

Read the full documentation.

Example project

See the runnable order approval workflow, which demonstrates durable steps, typed hooks, filesystem journaling, process-independent resume, and replay safety.

Community

Join the Terse Slack community to ask questions, share feedback, and show us what you build.