Quickstart
Create a workspace, connect a runner, and trigger your first workflow on Durablex Cloud.
You'll write a workflow that charges an order, waits, then ships it, and watch it run on Durablex Cloud - surviving retries and a runner restart without ever charging twice.
Durablex is in beta. The SDK below resolves to the current beta; pin it explicitly with
bun add @durablex/sdk@next. A stable v0.1.0 will take over the default later.
1. Create your workspace
Sign up at dx.tarqeem.cloud. A workspace is created for you, and the Overview walks you through the three steps below.
2. Issue an API key
In the console, open API Keys and issue a secret key. A secret key is read + write - your runner connects and reports results with it. It's shown once, so copy it now.
Set it in your runner's environment, along with your engine URL (both are shown in the console):
export DURABLEX_API_KEY="dx_live_..."
export DURABLEX_ENGINE_URL="https://dx-engine.tarqeem.cloud"3. Write a workflow
Add the SDK (this example uses Bun to run the workflow file):
bun add @durablex/sdkA workflow is a function triggered by an event. Wrap each unit of work in a step so it runs once and its result is remembered.
import { defineWorkflow } from "@durablex/sdk";
interface OrderData {
orderId: string;
}
const orderCreated = defineWorkflow<OrderData>({
name: "order.created",
retry: { maxAttempts: 3 },
handler: async (ctx) => {
const { orderId } = ctx.event.data;
const charge = await ctx.step.run("charge", () => ({
chargeId: `ch_${orderId}`,
amount: 4200,
}));
await ctx.step.sleep("settle", "10s");
const ship = await ctx.step.run("ship", () => ({
tracking: `trk_${orderId}`,
}));
return { orderId, charge, ship };
},
});Now run it. The simplest way is connect() - the runner dials the engine over a WebSocket, so there's
no port to expose, no framework, and no separate registration. It reads DURABLEX_ENGINE_URL and
DURABLEX_API_KEY from the environment you set above:
import { connect } from "@durablex/sdk";
connect({ app: "order-app", workflows: [orderCreated] });bun run runner.tsThat's the whole runner. Open Apps in the console and you'll see it connected.
Prefer HTTP? Mount it in your framework
If you already run an HTTP server, or you deploy to serverless, serve the runner over HTTP instead.
Import the adapter for your framework - each one both serves your workflows and registers them (passing
app turns on registration; the engine URL comes from DURABLEX_ENGINE_URL). Set url to the public
address the engine can reach; the route defaults to /invoke.
Bun and Next.js need only the SDK. The others also need their framework: bun add hono, elysia,
express, or fastify.
import { serve } from "@durablex/sdk/bun";
Bun.serve({ port: 6773, routes: { "/invoke": serve({ app: "order-app", url: "https://your-runner.example.com/invoke", workflows: [orderCreated] }) } });import { serve } from "@durablex/sdk/hono";
import { Hono } from "hono";
const app = new Hono();
app.post("/invoke", serve({ app: "order-app", url: "https://your-runner.example.com/invoke", workflows: [orderCreated] }));
Bun.serve({ port: 6773, fetch: app.fetch });import { serve } from "@durablex/sdk/elysia";
import { Elysia } from "elysia";
new Elysia().post("/invoke", serve({ app: "order-app", url: "https://your-runner.example.com/invoke", workflows: [orderCreated] })).listen(6773);import { serve } from "@durablex/sdk/express";
import express from "express";
const app = express();
app.post("/invoke", serve({ app: "order-app", url: "https://your-runner.example.com/invoke", workflows: [orderCreated] }));
app.listen(6773);import { serve } from "@durablex/sdk/fastify";
import Fastify from "fastify";
const app = Fastify();
app.register(serve({ app: "order-app", url: "https://your-runner.example.com/invoke", workflows: [orderCreated] }));
app.listen({ port: 6773 });import { serve } from "@durablex/sdk/next";
import { orderCreated } from "../../../workflows";
export const { POST } = serve({
app: "order-app",
url: "https://your-app.example.com/api/durablex",
workflows: [orderCreated],
});import { toNodeHandler } from "@durablex/sdk/node";
import { createServer } from "node:http";
createServer(toNodeHandler({ app: "order-app", url: "https://your-runner.example.com/invoke", workflows: [orderCreated] })).listen(6773);A serve-mode runner must be reachable at its url so the engine can POST invokes to it; connect()
avoids that by dialing out instead.
4. Trigger it
Send the event the workflow listens for - with the SDK client, or over the REST API authenticated with your key:
import { createClient } from "@durablex/sdk";
const durablex = createClient({
engineUrl: process.env.DURABLEX_ENGINE_URL!,
apiKey: process.env.DURABLEX_API_KEY,
});
await durablex.events.send({
name: "order.created",
app: "order-app",
data: { orderId: "A1" },
});curl -X POST "$DURABLEX_ENGINE_URL/events" \
-H "Authorization: Bearer $DURABLEX_API_KEY" \
-d '{"name":"order.created","app":"order-app","data":{"orderId":"A1"}}'5. Watch it run
Open Runs in the console. charge completes, the run waits out the sleep, then ship completes
and the run succeeds - streaming in live as it executes.
6. See durable execution
While the run is sleeping, stop your runner (Ctrl-C) and start it again with bun run runner.ts. The
engine holds the run, then re-invokes when your runner reconnects - and charge does not run a
second time, because its result was saved on the first pass. That's durable execution: your code
survives restarts without losing progress or repeating work.