Strait Docs
SDKs

Effect-first TypeScript SDK with generated operations, authoring DSL, and XState FSMs.

The TypeScript SDK (@strait/ts) is an Effect-first SDK with full API coverage, a code-first authoring DSL, and XState state machines for run lifecycle management.

Installation

npm install @strait/ts
# or
bun add @strait/ts

Client Creation

import { createClientFromConfigFile } from "@strait/ts/node";

const client = await createClientFromConfigFile();

See Configuration for strait.json schema and options.

From environment variables

import { createClientFromEnv } from "@strait/ts/node";

// Reads STRAIT_BASE_URL, STRAIT_API_KEY, STRAIT_AUTH_TYPE, STRAIT_TIMEOUT_MS
const client = createClientFromEnv();

Inline

import { createClient } from "@strait/ts";

const client = createClient({
  baseUrl: "https://api.strait.dev",
  auth: { type: "bearer", token: process.env.STRAIT_API_KEY! },
  timeoutMs: 30_000,
});

Calling Operations

High-level methods

const job = await client.createJob({
  body: {
    project_id: "proj_1",
    name: "Sync inventory",
    slug: "sync-inventory",
    endpoint_url: "https://worker.example/jobs/sync",
  },
});

Namespaced methods

const jobs = await client.jobs.list({ query: { project_id: "proj_1" } });

Result variants

Non-GET operations have *Result variants that return { ok, data, error } instead of throwing:

const result = await client.createJobResult({
  body: { project_id: "proj_1", name: "...", slug: "...", endpoint_url: "..." },
});

if (!result.ok) {
  console.error(result.error);
}

Authoring DSL

Defining jobs

import { defineJob, zodSchema } from "@strait/ts";
import { z } from "zod";

const syncInventory = defineJob({
  name: "Sync Inventory",
  slug: "sync-inventory",
  endpointUrl: "https://worker.dev/jobs/sync",
  projectId: "proj_1",
  schema: zodSchema(z.object({ sku: z.string() })),
  maxConcurrency: 5,
  maxAttempts: 5,
  retryStrategy: "exponential",
  timeoutSecs: 300,

  run: async (payload, ctx) => {
    ctx.logger.info("Starting sync", { sku: payload.sku });
    await ctx.reportProgress(0.5);
    const result = await fetchInventory(payload.sku);
    return { synced: true, count: result.items.length };
  },

  onSuccess: async ({ output }) => console.log("Synced", output.count, "items"),
  onFailure: async ({ error }) => alertOncall(error),
});

// Register, trigger, wait
await syncInventory.register(client);
const run = await syncInventory.trigger(client, { payload: { sku: "ABC-123" } });
const completed = await syncInventory.triggerAndWait(client, { payload: { sku: "ABC-123" } });

Defining workflows

import { defineWorkflow, step } from "@strait/ts";

const pipeline = defineWorkflow({
  name: "Order Pipeline",
  slug: "order-pipeline",
  projectId: "proj_1",
  steps: [
    step.job("validate", "job_validate"),
    step.job("charge", "job_charge", { dependsOn: ["validate"] }),
    step.approval("review", { dependsOn: ["charge"], approvalTimeoutSecs: 3600 }),
    step.waitForEvent("shipping", "shipping.confirmed", { dependsOn: ["review"] }),
    step.sleep("cooldown", 60, { dependsOn: ["shipping"] }),
  ],
});
// DAG is validated at definition time

Composition Helpers

import { withRetry, waitForRun, paginate, collectAll } from "@strait/ts";

// Retry with jitter
const run = await withRetry(
  () => client.triggerJob({ pathParams: { jobID: "job_1" }, body: { payload: { sku: "A" } } }),
  { attempts: 5, delayMs: 250, jitter: "full" }
);

// Wait for run completion
await waitForRun(client.getRun, run.id, { timeoutMs: 120_000 });

// Paginate
for await (const r of paginate((q) => client.listRuns({ query: q }))) {
  console.log(r.id, r.status);
}
const all = await collectAll(paginate((q) => client.listRuns({ query: q })));

FSM State Machines

XState v5 machines for run lifecycle validation and UI state management:

import { canTransitionRun, isTerminalRunStatus, runMachine } from "@strait/ts";
import { createActor } from "xstate";

canTransitionRun("executing", "COMPLETE"); // true
isTerminalRunStatus("completed"); // true

const actor = createActor(runMachine);
actor.start();
actor.send({ type: "ENQUEUE" });
actor.send({ type: "DEQUEUE" });
actor.send({ type: "EXECUTE" });
actor.getSnapshot().value; // "executing"

Middleware

const client = createClient({
  baseUrl: "https://api.strait.dev",
  auth: { type: "bearer", token: "..." },
}, {
  middleware: [{
    onRequest: ({ method, url }) => console.log(`-> ${method} ${url}`),
    onResponse: ({ status, durationMs }) => console.log(`<- ${status} (${durationMs}ms)`),
    onError: ({ error }) => console.error(error),
  }],
});

Error Handling

All errors extend Effect's Data.TaggedError:

import { NotFoundError, UnauthorizedError, RateLimitedError } from "@strait/ts";

try {
  await client.getJob({ pathParams: { jobID: "nonexistent" } });
} catch (e) {
  if (e instanceof NotFoundError) console.log("Not found:", e.message);
  if (e instanceof RateLimitedError) console.log("Rate limited:", e.message);
}
ErrorHTTP StatusDescription
TransportErrorNetwork failure
DecodeErrorJSON decode failure
ValidationErrorConfig/input validation
UnauthorizedError401, 403Auth failure
NotFoundError404Resource not found
ConflictError409Duplicate/conflict
RateLimitedError429Rate limit exceeded
ApiErrorotherGeneric HTTP error
TimeoutErrorPolling timeout
DagValidationErrorInvalid workflow DAG
Was this page helpful?

On this page