Skip to content

Repository files navigation

Chisel

Lightweight workflow engine built on BullMQ and Redis.

Chisel gives you 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 (no zombie steps).
  • 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 workflowconstonboardUser=defineWorkflow<{userId: string}>({id: "user/onboard",retries: 3,backoff: {type: "exponential",delay: 1000},},async(ctx)=>{constuser=awaitctx.step("fetch-user",async()=>{returndb.users.findById(ctx.data.userId);});awaitctx.step("send-welcome-email",async()=>{awaitemail.send({to: user.email,template: "welcome"});});awaitctx.step("provision-account",async()=>{awaitbilling.createAccount(user.id);});return{onboarded: true};});// 2. Create and start the engineconstengine=createEngine({connection: {host: "localhost",port: 6379},});engine.register(onboardUser);awaitengine.start();// 3. Trigger a runconst{ runId }=awaitengine.trigger(onboardUser,{userId: "usr_123"});

Parallel Steps

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

constprocessOrder=defineWorkflow<{orderId: string}>({id: "order/process"},async(ctx)=>{const[inventory,payment]=awaitctx.parallel([ctx.step("check-inventory",()=>inventory.check(ctx.data.orderId)),ctx.step("authorize-payment",()=>payments.authorize(ctx.data.orderId)),]);awaitctx.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.

awaitctx.step("send-reminder",async()=>{awaitemail.sendReminder(userId);});awaitctx.sleep("24h");// workflow pauses, worker is freedawaitctx.step("check-response",async()=>{// resumes here after 24 hours});

Trigger Child Workflows

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

Step Options

Each step can override retry and timeout settings:

awaitctx.step("call-external-api",async()=>{returnfetch("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";constworkflow=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";awaitctx.step("check-permissions",async()=>{if(!hasAccess){// Immediately fails the workflow — no retriesthrownewFatalError("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 Status

construn=awaitengine.getRun(runId);// { id, workflowId, status, data, result, steps, progress: { completed, total, percentage }}awaitengine.cancelRun(runId);awaitengine.retryRun(runId);

Hono REST Adapter

import{Hono}from"hono";import{chiselHono}from"chisel-engine/hono";constapp=newHono();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

Chisel Studio

A real-time dashboard for monitoring and managing your workflows.

npm install chisel-studio
import{createStudio}from"chisel-studio";conststudio=createStudio(engine,{port: 4040});awaitstudio.start();// → Chisel Studio running at http://localhost:4040

Features:

  • Real-time activity feed — live SSE stream of workflow and step events
  • Step trace visualization — waterfall timeline showing step durations and status
  • Workflow management — trigger, retry, and cancel runs from the UI
  • Light & dark mode — system preference detection with manual toggle

Options:

createStudio(engine,{port: 4040,// default: 4040host: "localhost",// default: "localhost"open: true,// auto-open in browser (default: false)});

Configuration

constengine=createEngine({// Redis connectionconnection: {host: "localhost",port: 6379},// or: connection: { url: "redis://..." },// Defaults applied to all workflowsdefaults: {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 middlewaremiddleware: {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 retriesbackoff: {type: "exponential",delay: 1000},// backoff strategytimeout: 120_000,// workflow timeout (ms)priority: 1,// lower = higher priorityconcurrency: {limit: 1,key: (data)=>data.tenantId,// keyed concurrency},input: zodSchema,// optional input validation});

License

MIT

About

lightweight workflow engine with superior DX

Resources

Stars

23 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages