Skip to main content
Duraton’s model lines up closely with Inngest’s: an event triggers a durable function, work happens in replay-safe steps, and flow control lives in the definition. Most of a port is mechanical renames - createFunction becomes workflow, step.run stays step.run. This guide gives you the renames first, then the three differences that compile fine and fail at runtime, which are the ones worth reading before you start.

Core primitives

Before / after

An Inngest function:
The Duraton equivalent - the handler body is unchanged; the wrapper differs, and the runner dials out with connect() instead of exposing an HTTP route:

Flow control

Every knob has a counterpart, with two renames and one shape change to watch:
Duraton also has flow control Inngest does not: singleton, plus the AI-spend controls cap and tokenThrottle. See Flow control.

What breaks silently

These three compile cleanly against the Duraton types but behave differently from Inngest at runtime - the ones that actually bite during a port.

1. Key fields are a field path, not an expression

In Inngest, idempotency (and the key on concurrency / rateLimit) is a CEL expression evaluated against the event - you can compute and concatenate:
In Duraton, every key field is a dotted field path into the event data - a lookup, never an expression. Duraton resolves it by walking the path (user.id reads data.user.id); a missing field or a non-scalar value yields the workflow-global scope, and there is no arithmetic, concatenation, or CEL:
If you need a composite key, compute it upstream and emit it as a single field on the event, then point the path at that field. This applies to concurrency.key, rateLimit.key, throttle.key, debounce.key, and batch.key too - all of them are paths, so any Inngest expression key must be flattened into one event field first.

2. Middleware has a different shape

Inngest middleware is a nested factory registered on the client, with per-run, per-step, and input/output transform hooks:
Duraton middleware is a flat object with two hooks, passed straight to connect() - there is no client object to register on and no step-level interception:
onInvoke covers Inngest’s onFunctionRun + beforeExecution; onResult covers transformOutput for the terminal result or error. There is no per-step hook and no transformInput - anything that wrapped individual steps has no Duraton equivalent and must move into the step body. The built-in sanitizeErrors() and bindLogContext() helpers ship as ready-made middleware.

3. There is no step.sendEvent - it’s step.emit

Inngest sends events from inside a function with step.sendEvent, which also accepts an array of events:
Duraton’s method is step.emit, and it sends one event per call - name is the event name, with an optional app to target a single app (omit it to broadcast project-wide) and an optional dedupeId:
Outside a run, the analog of inngest.send is duraton.events.send({ name, app, data }). See Events.