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: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:
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:
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: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:
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:
inngest.send is duraton.events.send({ name, app, data }). See
Events.