Claude Code for Temporal Workflows: Durable Execution and Long-Running Processes — Claude Skills 360 Blog
Blog / Backend / Claude Code for Temporal Workflows: Durable Execution and Long-Running Processes
Backend

Claude Code for Temporal Workflows: Durable Execution and Long-Running Processes

Published: September 6, 2026
Read time: 10 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free