Temporal solves the hard problem of distributed systems: what happens when your multi-step process spans hours or days, and any step can fail? Traditional approaches — polling loops, queues, cron jobs, manual retry logic — all have gaps. Temporal’s durable execution model means your code runs to completion even if the worker crashes mid-execution, the database goes down, or a third-party API times out for 20 minutes.
Claude Code generates Temporal workflows correctly — understanding the determinism constraints on workflow code, the separation between workflows and activities, and patterns for testing durable execution.
Core Concepts Setup
CLAUDE.md for Temporal Projects
## Temporal Configuration
- Temporal TypeScript SDK (@temporalio/workflow, @temporalio/activity, @temporalio/worker, @temporalio/client)
- Namespace: default (dev), production (prod)
- Task queue: order-processing
- Workflow files: src/workflows/ — MUST be deterministic (no I/O, no Date.now(), no Math.random())
- Activity files: src/activities/ — all I/O goes here (DB, API calls, email)
- Worker: src/worker.ts — registers workflows and activities
- Temporal dev server: `temporal server start-dev` (local)
## Critical Rules
- Workflows: deterministic only — no direct I/O, use workflow.sleep() not setTimeout
- Activities: all I/O goes here, activities are retried automatically
- Never import Node.js APIs in workflow files (fs, http, etc.)
- Use workflow.log not console.log in workflows
Order Processing Workflow
Build an order processing workflow that:
1. Validates payment
2. Allocates inventory
3. Sends confirmation email
4. Ships order (may take hours)
5. Handles failures at each step with compensation
// src/workflows/orderProcessing.ts
import {
proxyActivities,
sleep,
defineSignal,
defineQuery,
setHandler,
condition,
log,
workflowInfo,
} from '@temporalio/workflow';
import type { OrderActivities } from '../activities/orderActivities';
// Proxy activities with retry policy
const activities = proxyActivities<OrderActivities>({
startToCloseTimeout: '30 seconds',
retry: {
maximumAttempts: 3,
initialInterval: '1 second',
backoffCoefficient: 2,
maximumInterval: '30 seconds',
nonRetryableErrorTypes: ['PaymentDeclinedError', 'InvalidOrderError'],
},
});
// Signals — external code can send these during execution
export const shipmentReadySignal = defineSignal<[{ trackingNumber: string }]>('shipmentReady');
export const cancelOrderSignal = defineSignal('cancelOrder');
// Queries — read workflow state without interrupting it
export const orderStatusQuery = defineQuery<OrderStatus>('orderStatus');
type OrderStatus = 'VALIDATING' | 'PAYMENT_PROCESSING' | 'ALLOCATING' | 'AWAITING_SHIPMENT' | 'SHIPPED' | 'CANCELLED';
export interface OrderWorkflowInput {
orderId: string;
customerId: string;
items: Array<{ productId: string; quantity: number; priceInCents: number }>;
paymentMethodId: string;
}
export async function processOrder(input: OrderWorkflowInput): Promise<{ success: boolean; trackingNumber?: string }> {
const { orderId } = input;
let status: OrderStatus = 'VALIDATING';
let cancelled = false;
let trackingNumber: string | undefined;
// Register query handler
setHandler(orderStatusQuery, () => status);
// Register signal handlers
setHandler(cancelOrderSignal, () => {
if (status === 'AWAITING_SHIPMENT') {
cancelled = true;
}
});
setHandler(shipmentReadySignal, ({ trackingNumber: tn }) => {
trackingNumber = tn;
});
log.info('Order processing started', { orderId, workflowId: workflowInfo().workflowId });
try {
// Step 1: Validate order
status = 'VALIDATING';
await activities.validateOrder(input);
// Step 2: Process payment
status = 'PAYMENT_PROCESSING';
const paymentResult = await activities.processPayment({
orderId,
customerId: input.customerId,
paymentMethodId: input.paymentMethodId,
amountInCents: input.items.reduce((sum, i) => sum + i.priceInCents * i.quantity, 0),
});
// Step 3: Allocate inventory
status = 'ALLOCATING';
try {
await activities.allocateInventory({ orderId, items: input.items });
} catch (err) {
// Compensate: refund if inventory allocation fails
await activities.refundPayment({ orderId, paymentIntentId: paymentResult.paymentIntentId });
throw err;
}
// Step 4: Send confirmation
await activities.sendOrderConfirmation({ orderId, customerId: input.customerId });
// Step 5: Wait for shipment (up to 7 days)
status = 'AWAITING_SHIPMENT';
await activities.notifyWarehouse({ orderId, items: input.items });
// Wait for shipment signal OR cancellation OR timeout
const shipped = await condition(
() => trackingNumber !== undefined || cancelled,
'7 days',
);
if (!shipped) {
// Timeout — escalate to customer service
await activities.escalateToSupport({ orderId, reason: 'shipment_timeout' });
return { success: false };
}
if (cancelled) {
await activities.cancelShipment({ orderId });
await activities.refundPayment({ orderId, paymentIntentId: paymentResult.paymentIntentId });
status = 'CANCELLED';
return { success: false };
}
// Shipped successfully
status = 'SHIPPED';
await activities.sendShippingNotification({ orderId, customerId: input.customerId, trackingNumber: trackingNumber! });
log.info('Order completed', { orderId, trackingNumber });
return { success: true, trackingNumber };
} catch (err) {
log.error('Order processing failed', { orderId, error: String(err) });
throw err;
}
}
Activity Implementations
// src/activities/orderActivities.ts
import { Context } from '@temporalio/activity';
import { db } from '../lib/db';
import { stripe } from '../lib/stripe';
import { mailer } from '../lib/mailer';
export interface OrderActivities {
validateOrder(input: OrderWorkflowInput): Promise<void>;
processPayment(input: PaymentInput): Promise<{ paymentIntentId: string }>;
allocateInventory(input: InventoryInput): Promise<void>;
sendOrderConfirmation(input: NotificationInput): Promise<void>;
notifyWarehouse(input: WarehouseInput): Promise<void>;
cancelShipment(input: { orderId: string }): Promise<void>;
refundPayment(input: { orderId: string; paymentIntentId: string }): Promise<void>;
sendShippingNotification(input: ShippingNotificationInput): Promise<void>;
escalateToSupport(input: { orderId: string; reason: string }): Promise<void>;
}
export const orderActivities: OrderActivities = {
async validateOrder({ orderId, customerId, items }) {
// Heartbeat for long operations — prevents timeout during large validations
Context.current().heartbeat('starting validation');
const customer = await db.findCustomer(customerId);
if (!customer) throw new Error('Customer not found'); // Will retry
for (const item of items) {
const product = await db.findProduct(item.productId);
if (!product?.active) {
// NonRetryable — retrying won't fix a deactivated product
const error = new Error(`Product ${item.productId} is not available`);
error.name = 'InvalidOrderError';
throw error;
}
}
await db.createOrder({ orderId, customerId, items, status: 'VALIDATING' });
},
async processPayment({ orderId, paymentMethodId, amountInCents }) {
// Idempotency key = orderId — safe to retry, Stripe deduplicates
const paymentIntent = await stripe.paymentIntents.create({
amount: amountInCents,
currency: 'usd',
payment_method: paymentMethodId,
confirm: true,
idempotency_key: `order-${orderId}`,
});
if (paymentIntent.status === 'canceled' || paymentIntent.last_payment_error) {
const error = new Error(`Payment declined: ${paymentIntent.last_payment_error?.message}`);
error.name = 'PaymentDeclinedError'; // NonRetryable
throw error;
}
await db.updateOrder(orderId, { paymentIntentId: paymentIntent.id, status: 'PAID' });
return { paymentIntentId: paymentIntent.id };
},
async allocateInventory({ orderId, items }) {
// Use database transaction for atomicity
await db.transaction(async (trx) => {
for (const item of items) {
const updated = await trx('inventory')
.where('product_id', item.productId)
.where('quantity', '>=', item.quantity)
.decrement('quantity', item.quantity);
if (updated === 0) {
throw new Error(`Insufficient inventory for ${item.productId}`);
}
}
await trx('orders').where('id', orderId).update({ status: 'ALLOCATED' });
});
},
async sendOrderConfirmation({ orderId, customerId }) {
const [order, customer] = await Promise.all([
db.findOrder(orderId),
db.findCustomer(customerId),
]);
await mailer.send({
to: customer.email,
template: 'order-confirmation',
data: { order, customer },
});
},
};
Worker Setup
// src/worker.ts
import { Worker, NativeConnection } from '@temporalio/worker';
import { orderActivities } from './activities/orderActivities';
import * as workflows from './workflows';
async function run() {
const connection = await NativeConnection.connect({
address: process.env.TEMPORAL_ADDRESS ?? 'localhost:7233',
});
const worker = await Worker.create({
connection,
namespace: process.env.TEMPORAL_NAMESPACE ?? 'default',
taskQueue: 'order-processing',
// Workflow bundle — Temporal bundles separately to enforce determinism
workflowsPath: require.resolve('./workflows'),
// Activities run in the worker process with full Node.js access
activities: orderActivities,
// Worker tuning
maxConcurrentWorkflowTaskExecutions: 100,
maxConcurrentActivityTaskExecutions: 50,
});
await worker.run();
}
run().catch((err) => {
console.error('Worker error', err);
process.exit(1);
});
Starting and Signaling Workflows
// src/client.ts — starting workflows and sending signals
import { Client, Connection } from '@temporalio/client';
import { processOrder, shipmentReadySignal, orderStatusQuery } from './workflows/orderProcessing';
const connection = await Connection.connect({ address: process.env.TEMPORAL_ADDRESS });
const client = new Client({ connection });
// Start a workflow
const handle = await client.workflow.start(processOrder, {
taskQueue: 'order-processing',
workflowId: `order-${orderId}`, // Deterministic ID enables deduplication
args: [{ orderId, customerId, items, paymentMethodId }],
});
console.log('Workflow started:', handle.workflowId);
// Query current status without blocking
const status = await handle.query(orderStatusQuery);
console.log('Current status:', status); // 'AWAITING_SHIPMENT'
// Send signal when warehouse ships
await handle.signal(shipmentReadySignal, { trackingNumber: 'UPS123456789' });
// Wait for result
const result = await handle.result();
console.log('Completed:', result);
Child Workflows and Schedules
The notification workflow is complex enough to be its own workflow.
Extract it and call it as a child workflow.
// src/workflows/notificationWorkflow.ts
export async function sendOrderNotifications(input: {
orderId: string;
customerId: string;
events: Array<{ type: string; timestamp: string }>;
}): Promise<void> {
const notifActivities = proxyActivities<NotificationActivities>({
startToCloseTimeout: '10 seconds',
});
for (const event of input.events) {
await notifActivities.sendNotification({ ...input, event });
await sleep('100 milliseconds'); // Rate limiting between sends
}
}
// In processOrder workflow, start child workflow
import { startChild } from '@temporalio/workflow';
const notifHandle = await startChild(sendOrderNotifications, {
args: [{ orderId, customerId, events }],
workflowId: `notifications-${orderId}`,
// Child runs independently — parent doesn't wait
parentClosePolicy: ParentClosePolicy.ABANDON,
});
Scheduled workflows (cron-style):
// Schedule a daily report
await client.schedule.create({
scheduleId: 'daily-order-report',
spec: {
cronExpressions: ['0 9 * * MON-FRI'], // 9 AM weekdays
},
action: {
type: 'startWorkflow',
workflowType: generateDailyReport,
taskQueue: 'reports',
args: [{}],
},
});
Testing Temporal Workflows
Write tests for the order processing workflow.
Test the happy path and the cancellation signal.
// src/workflows/__tests__/orderProcessing.test.ts
import { TestWorkflowEnvironment } from '@temporalio/testing';
import { Worker } from '@temporalio/worker';
import { processOrder, cancelOrderSignal, orderStatusQuery } from '../orderProcessing';
import { MockOrderActivities } from './__mocks__/orderActivities';
describe('processOrder workflow', () => {
let env: TestWorkflowEnvironment;
beforeAll(async () => {
// Auto-skip time for sleep/condition calls
env = await TestWorkflowEnvironment.createTimeSkipping();
});
afterAll(async () => {
await env.teardown();
});
const runWithActivities = async (activitiesOverride: Partial<MockOrderActivities> = {}) => {
const worker = await Worker.create({
connection: env.nativeConnection,
taskQueue: 'test',
workflowsPath: require.resolve('../orderProcessing'),
activities: { ...MockOrderActivities, ...activitiesOverride },
});
return worker.runUntil(
env.client.workflow.execute(processOrder, {
taskQueue: 'test',
workflowId: `test-order-${Date.now()}`,
args: [testOrderInput],
})
);
};
it('completes happy path', async () => {
const result = await runWithActivities();
expect(result.success).toBe(true);
expect(result.trackingNumber).toBe('TEST-TRACKING-123');
});
it('refunds payment when inventory allocation fails', async () => {
const refundCalled = jest.fn();
await expect(
runWithActivities({
allocateInventory: jest.fn().mockRejectedValue(new Error('Out of stock')),
refundPayment: refundCalled,
})
).rejects.toThrow('Out of stock');
expect(refundCalled).toHaveBeenCalledWith(
expect.objectContaining({ orderId: testOrderInput.orderId })
);
});
it('handles cancellation signal', async () => {
const worker = await Worker.create({
connection: env.nativeConnection,
taskQueue: 'test',
workflowsPath: require.resolve('../orderProcessing'),
activities: {
...MockOrderActivities,
notifyWarehouse: jest.fn().mockResolvedValue(undefined),
// Don't send shipmentReady to leave in AWAITING_SHIPMENT state
},
});
const handle = await env.client.workflow.start(processOrder, {
taskQueue: 'test',
workflowId: `test-cancel-${Date.now()}`,
args: [testOrderInput],
});
// Wait until AWAITING_SHIPMENT
await env.sleep('5 seconds');
// Send cancellation
await handle.signal(cancelOrderSignal);
const result = await worker.runUntil(handle.result());
expect(result.success).toBe(false);
const status = await handle.query(orderStatusQuery);
expect(status).toBe('CANCELLED');
});
});
For durable execution patterns beyond Temporal — like event sourcing and CQRS — see the event-driven architecture guide. For deploying Temporal workers on Kubernetes with autoscaling, the Kubernetes guide covers the deployment patterns. The Claude Skills 360 bundle includes Temporal workflow skill sets covering saga patterns, compensation logic, and workflow testing. Start with the free tier to try workflow scaffolding prompts.