Concepts

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);
});

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.

FieldTypeDescription
credentialstringThe connection's plaintext, decoded from the wire. Duraton treats it as opaque - see the shapes below for what it holds.
authShapeConnectionAuthShapeWhich 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.

authShapecredential holds
api_keyThe 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_authThe credential itself.
oauth2{"access_token": "...", "refresh_token": "..."}, written and refreshed by Duraton.
oauth1, smtp, incoming_webhookWhatever 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 } },
);

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.

On this page