Connections
Resolve a stored third-party credential inside a step, in place of an environment variable - the credential is used only transiently and never enters a step's durable input or output.
A connection is a credential Duraton stores on your behalf (an API key, an OAuth token, SMTP
settings) so a workflow can call a third-party service without the credential living in an
environment variable. client.connections.resolve() reads one back at execution time.
Resolve inside a step
Resolution must happen inside a step.run (or step.ai.*) closure, never in the workflow
handler's top-level body. A step's input and output are durable - they are recorded and replayed
forever - so a credential resolved outside a step, then passed into one, would sit in that replayed
record permanently. Resolved inside the closure, the credential is used only for the one call that
needs it and is gone once the step returns:
import { createClient } from "@duraton/sdk";
const duraton = createClient({
url: process.env.DURATON_URL!,
apiKey: process.env.DURATON_API_KEY,
});
await ctx.step.run("publish", async () => {
const conn = await duraton.connections.resolve(connectionId);
return post(conn.credential, topic);
});from duraton.client import AsyncDuratonClient
dx = AsyncDuratonClient()
async def publish() -> object:
conn = await dx.connections.resolve(connection_id)
return await post(conn.credential, topic)
await ctx.step.run("publish", publish)import "duraton.dev/sdk-go/client"
dx := client.New(client.Options{})
_, err := duraton.Run(c, "publish", func() (any, error) {
conn, err := dx.Connections.Resolve(c.Ctx, connectionID)
if err != nil {
return nil, err
}
return post(conn.Credential, topic)
})resolve() called outside a step - directly in the handler body, or stashed in a variable before a
step.run call - throws immediately (ConnectionResolveOutsideStepError) rather than silently
handing back a credential with no durable-input protection.
| Field | Type | Description |
|---|---|---|
credential | string | The connection's plaintext, decoded from the wire. Duraton treats it as opaque - see the shapes below for what it holds. |
authShape | ConnectionAuthShape | Which shape credential holds. |
Auth shapes
authShape tells you how to read credential and how to present it to the third party. A
single-value shape is the value itself; a multi-field shape is a JSON string you parse yourself.
authShape | credential holds |
|---|---|
api_key | The key itself, with no placement implied - you decide where it goes. |
header_auth | {"name": "X-Api-Key", "value": "..."} - send value as the request header name. |
query_auth | {"name": "token", "value": "..."} - send value as the URL query parameter name. |
basic_auth | The credential itself. |
oauth2 | {"access_token": "...", "refresh_token": "..."}, written and refreshed by Duraton. |
oauth1, smtp, incoming_webhook | Whatever you stored. Duraton defines no schema for these - the bytes are yours. |
header_auth and query_auth are distinct shapes rather than an api_key with a placement
setting, because placement changes how you use the credential, not just what its bytes are - so it
travels in the shape rather than hidden inside the blob.
A replayed step never re-invokes its body, so a memoized resolve() call never runs twice and never
fails if the connection was since deleted - the credential only had to exist at the moment the step
first ran.
Retrying a rejected credential
Duraton refreshes an OAuth connection's access token ahead of its known expiry, so a normal resolve
already returns a live token. A token can still be rejected by the provider despite looking valid
(clock skew, early revocation) - catch that in the step and let the step's own retry call resolve()
again:
await ctx.step.run(
"publish",
async () => {
const conn = await duraton.connections.resolve(connectionId);
const res = await post(conn.credential, topic);
if (res.status === 401) throw new Error("credential rejected, retrying");
return res;
},
{ retry: { maxAttempts: 2 } },
);async def publish() -> object:
conn = await dx.connections.resolve(connection_id)
res = await post(conn.credential, topic)
if res.status == 401:
raise Exception("credential rejected, retrying")
return res
await ctx.step.run("publish", publish, retry=RetryConfig(max_attempts=2))_, err := duraton.Run(c, "publish", func() (any, error) {
conn, err := dx.Connections.Resolve(c.Ctx, connectionID)
if err != nil {
return nil, err
}
res, err := post(conn.Credential, topic)
if err != nil {
return nil, err
}
if res.StatusCode == 401 {
return nil, errors.New("credential rejected, retrying")
}
return res, nil
}, duraton.StepRunOptions{Retry: &duraton.RetryConfig{MaxAttempts: 2}})The second attempt calls resolve() again from scratch, picking up a refreshed token if one is due.
A token rejected well before its known expiry (the provider revoked access early) is not fixed by a
retry - the connection needs re-authorization; see Retries & failure handling
for NonRetriableError to fail fast on that case instead of spending the retry budget.
Webhooks
Let a third party start a run, and let a finished run tell the outside world - verified inbound POSTs, signed outbound ones, one durable delivery log.
Runners (connect vs serve)
Run your workflow code wherever it already lives: dial out over a WebSocket with connect, or expose an inbound HTTP endpoint with serve.