Yella Engine

Your agent is 10 steps deep when the process dies. Now what?

A durable TypeScript runtime for agents and automations. LLM calls, tool results, and side effects are stored as they complete. Crash? Restart? Pick up where you left off. No platform, no infrastructure, no vendor lock-in. Just a library you import and own.

Completed Effects Don't Re-Run

Your agent calls an LLM. Parses the response. Calls a tool. Then the process dies. Did the LLM call go through? Will it run again on restart and bill you twice?

ctx.effect() stores the result when it completes successfully. Next attempt? Returns what already worked instead of running again. No replay engine, no duplicate LLM calls in the common case, no rebuilding state by hand.

await ctx.effect({
  key: 'llm-analyze',
  run: () => llm.chat('Summarize this document'),
})
// completed once? retries return the stored result.
// common case: $0.15 well spent, not $0.30.
stateDiagram-v2 [*] --> idle: emit() idle --> running: dispatcher claims running --> completed: handler succeeds running --> errored: failed / dead-letter running --> idle: retry / lease reclaim errored --> idle: retry / requeue

Just TypeScript

Most durable execution frameworks force deterministic, replayable code. No Math.random(), no new Date(), no branching on external state.

Yella doesn't replay anything. Your handler re-executes from the top on every retry. Effects that already completed return their stored results, the function doesn't run again.

Write normal async functions. Use any library. Branch however you want. There's no SDK to learn, no execution model to study. If your code runs in Node.js today, it runs with Yella tomorrow.

See Everything

Your agent branched three times and failed on step 8. Correlation IDs link a single event through every handler it triggered. One config line enables a live dashboard with traces, graph views, deferred pauses, and dead-letter visibility.

Handlers re-execute from the top, but completed effects are replayed from storage. That makes failed steps inspectable and repairable without building your own workflow UI.

createEngine({ server: { port: 3400 } })

live trace

Why not just...

async/await?

You write try/catch. You add a database to track what already ran. You build retry logic with exponential backoff. You handle idempotency with deduplication keys. You handle crashes by hoping they don't happen. Six months in, half your codebase is a workflow engine you never intended to build.

a platform?

You don't need a visual builder, a hosted service, or a framework that owns your control flow. You need a library you import. One file. No infrastructure. No accounts to create, no dashboards to manage, no billing surprises. Your code, your process, your server.

Quick Start
import { createEngine } from '@yellatech/engine'

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

engine.process({
  name: 'research',
  on: 'topic:assigned',
  run: async (ctx) => {
    // already called? returns the stored result.
    await ctx.effect({
      key: 'llm-analyze',
      run: () => llm.chat(`Analyze: ${ctx.payload.topic}`),
    })
    return ctx.ok({ analysis: true }, { emit: 'topic:analyzed' })
  },
})

engine.emit('topic:assigned', { topic: 'durable execution models' })
// drain() waits for all runs to finish, only needed in scripts and tests.
// in a server, emit() returns immediately and the dispatcher handles the rest.
await engine.drain()

Also See

Yella Conduit

A connector framework built on Engine. Standard interface for Discord, email, webhooks, cron: with all of Engine's guarantees. Get started with Conduit.

Blog

Engineering updates, design decisions, and deep dives. Read the latest.