connect dials Duraton over a WebSocket and receives invokes on that socket, so the runner needs no
inbound URL, no framework, and no separate registration - it works from behind NAT, from a laptop, or
from a container with no ingress.
A connect runner authenticates with your API key on the WebSocket upgrade.
ConnectOptions
AnyWorkflowDefinition[]
required
The workflows this runner serves. Their manifest is registered over the socket on every (re)connection.
string
default:"DURATON_URL, then https://run.duraton.dev"
Your Duraton base URL; the WebSocket URL is derived from it.
string
default:"DURATON_APP, then \"default\""
The app this runner belongs to; its workflow names are registered under that app.
string
A stable runner id. Declare it to receive pinned runs; omit it to join the app’s anycast pool.
string
default:"DURATON_API_KEY"
The project API key, sent as a Bearer token on the WebSocket upgrade.
ProviderResolver
default:"the built-in registry (getProvider)"
Swaps how a step.ai call turns a provider name into an adapter. See AI steps - Providers.
CostSource
default:"unset; cost stays absent"
Prices each step.ai call from its metering axes, so maxCost caps bite. See AI steps - Cost.
AICache
default:"createMemoryCache() - a process-local LRU"
The store backing opt-in inference caching. Inert until a call passes cache. See AI steps - Inference cache.
Middleware
default:"unset; no hooks run"
Hooks that run before and after your handler: onInvoke enriches per-run log context, onResult transforms the result or error before it leaves the process.
ConnectHandle
connect returns synchronously - it does not await the socket - so the handle is available before the
first invoke arrives.
() => void
required
Closes the socket and stops reconnecting. A closed handle never redials.
Pinned and anycast runners
Declaring arunner id makes the runner addressable: runs pinned to that id are delivered to it, and
ctx.step.runWorkflow({ runner }) can target it. Omitting runner puts the process in the app’s
anycast pool, where any replica may take any run - the right shape for stateless replicas you scale
horizontally.
Reconnection
The socket redials on close with exponential backoff, starting at 500ms and doubling to a ceiling of 30s; a successful open resets the backoff to 500ms. Each reconnection re-sends the workflow manifest, so a runner that comes back is immediately eligible for runs again - you do not re-register.Liveness
A dropped socket that firesclose is easy - the reconnect above handles it. The harder failure is a
half-open (zombie) socket: the TCP connection is silently dead (a proxy evicted an idle connection,
a NAT mapping expired, the network black-holed), yet readyState stays OPEN and no close ever
fires. Left alone, the runner looks connected while invokes go nowhere.
The runner defeats this with an application-level heartbeat:
- It sends a
pingframe on an interval, capped at the server’s advertised heartbeat so the cadence never drifts slower than the interval Duraton expects to hear from you on. - Any inbound frame - a
pong, an invoke, a result - clears a pong watchdog, since any frame proves the socket still delivers server to client. - If the watchdog fires (no frame arrived within the pong timeout of a ping), the runner tears the
socket down immediately rather than waiting on a
closethat a black-holed socket may never send, then reconnects. So a zombie surfaces within roughlyping + pong timeout(~35s by default), not after the OS TCP timeout minutes later.
number
default:"DURATON_CONNECT_PING_MS, then 25000"
App-level ping cadence, capped at the server heartbeat. A half-open socket surfaces within pingIntervalMs + pongTimeoutMs.
number
default:"DURATON_CONNECT_PONG_TIMEOUT_MS, then 10000"
How long a ping waits for any inbound frame before the socket is treated as dead.
number
default:"DURATON_CONNECT_RECONNECT_INITIAL_MS, then 500"
The initial backoff before the first redial after a drop.
number
default:"DURATON_CONNECT_RECONNECT_MAX_MS, then 30000"
The ceiling the doubling backoff is capped at.
Runtime
connect uses the runtime’s global WebSocket when one exists. Bun, Deno, Cloudflare Workers, and
Node 22+ ship it, so on those there is no extra dependency:
WebSocket. The SDK detects this and falls back to the optional
ws package, imported lazily so it stays out of the graph
everywhere else - install it and connect uses it automatically:
Streaming AI steps (
step.ai.generate({ stream: true })) use the live channel this socket provides.
See AI steps.