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

slapflow

v1.5.1

Published

Declarative orchestration runtime for strategy graphs

Downloads

1,088

Readme

Slapflow

When the same business flow can start from a form, an API route, a job, or a WebSocket message, its control flow tends to spread across the application. Slapflow gives that flow one explicit home: a chain of ordinary TypeScript functions.

bundle size Socket security

Slapflow takes care of orchestration, concurrency, cancellation, and diagnostics. Your application keeps ownership of its domain state and side effects.

Why use it?

  • Keep the business flow visible. Put a scenario in one chain instead of hiding it among UI handlers, transport callbacks, and service code.
  • Test the scenario without the surrounding application. Pass in context and events; actions and conditions are just TypeScript functions.
  • Make async behavior deliberate. Choose parallel, latest, queue, drop, or workers for each event source. Actions receive an AbortSignal when cancellation matters.
  • Use the same flow in more than one place. The chain can start from a typed bus, DOM event, API callback, timer, worker, or WebSocket message.

When to use Slapflow

Slapflow orchestrates a control flow that lives and finishes inside a single process. Reach for it when a scenario spans several steps, branches, and failure paths and you want it in one place rather than spread across handlers and callbacks.

What it is not: a web framework (no routing, no middleware stack — pair it with Express, Hono, or Next.js routes) or a job queue (no built-in persistence, no distributed workers). It is also not a durable workflow engine: a chain runs inside a live process and stops with it — a core.loop can spin indefinitely while the process runs, but unlike Temporal there is no state that survives a crash or restart unless your application prevails it. It is closest to a lightweight, in-process state machine: decisions, ordering, concurrency, and cancellation live in a chain, while your application keeps the domain state and side effects.

Installation

npm install slapflow

Quick start

This example handles an order submission from a typed event bus. The latest mode cancels a previous submission for the same order when a newer event arrives.

import { createFlow, createPubSub } from 'slapflow'

type Context = {
  orders: Map<string, { id: string; status: 'draft' | 'submitted' }>
}

type Events = {
  'order.submit': { orderId: string }
}

const bus = createPubSub<Events>()
const context: Context = {
  orders: new Map([['order-1', { id: 'order-1', status: 'draft' }]]),
}

const flow = createFlow<Context, unknown, Events>(
  {
    events: {
      '[bus] order.submit': {
        entrypoint: 'order.submit',
        options: {
          concurrency: {
            mode: 'latest',
            key: ({ orderId }) => orderId,
          },
        },
      },
    },
    actions: {
      'order.submit': ({ context, input }) => {
        const order = context.orders.get(input.orderId as string)

        if (order) {
          order.status = 'submitted'
        }
      },
    },
    config: {
      entrypoints: { 'order.submit': 'order.submit' },
      strategies: { 'order.submit': { fn: 'order.submit' } },
    },
  },
  { bus, context }
)

flow.start()
bus.emit('order.submit', { orderId: 'order-1' }, { origin: 'api' })

Fetch data with cancellation and retries

core.fetch uses the run AbortSignal, retries transient failures, and keeps the response available to the next strategy without coupling the flow to a specific HTTP client.

const config = {
  strategies: {
    'catalog.load': {
      fn: 'core.fetch',
      props: {
        url: '/api/catalog',
        response: 'json',
        dataPath: 'catalogResponse',
        retry: { maxAttempts: 2, initialDelay: 250, maxDelay: 1_000 },
      },
      then: ['catalog.apply'],
    },
    'catalog.apply': { fn: 'catalog.apply' },
  },
}

const applyCatalog = ({ runtime }) => {
  const { body } = runtime.data.get('catalogResponse')

  // Update your application state with body.
}

Route branches with compound conditions

Every then and catch target can have its own condition. Combine built-in conditions with and, or, and not to keep branching in the graph:

const config = {
  strategies: {
    'catalog.load': {
      fn: 'core.fetch',
      props: {
        url: '/api/catalog',
        response: 'json',
        dataPath: 'catalogResponse',
      },
      then: [
        {
          strategy: 'catalog.apply',
          when: [
            'and',
            ['typeIs', '$data.catalogResponse.body', 'record'],
            ['typeIs', '$data.catalogResponse.body.items', 'array'],
            ['not', ['empty', '$data.catalogResponse.body.items']],
          ],
        },
        {
          strategy: 'catalog.showEmpty',
          when: ['or', ['missing', '$data.catalogResponse.body.items'], ['empty', '$data.catalogResponse.body.items']],
        },
      ],
      catch: [
        {
          strategy: 'catalog.queueRetry',
          when: ['and', ['falsy', '$context.network.online'], ['includes', ['startup', 'refresh'], '$input.source']],
        },
        {
          strategy: 'catalog.showError',
          when: ['or', ['truthy', '$context.network.online'], ['eq', '$input.source', 'manual']],
        },
      ],
    },
    'catalog.apply': { fn: 'catalog.apply' },
    'catalog.showEmpty': { fn: 'catalog.showEmpty' },
    'catalog.queueRetry': { fn: 'catalog.queueRetry' },
    'catalog.showError': { fn: 'catalog.showError' },
  },
}

Fan out work through a keyed pool

A workers binding runs tasks through a shared pool: at most workers runs at once, tasks with the same line key run in FIFO order and never in parallel, and different keys run concurrently. An action fans work into a named pool with runtime.enqueue:

const flow = createFlow<Context, Patch, Events>(
  {
    config,
    actions: {
      'world.tick': async ({ input, runtime }) => {
        for (const colonyId of Object.keys(context.colonies)) {
          await runtime.enqueue('colony.tick', { tick: input.tick, colonyId }, { pool: 'colony', key: colonyId })
        }
      },
    },
    events: {
      // dispatcher runs outside the pool it feeds
      '[bus] game.tick': { entrypoint: 'world.tick', options: { concurrency: { mode: 'parallel' } } },
      '[bus] colony.observed': {
        entrypoint: 'colony.observed',
        options: { concurrency: { mode: 'workers', pool: 'colony', key: '$input.colonyId' } },
      },
    },
  },
  {
    context: () => store.getState(),
    bus,
    pools: { colony: { workers: 4, maxQueueSize: 2_000, overflow: 'wait' } },
  }
)

overflow: 'wait' applies backpressure instead of dropping accepted work. A task cannot enqueue into its own pool (ENQUEUE_SELF_POOL), so the fan-out dispatcher runs outside colony. flow.poolStats('colony') reports active/queued/oldestQueuedMs, and flow.drain({ timeoutMs }) waits for pools and binding lanes to empty on shutdown.

The pool is in-process and in-memory: it bounds concurrency and preserves per-key order, but it is not durable storage.

What Slapflow provides

  • Declarative strategies, conditions, error branches, and entrypoints.
  • A broad set of built-in conditions for comparisons, type checks, collections, and compound logic.
  • Typed PubSub bindings and delegated DOM bindings.
  • parallel, latest, queue, and drop concurrency modes with per-entity lanes, plus a keyed workers pool with named pools, backpressure, and runtime.enqueue fan-out.
  • A native WebSocket client that proxies socket events into the bus.
  • core.fetch with response parsing, cancellation, and retry backoff.
  • core.invoke to fan out a then branch over the elements of an array or object, exposing each element to the branch as $input.
  • Normalized results, execution trace, validation, and lifecycle diagnostics such as slapflow.run.started and slapflow.run.failed.
  • Runtime variables for configuration values, templates, and expressions.

Where to go next

  • See slapflow-studio, a working application built entirely on Slapflow.
  • Read the complete technical specification for the runner API, built-in actions and conditions, expressions, validation, safety limits, transport behavior, and lifecycle semantics.
  • Russian documentation: README-RU.md and SPEC-RU.md.

Development

npm test
npm run build
npm run pack:check

Agent skill

The package ships a SKILL.md skill in skills/slapflow/. Point your agent at it — symlink the folder, not the SKILL.md file.

| Tool | Location | Install | | --- | --- | --- | | opencode | opencode.json → skills.paths | config | | Claude Code | ~/.claude/skills/ (personal) or .claude/skills/ (project) | symlink | | Codex | ~/.agents/skills/ (user) or .agents/skills/ (repo) | symlink |

opencode — add the packaged path to opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "skills": { "paths": ["node_modules/slapflow/skills"] }
}

Claude Code and Codex — symlink the folder (.agents/skills also serves other Agent Skills clients):

ln -sfn "$PWD/node_modules/slapflow/skills/slapflow" ~/.agents/skills/slapflow
ln -sfn "$PWD/node_modules/slapflow/skills/slapflow" ~/.claude/skills/slapflow

Restart opencode and Codex after installing; Claude Code picks up new skills after a restart too, then watches them live. Re-run the symlink if you reinstall the package.