Getting Started

Installation

npm install @yellatech/engine

Hello World

import { createEngine } from '@yellatech/engine'

const engine = createEngine()

engine.register('greet', 'user:signup', async (ctx) => {
  const payload = ctx.payload as { name: string }
  console.log(`Welcome, ${payload.name}!`)
  return { success: true }
})

// emitAndWait emits the event, then waits for the engine to go idle.
// In a real service, you'd just call engine.emit(), the dispatcher processes
// runs in the background. emitAndWait is useful for scripts and tests.
await engine.emitAndWait('user:signup', { name: 'Alice' })
await engine.stop()

With SQLite Persistence

const engine = createEngine({
  store: { type: 'sqlite', path: './my-app.db' },
})

Or set STATE_DB_PATH environment variable:

STATE_DB_PATH=./my-app.db node app.js

The process() API provides a cleaner syntax with named parameters and built-in helpers:

import { createEngine } from '@yellatech/engine'

const engine = createEngine()

engine.process({
  name: 'greet',
  on: 'user:signup',
  run: async (ctx) => {
    const payload = ctx.payload as { name: string }
    console.log(`Welcome, ${payload.name}!`)
    return ctx.ok()
  },
})

// Same as above, emitAndWait is a convenience for scripts.
// In a server, just call engine.emit() and let the dispatcher handle it.
await engine.emitAndWait('user:signup', { name: 'Alice' })
await engine.stop()

ctx.ok() and ctx.fail() replace manual { success: true/false } construction. Both register() and process() coexist, use whichever you prefer.

emit() vs emitAndWait() vs drain()

In a running service, use emit(), it enqueues work and returns immediately:

engine.emit('user:signup', { name: 'Alice' })
// returns immediately, dispatcher processes in the background

drain() waits for all in-flight runs to finish. Useful as a sync point or for graceful shutdown:

engine.emit('order:placed', { item: 'widget' })
engine.emit('order:placed', { item: 'gadget' })

// wait for everything to settle
await engine.drain()
console.log(`Done: ${engine.countByState('completed')} runs completed`)

emitAndWait() is shorthand for emitting an event and then waiting for the engine to go idle, handy in scripts and tests.

Dev Dashboard

Add server to see a live dashboard of your processes, runs, and traces:

const engine = createEngine({
  server: { port: 3000 },
})

Open http://localhost:3000 in your browser. The dashboard is intended for development and operator visibility; see Dashboard for the full UI walkthrough and route surface, and Configuration for the server option itself.

Declaring Event Flow

When processes chain events via triggerEvent or ctx.ok(..., { emit }), you can declare what each process emits. This builds a static event graph visible in the dashboard’s Graph tab and via engine.getGraph():

engine.process({
  name: 'validate',
  on: 'order:new',
  emits: ['order:validated'],
  run: async (ctx) => {
    // ... validation logic
    return ctx.ok(ctx.payload, { emit: 'order:validated' })
  },
})

engine.process({
  name: 'charge',
  on: 'order:validated',
  emits: ['order:charged'],
  run: async (ctx) => {
    const payload = ctx.payload as { total: number }
    const charge = await ctx.effect({
      key: 'stripe-charge',
      run: () => stripe.charges.create({ amount: payload.total }),
    })
    return ctx.ok({ chargeId: charge.id }, { emit: 'order:charged' })
  },
})

// Inspect the topology
const graph = engine.getGraph()
// graph.edges: [{ from: 'validate', event: 'order:validated', to: 'charge' }, ...]

The emits field is optional, processes work without it. But declaring it enables the Graph tab to show how your processes connect.

What Happens

  1. register (or process) maps the handler greet to the event user:signup.
  2. emitAndWait creates a run in idle state, then waits for the engine to go idle.
  3. The dispatcher claims the run, transitions it to running, and calls your handler.
  4. Your handler returns { success: true } (or ctx.ok()), and the run transitions to completed.