Skip to main content
Technology & EngineeringBackground Jobs Services385 lines

Temporal

"Temporal: durable execution, workflows, activities, signals, queries, retries, timers, TypeScript SDK"

Quick Summary18 lines
Temporal provides durable execution for application logic. A Temporal Workflow is a function whose execution state is automatically persisted — if the process crashes, the workflow resumes exactly where it left off. This eliminates the need for manual state machines, status columns, retry logic, and timeout handling that plague distributed systems.

## Key Points

- **Keep Workflows deterministic.** No I/O, no random numbers, no Date.now() — use Temporal's built-in time functions. All side effects belong in Activities.
- **Use Signals for external input** into running workflows. This replaces polling databases for status changes.
- **Use Queries for reading state.** Queries are synchronous reads of workflow state that do not affect execution history.
- **Set Activity timeouts carefully.** `startToCloseTimeout` caps individual attempt duration. `scheduleToCloseTimeout` caps total time including retries.
- **Use heartbeats for long Activities.** Heartbeats let Temporal detect stuck activities and retry them on a different worker.
- **Use workflow IDs for idempotency.** Starting a workflow with the same ID as a running workflow is rejected by default, preventing duplicates.
- **Implement compensation logic** (saga pattern) for multi-step workflows. If step 3 fails, undo steps 1 and 2.
- **Version workflows** using `patched()` when you need to change running workflow logic without breaking in-flight executions.
- **Calling APIs directly in Workflow code.** Workflows replay from history; a direct HTTP call would execute on every replay. Always use Activities for I/O.
- **Using mutable global state in Workflows.** Workflows must be isolated. Shared mutable state breaks determinism and causes replay corruption.
- **Very large workflow histories.** Workflows with millions of events become slow to replay. Use ContinueAsNew to reset history for long-lived workflows.
- **Not setting timeouts on Activities.** Without timeouts, a stuck Activity blocks the Workflow forever. Always set startToCloseTimeout.
skilldb get background-jobs-services-skills/TemporalFull skill: 385 lines
Paste into your CLAUDE.md or agent config

Temporal

Core Philosophy

Temporal provides durable execution for application logic. A Temporal Workflow is a function whose execution state is automatically persisted — if the process crashes, the workflow resumes exactly where it left off. This eliminates the need for manual state machines, status columns, retry logic, and timeout handling that plague distributed systems.

The key abstraction is the separation of Workflows and Activities. Workflows are deterministic orchestration functions that define the sequence and logic of work. Activities are the actual side-effectful operations (API calls, database writes, file processing). Temporal replays Workflow code to reconstruct state, and activities are executed at most once per invocation (with configurable retries). This gives you the reliability of a state machine with the readability of imperative code.

Temporal is infrastructure you operate (or use Temporal Cloud). It is best suited for systems where reliability, long-running processes, and complex coordination are critical — payment processing, order fulfillment, infrastructure provisioning, data pipelines, and human-in-the-loop workflows.

Setup

Installation and Project Structure

// npm install @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity

// Project structure:
// src/
//   workflows/     — Workflow definitions (deterministic, no I/O)
//   activities/    — Activity implementations (side effects, I/O)
//   workers/       — Worker process that runs workflows + activities
//   client/        — Client code that starts and signals workflows

Worker Configuration

// src/workers/worker.ts
import { Worker, NativeConnection } from "@temporalio/worker";
import * as activities from "../activities";

async function run() {
  const connection = await NativeConnection.connect({
    address: process.env.TEMPORAL_ADDRESS ?? "localhost:7233",
  });

  const worker = await Worker.create({
    connection,
    namespace: "default",
    taskQueue: "main-queue",
    workflowsPath: require.resolve("../workflows"),
    activities,
    maxConcurrentActivityTaskExecutions: 20,
    maxConcurrentWorkflowTaskExecutions: 10,
  });

  console.log("Worker started");
  await worker.run();
}

run().catch((err) => {
  console.error("Worker failed:", err);
  process.exit(1);
});

Client Configuration

// src/client/temporal.ts
import { Connection, Client } from "@temporalio/client";

let client: Client;

export async function getTemporalClient(): Promise<Client> {
  if (client) return client;

  const connection = await Connection.connect({
    address: process.env.TEMPORAL_ADDRESS ?? "localhost:7233",
  });

  client = new Client({ connection, namespace: "default" });
  return client;
}

Key Techniques

Defining Activities

// src/activities/index.ts
import { Context } from "@temporalio/activity";

export async function chargePayment(orderId: string, amount: number): Promise<string> {
  const heartbeatDetails = Context.current().info;
  console.log(`Charging ${amount} for order ${orderId}`);

  const result = await stripe.paymentIntents.create({
    amount: Math.round(amount * 100),
    currency: "usd",
    metadata: { orderId },
  });

  return result.id;
}

export async function reserveInventory(items: Array<{ sku: string; qty: number }>): Promise<boolean> {
  for (const item of items) {
    const stock = await db.inventory.findUnique({ where: { sku: item.sku } });
    if (!stock || stock.available < item.qty) {
      return false;
    }
  }

  // Reserve all items in a transaction
  await db.$transaction(
    items.map((item) =>
      db.inventory.update({
        where: { sku: item.sku },
        data: { available: { decrement: item.qty } },
      })
    )
  );

  return true;
}

export async function sendEmail(to: string, templateId: string, vars: Record<string, string>): Promise<void> {
  await emailService.send({ to, templateId, vars });
}

export async function generateShippingLabel(orderId: string): Promise<string> {
  // Long-running activity — use heartbeats to signal liveness
  const ctx = Context.current();

  const order = await db.order.findUniqueOrThrow({
    where: { id: orderId },
    include: { shippingAddress: true },
  });

  // Heartbeat periodically for long operations
  ctx.heartbeat("generating-label");
  const label = await shippingApi.createLabel(order.shippingAddress);

  ctx.heartbeat("uploading-label");
  await storage.upload(`labels/${orderId}.pdf`, label.pdf);

  return label.trackingNumber;
}

Defining Workflows

// src/workflows/order-fulfillment.ts
import { proxyActivities, defineSignal, defineQuery, setHandler, sleep, condition } from "@temporalio/workflow";
import type * as activities from "../activities";

// Proxy activities with retry configuration
const { chargePayment, reserveInventory, sendEmail, generateShippingLabel } = proxyActivities<typeof activities>({
  startToCloseTimeout: "30s",
  retry: {
    initialInterval: "1s",
    backoffCoefficient: 2,
    maximumAttempts: 5,
    maximumInterval: "30s",
    nonRetryableErrorTypes: ["InvalidOrderError"],
  },
});

// Signals allow external systems to send data into a running workflow
export const cancelOrderSignal = defineSignal<[string]>("cancelOrder");
export const updateShippingSignal = defineSignal<[{ address: string }]>("updateShipping");

// Queries allow reading workflow state without affecting execution
export const getOrderStatusQuery = defineQuery<OrderStatus>("getOrderStatus");

interface OrderStatus {
  step: string;
  paymentId?: string;
  trackingNumber?: string;
  cancelled: boolean;
}

export async function orderFulfillment(orderId: string, items: Array<{ sku: string; qty: number }>, amount: number): Promise<OrderStatus> {
  const status: OrderStatus = { step: "started", cancelled: false };

  // Handle signals
  setHandler(cancelOrderSignal, (reason: string) => {
    status.cancelled = true;
    status.step = `cancelled: ${reason}`;
  });

  setHandler(getOrderStatusQuery, () => status);

  // Step 1: Reserve inventory
  status.step = "reserving-inventory";
  const reserved = await reserveInventory(items);
  if (!reserved) {
    status.step = "out-of-stock";
    await sendEmail("customer@example.com", "out-of-stock", { orderId });
    return status;
  }

  // Check for cancellation between steps
  if (status.cancelled) return status;

  // Step 2: Charge payment
  status.step = "charging-payment";
  status.paymentId = await chargePayment(orderId, amount);

  if (status.cancelled) {
    // Compensating action: refund if cancelled after payment
    status.step = "refunding";
    await refundPayment(status.paymentId);
    return status;
  }

  // Step 3: Generate shipping label
  status.step = "generating-shipping-label";
  status.trackingNumber = await generateShippingLabel(orderId);

  // Step 4: Send confirmation
  status.step = "sending-confirmation";
  await sendEmail("customer@example.com", "order-shipped", {
    orderId,
    trackingNumber: status.trackingNumber,
  });

  status.step = "completed";
  return status;
}

Starting Workflows from Application Code

// app/api/orders/route.ts
import { getTemporalClient } from "@/src/client/temporal";
import { orderFulfillment } from "@/src/workflows/order-fulfillment";

export async function POST(req: Request) {
  const { orderId, items, amount } = await req.json();
  const client = await getTemporalClient();

  // Start workflow with a unique ID (prevents duplicates)
  const handle = await client.workflow.start(orderFulfillment, {
    taskQueue: "main-queue",
    workflowId: `order-${orderId}`,
    args: [orderId, items, amount],
    // Workflow-level timeout
    workflowExecutionTimeout: "24h",
  });

  return Response.json({
    workflowId: handle.workflowId,
    runId: handle.firstExecutionRunId,
  });
}

// Query workflow state
export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const orderId = searchParams.get("orderId")!;
  const client = await getTemporalClient();

  const handle = client.workflow.getHandle(`order-${orderId}`);
  const status = await handle.query("getOrderStatus");

  return Response.json(status);
}

// Send a signal to cancel
export async function DELETE(req: Request) {
  const { orderId, reason } = await req.json();
  const client = await getTemporalClient();

  const handle = client.workflow.getHandle(`order-${orderId}`);
  await handle.signal("cancelOrder", reason);

  return Response.json({ cancelled: true });
}

Timers and Conditional Waiting

// src/workflows/subscription-trial.ts
import { proxyActivities, sleep, condition, defineSignal, setHandler } from "@temporalio/workflow";
import type * as activities from "../activities";

const { sendEmail, activateSubscription, deactivateAccount } = proxyActivities<typeof activities>({
  startToCloseTimeout: "10s",
  retry: { maximumAttempts: 3 },
});

export const convertToPayingSignal = defineSignal<[{ planId: string }]>("convertToPaying");

export async function subscriptionTrial(userId: string, trialDays: number): Promise<string> {
  let converted = false;
  let planId: string | undefined;

  setHandler(convertToPayingSignal, (data) => {
    converted = true;
    planId = data.planId;
  });

  // Send welcome email
  await sendEmail(userId, "trial-started", { trialDays: String(trialDays) });

  // Wait for half the trial, then send a reminder
  await sleep(`${Math.floor(trialDays / 2)}d`);

  if (!converted) {
    await sendEmail(userId, "trial-halfway", {});
  }

  // Wait for the rest of the trial or until they convert
  const didConvert = await condition(() => converted, `${Math.ceil(trialDays / 2)}d`);

  if (didConvert && planId) {
    await activateSubscription(userId, planId);
    await sendEmail(userId, "subscription-activated", { planId });
    return "converted";
  }

  // Trial expired without conversion
  await sendEmail(userId, "trial-expired", {});

  // Give a 3-day grace period
  const lastChance = await condition(() => converted, "3d");

  if (lastChance && planId) {
    await activateSubscription(userId, planId);
    return "converted-grace-period";
  }

  await deactivateAccount(userId);
  return "expired";
}

Child Workflows

// src/workflows/batch-processing.ts
import { executeChild } from "@temporalio/workflow";
import { orderFulfillment } from "./order-fulfillment";

export async function batchFulfillment(orders: Array<{ id: string; items: any[]; amount: number }>) {
  // Run child workflows in parallel
  const results = await Promise.allSettled(
    orders.map((order) =>
      executeChild(orderFulfillment, {
        workflowId: `order-${order.id}`,
        args: [order.id, order.items, order.amount],
        taskQueue: "main-queue",
      })
    )
  );

  const succeeded = results.filter((r) => r.status === "fulfilled").length;
  const failed = results.filter((r) => r.status === "rejected").length;

  return { total: orders.length, succeeded, failed };
}

Best Practices

  • Keep Workflows deterministic. No I/O, no random numbers, no Date.now() — use Temporal's built-in time functions. All side effects belong in Activities.
  • Use Signals for external input into running workflows. This replaces polling databases for status changes.
  • Use Queries for reading state. Queries are synchronous reads of workflow state that do not affect execution history.
  • Set Activity timeouts carefully. startToCloseTimeout caps individual attempt duration. scheduleToCloseTimeout caps total time including retries.
  • Use heartbeats for long Activities. Heartbeats let Temporal detect stuck activities and retry them on a different worker.
  • Use workflow IDs for idempotency. Starting a workflow with the same ID as a running workflow is rejected by default, preventing duplicates.
  • Implement compensation logic (saga pattern) for multi-step workflows. If step 3 fails, undo steps 1 and 2.
  • Version workflows using patched() when you need to change running workflow logic without breaking in-flight executions.

Anti-Patterns

  • Calling APIs directly in Workflow code. Workflows replay from history; a direct HTTP call would execute on every replay. Always use Activities for I/O.
  • Using mutable global state in Workflows. Workflows must be isolated. Shared mutable state breaks determinism and causes replay corruption.
  • Very large workflow histories. Workflows with millions of events become slow to replay. Use ContinueAsNew to reset history for long-lived workflows.
  • Not setting timeouts on Activities. Without timeouts, a stuck Activity blocks the Workflow forever. Always set startToCloseTimeout.
  • Tight-looping in Workflows. Use sleep or condition for waiting. A busy loop generates enormous history and wastes resources.
  • Storing large data in Workflow state. Workflow state is replayed from event history. Keep state small; store large data externally and reference it by ID.

Install this skill directly: skilldb add background-jobs-services-skills

Get CLI access →