Durable workflows and agents on Temporal — you define workflow() and agent(); you never import Temporal.
Durion is an SDK for durable AI execution. You get replay-safe workflows and agents that call LLMs and tools, with cost tracking and optional observability. It is built on Temporal and the Vercel AI SDK. You write workflow() and agent() with ctx.model() and ctx.tool(); the SDK turns them into Temporal workflows and activities so runs survive restarts and scale.
End-user guides (getting started, concepts, env vars, streaming, troubleshooting) and the Gateway API v0 spec are in docs/README.md.
- Positioning: Why Durion? — how this compares to using the Vercel AI SDK alone, rolling your own Temporal + AI SDK, and Temporal’s
@temporalio/ai-sdkbridge.
Durion splits authoring (workflow / agent), execution (worker + Temporal), and optional HTTP + UI (reference gateway + React). The SDK does not run inside the browser; only @durion/react does, talking to your or the reference gateway.
flowchart TB
subgraph clients [Clients]
ReactApp["React @durion/react\nuseRunStream · useSendSignal"]
NodeClient["Node or CLI\ncreateClient Temporal gRPC"]
end
subgraph gatewayLayer [Optional HTTP]
ExampleSrv["example-server\nGateway API v0"]
end
subgraph durionRuntime [Durion runtime in your process]
WorkerProc["Worker createWorker or createApp"]
WfBundle["Workflow bundle workflow agent"]
end
subgraph external [Infrastructure]
TemporalSrv["Temporal server"]
RedisBus["Redis StreamBus\n(pub/sub)"]
LLMProviders["LLMs and tools"]
end
ReactApp -->|"REST: start, signal, stream-state"| ExampleSrv
ReactApp -->|"SSE: token-stream"| ExampleSrv
NodeClient -->|"Temporal gRPC"| TemporalSrv
NodeClient -.->|"optional: same REST API"| ExampleSrv
ExampleSrv -->|"start, query, signal"| TemporalSrv
ExampleSrv -->|"subscribe, relay to client"| RedisBus
WfBundle -.->|"bundled by"| WorkerProc
WorkerProc -->|"poll task queue"| TemporalSrv
WorkerProc -->|"LLM tool activities"| LLMProviders
WorkerProc -->|"publish token chunks"| RedisBus
useRunStream already opens the token SSE and polls stream-state (same Gateway v0 paths). Use useGatewayStreamState + useGatewayTokenStream only when you need separate control; useWorkflowStreamState / useWorkflowTokenStream are fully custom URL escape hatches.
@durion/sdk: workflow/agent definitions, worker (createWorker/createApp),createClient, streaming helpers (pipeStreamToResponse,RedisStreamBus). Runs in Node next to Temporal workers.example-server: reference gateway only — maps HTTP/SSE to Temporal and subscribes to Redis for token relay. Swap or omit it if you expose your own API.@durion/react:useRunStream,useSendSignal, Gateway v0 helpers (useGatewayStreamState,useGatewayTokenStream, URL builders), and low-level hooks above.
Install the core SDK in your worker or server process:
npm install @durion/sdkFor React frontends, also install the React package (requires React 18+ and @durion/sdk as peer dependencies):
npm install @durion/reactNote:
@durion/sdkhas a peer dependency on a running Temporal server. For local development, use the Temporal CLI:temporal server start-dev.
1. Start Temporal
Use a local Temporal dev server (for example the Temporal CLI: temporal server start-dev) or your own deployment. Default address is localhost:7233 — match TEMPORAL_ADDRESS in .env.
2. Environment
cp .env.example .envSet at least: TEMPORAL_ADDRESS=localhost:7233, TEMPORAL_NAMESPACE=default, API_PORT=3000, and OPENAI_API_KEY (or GEMINI_API_KEY for Gemini-based examples).
3. Install and run
npm install
cd examples && npm install && cd ..
npm run buildTerminal 1 — run the customer-support example worker (from examples/):
cd examples && npm run worker:customer-supportTerminal 2 — start the example API:
npm run api4. Test it
Start a workflow:
curl -s -X POST http://localhost:3000/workflows/start \
-H "Content-Type: application/json" \
-d '{"workflowType":"customerSupport","input":{"message":"I want a refund","orderId":"ORD-123"}}'Use the returned workflowId to get the result:
curl -s http://localhost:3000/runs/<workflowId>/resultMore workflows and agents, and which env vars each needs, are in examples/README.md.
Define workflows and agents in a file that Temporal will bundle (use the SDK workflow entry point only):
// workflows.ts
import { workflow, agent } from '@durion/sdk/workflow';
export const myWorkflow = workflow('myWorkflow', async (ctx) => {
const reply = await ctx.model('fast', { prompt: ctx.input.prompt });
return { reply: reply.result, cost: ctx.metadata.accumulatedCost };
});
export const myAgent = agent('myAgent', {
model: 'fast',
instructions: 'You are a helpful assistant.',
tools: ['my_tool'],
maxSteps: 8,
});In your worker entry, register models and tools with createRuntime, then create the worker:
// worker.ts
import { z } from 'zod';
import { openai } from '@ai-sdk/openai';
import { createRuntime, createWorker } from '@durion/sdk';
createRuntime({
models: { fast: openai.chat('gpt-4o-mini') },
tools: [
{
name: 'my_tool',
description: 'Does something useful',
input: z.object({ q: z.string() }),
output: z.object({ answer: z.string() }),
execute: async ({ q }) => ({ answer: `Result for ${q}` }),
},
],
});
const handle = await createWorker({
workflowsPath: require.resolve('./workflows'),
taskQueue: 'my-queue',
});
await handle.run();Workflows and agents are Temporal workflows; activities run your model and tool calls. Each ctx.model() and ctx.tool() is durable — if the worker stops, the run resumes from the last step.
createApp builds one RuntimeContext and wires the same taskQueue / Temporal settings to createWorker() and a cached client():
import { createApp } from '@durion/sdk';
import { openai } from '@ai-sdk/openai';
const app = await createApp({
models: { fast: openai.chat('gpt-4o-mini') },
tools: [/* ... */],
workflowsPath: require.resolve('./workflows'),
taskQueue: 'my-queue',
});
const worker = await app.createWorker();
// Dedicated worker process: await worker.run() (blocks until shutdown).
// Same Node process as an HTTP server: start the server first, then await worker.run()
// at the bottom of main() — see examples/streaming/server.ts.For a separate API or CLI that only starts runs, use createClient below — do not call createApp again unless you want a second full app instance.
Use createClient and the type-safe start() method with a direct function reference:
// client.ts
import { createClient } from '@durion/sdk';
import { myWorkflow, myAgent } from './workflows';
const client = await createClient({ taskQueue: 'my-queue' });
const handle = await client.start(myWorkflow, { input: { prompt: 'Hello' } });
const result = await handle.result();
const agentHandle = await client.start(myAgent, { input: { message: 'Help me plan a trip' } });
const agentResult = await agentHandle.result();
await client.close();taskQueue on createClient sets the default for all starts; you can override per call. For REST/HTTP bridges where the workflow type is a string, use client.startWorkflow('myWorkflow', { input }).
Spec: docs/gateway-api-v0.md (Gateway API v0 — /v0/runs/..., /v0/workflows/...). Install @durion/react and use useGatewayStreamState + useGatewayTokenStream with baseURL and optional accessToken when your gateway implements v0 (reference: example-server). Hook and URL helper names omit “v0”; paths still use /v0/....
import { useGatewayStreamState, useGatewayTokenStream } from '@durion/react';
function RunProgress({ workflowId, apiBase }: { workflowId: string; apiBase: string }) {
const { state, error, loading } = useGatewayStreamState({
workflowId,
baseURL: apiBase,
pollIntervalMs: 1500,
});
if (error) return <p>{error.message}</p>;
if (!state) return <p>{loading ? 'Loading…' : 'No data'}</p>;
return (
<pre>
{state.status} — step {state.currentStep ?? '—'}
{state.partialReply ? `\n${state.partialReply}` : ''}
</pre>
);
}For custom HTTP shapes, use low-level useWorkflowStreamState (queryFn) and useWorkflowTokenStream (getTokenStreamUrl). See packages/react/README.md and examples/react-hitl-ui.
Workflows and agents can call each other via ctx.run(). It executes a child workflow on the same task queue and returns its result directly.
// workflows.ts
import { workflow, agent } from '@durion/sdk/workflow';
export const researcher = agent('researcher', {
model: 'fast',
instructions: 'You research topics thoroughly.',
tools: ['web_search'],
});
export const summarizer = workflow('summarizer', async (ctx) => {
const result = await ctx.model('fast', { prompt: `Summarize: ${ctx.input.text}` });
return { summary: result.result };
});
// Parent workflow calling both
export const pipeline = workflow('pipeline', async (ctx) => {
const research = await ctx.run(researcher, { message: ctx.input.topic });
const summary = await ctx.run(summarizer, { text: research.reply });
return summary;
});Agents can also delegate to other agents or workflows as tools using delegates:
export const orchestrator = agent('orchestrator', {
model: 'reasoning',
instructions: 'You coordinate research and summarization.',
tools: ['format_output'],
delegates: [
{ name: 'research', description: 'Deep research on a topic', fn: researcher },
],
});When the model calls the research tool, the SDK executes researcher as a child workflow and returns the result to the model's tool loop.
| Path | Description |
|---|---|
packages/sdk |
Core SDK: workflow(), agent(), createRuntime(), createWorker(), createClient(), createApp() |
packages/react |
React hooks: useWorkflowStreamState (poll stream state via your API) |
packages/eval |
Optional evaluation plugin (capture runs, datasets, metrics) |
example-server/ |
Reference REST API to start workflows/agents, stream state, token SSE (Redis), and signals |
examples/ |
Per-example workers and workflows (ReAct, multi-agent, etc.); see examples/README.md |
examples/react-hitl-ui/ |
Vite + React: HITL + token streaming against example-server — examples/react-hitl-ui/README.md |
- Node.js 18+
- A Temporal server on
TEMPORAL_ADDRESS(defaultlocalhost:7233; e.g. Temporal CLItemporal server start-devor Docker)
| Script | Description |
|---|---|
npm run build |
Build all packages |
npm run api |
Start the example API server |
npm run api:dev |
Start the API with ts-node |
npm run ui:hitl |
Vite app for HITL + SSE token streaming (examples/react-hitl-ui/README.md) |
| (examples) | Example workers and clients — run from examples/ with npm run <script>; see examples/README.md |
npm run test |
Run SDK tests |
npm run eval:build-dataset |
Build evaluation dataset (optional) |
npm run eval:run |
Run evaluation metrics (optional) |
Enable tracing and metrics by passing true or false when you call initObservability() in your worker or server. The SDK emits ai.run_model and ai.run_tool spans (OTLP) and metrics such as ai_model_calls_total, ai_model_tokens_total, and ai_model_cost_usd_total (default port 9464). You can send traces to any OTLP-compatible backend (e.g. Jaeger) and scrape metrics with Prometheus and visualize with Grafana if you like; see docker-compose.metrics.yml for an optional stack.
// worker.ts — enable tracing and metrics in code
import { initObservability } from '@durion/sdk';
initObservability({
tracing: { enabled: true },
metrics: { enabled: true },
});
// ... then createRuntime, createWorker, etc.For evaluation, call initEvaluation({ enabled: true, dbUrl: '...' }) when you want to capture runs (and ensure Postgres and the eval schema are in place). Use enabled: false or omit the call otherwise. See scripts/ and packages/eval for dataset build and run.
// worker.ts — optional evaluation (capture runs for datasets and metrics)
import { initEvaluation } from '@durion/eval';
initEvaluation({
enabled: true,
dbUrl: process.env.DURION_EVAL_DB_URL,
defaultVariantName: 'baseline',
});
// ... rest of worker setupEarly-stage. APIs and internals may change. See CHANGELOG.md and docs/why-durion.md for release notes and comparisons to related stacks.
Contributions are welcome. This project is under the MIT License.