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

chisel-engine

v0.2.6

Published

Lightweight workflow engine on BullMQ and Redis

Readme

chisel-engine

Lightweight workflow engine built on BullMQ and Redis.

An Inngest-shaped developer experience — defineWorkflow, ctx.step(), checkpoint-and-resume — with zero magic. No build plugins, no code transforms, no directives. Works anywhere JavaScript runs.

Features

  • Durable steps — each step checkpoints its result to Redis. On failure, the workflow resumes from the last successful step.
  • Step & workflow retries — configurable retry counts and backoff (exponential or fixed) at both the step and workflow level.
  • Parallel execution — run steps concurrently with ctx.parallel(). All steps complete before errors propagate.
  • Sleep & delaysctx.sleep("5m") pauses the workflow using BullMQ delayed jobs for long durations.
  • Workflow triggers — trigger child workflows from within a workflow via ctx.trigger().
  • Input validation — optional Zod schema validation on workflow input (Zod is not a dependency).
  • Keyed concurrency — per-key concurrency limits via Redis locks.
  • Deduplication — prevent duplicate triggers with configurable TTL.
  • Bounded Redis retention — terminal run state is pruned by age/count defaults so checkpoints do not grow without bound.
  • Lifecycle events — subscribe to workflow:start, workflow:complete, step:fail, etc.
  • MiddlewarebeforeStep / afterStep / beforeWorkflow hooks.
  • Hono adapter — optional REST API adapter for trigger, status, cancel, retry, and health.
  • Typed end-to-end — full TypeScript generics from defineWorkflow<TInput> to ctx.data.

Install

npm install chisel-engine
# or
pnpm add chisel-engine
# or
bun add chisel-engine

Redis must be available. BullMQ and ioredis are bundled dependencies.

Quick Start

import { createEngine, defineWorkflow } from "chisel-engine";

// 1. Define a workflow
const onboardUser = defineWorkflow<{ userId: string }>(
  {
    id: "user/onboard",
    retries: 3,
    backoff: { type: "exponential", delay: 1000 },
  },
  async (ctx) => {
    const user = await ctx.step("fetch-user", async () => {
      return db.users.findById(ctx.data.userId);
    });

    await ctx.step("send-welcome-email", async () => {
      await email.send({ to: user.email, template: "welcome" });
    });

    await ctx.step("provision-account", async () => {
      await billing.createAccount(user.id);
    });

    return { onboarded: true };
  }
);

// 2. Create and start the engine
const engine = createEngine({
  connection: { host: "localhost", port: 6379 },
});

engine.register(onboardUser);
await engine.start();

// 3. Trigger a run
const { runId } = await engine.trigger(onboardUser, { userId: "usr_123" });

Parallel Steps

Run steps concurrently with ctx.parallel(). All steps finish before any error is thrown.

const processOrder = defineWorkflow<{ orderId: string }>(
  { id: "order/process" },
  async (ctx) => {
    const [inventory, payment] = await ctx.parallel([
      ctx.step("check-inventory", () => inventory.check(ctx.data.orderId)),
      ctx.step("authorize-payment", () => payments.authorize(ctx.data.orderId)),
    ]);

    await ctx.step("fulfill", () => fulfillment.ship(ctx.data.orderId));
  }
);

Sleep

Pause a workflow. Short sleeps use an in-process timer; long sleeps (>5s) use BullMQ delayed jobs so no worker is blocked.

await ctx.step("send-reminder", async () => {
  await email.sendReminder(userId);
});

await ctx.sleep("24h"); // workflow pauses, worker is freed

await ctx.step("check-response", async () => {
  // resumes here after 24 hours
});

Trigger Child Workflows

const parent = defineWorkflow({ id: "parent" }, async (ctx) => {
  const { runId } = await ctx.trigger(childWorkflow, { key: "value" });
});

Step Options

Each step can override retry and timeout settings:

await ctx.step(
  "call-external-api",
  async () => {
    return fetch("https://api.example.com/data").then((r) => r.json());
  },
  {
    retries: 5,
    backoff: { type: "exponential", delay: 2000 },
    timeout: 30_000,
  }
);

Input Validation

Pass a Zod schema (or any object with .parse()) to validate input at trigger time:

import { z } from "zod";

const workflow = defineWorkflow(
  {
    id: "validated",
    input: z.object({ email: z.string().email() }),
  },
  async (ctx) => {
    // ctx.data is typed and validated
  }
);

Error Handling

import { FatalError } from "chisel-engine";

await ctx.step("check-permissions", async () => {
  if (!hasAccess) {
    // Immediately fails the workflow — no retries
    throw new FatalError("User lacks required permissions");
  }
});

Lifecycle Events

engine.on("workflow:complete", ({ workflowId, runId, result, duration }) => {
  console.log(`${workflowId} completed in ${duration}ms`);
});

engine.on("step:fail", ({ workflowId, stepName, error, attempt }) => {
  metrics.increment("step.failure", { workflowId, stepName });
});

Available events: workflow:start, workflow:complete, workflow:fail, step:start, step:complete, step:fail, step:retry.

Run Management

const run = await engine.getRun(runId);
// { id, workflowId, status, data, result, steps, progress: { completed, total, percentage } }

await engine.cancelRun(runId);
await engine.retryRun(runId);

Hono REST Adapter

import { Hono } from "hono";
import { chiselHono } from "chisel-engine/hono";

const app = new Hono();
app.route("/workflows", chiselHono(engine));

Endpoints:

  • POST /:workflowId — trigger a workflow
  • GET /runs/:runId — get run status
  • GET /runs/:runId/steps — get step details
  • POST /runs/:runId/cancel — cancel a run
  • POST /runs/:runId/retry — retry a failed run
  • GET /health — health check

Configuration

const engine = createEngine({
  // Redis connection
  connection: { host: "localhost", port: 6379 },
  // or: connection: { url: "redis://..." },

  // Defaults applied to all workflows
  defaults: {
    retries: 3,
    backoff: { type: "exponential", delay: 2000 },
    timeout: 60_000,
  },

  // Redis key prefix (default: "chisel")
  prefix: "myapp",

  // Terminal run retention in Redis (defaults shown)
  retention: {
    completed: { age: 7 * 24 * 60 * 60, count: 10_000 },
    failed: { age: 30 * 24 * 60 * 60, count: 10_000 },
    cancelled: { age: 7 * 24 * 60 * 60, count: 10_000 },
  },

  // Custom logger (default: console)
  logger: pino(),

  // Global middleware
  middleware: {
    beforeStep: ({ workflowId, stepName }) => { /* ... */ },
    afterStep: ({ stepName, result, duration }) => { /* ... */ },
  },
});

Set retention: false to disable pruning entirely, or set a status to false to keep that terminal state indefinitely.

Workflow Options

defineWorkflow({
  id: "my/workflow",
  retries: 5,                                    // workflow-level retries
  backoff: { type: "exponential", delay: 1000 }, // backoff strategy
  timeout: 120_000,                              // workflow timeout (ms)
  priority: 1,                                   // lower = higher priority
  concurrency: {
    limit: 1,
    key: (data) => data.tenantId,                // keyed concurrency
  },
  input: zodSchema,                              // optional input validation
});

License

MIT