Temporal
"Temporal: durable execution, workflows, activities, signals, queries, retries, timers, TypeScript SDK"
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 linesTemporal
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.
startToCloseTimeoutcaps individual attempt duration.scheduleToCloseTimeoutcaps 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
sleeporconditionfor 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
Related Skills
BullMQ
"BullMQ: Redis-based job queue, workers, delayed jobs, rate limiting, job priorities, repeatable jobs, concurrency, dashboard"
Faktory
"Faktory: polyglot background job server, language-agnostic workers, job priorities, retries, scheduled jobs, batches, middleware, Web UI"
Graphile Worker
"Graphile Worker: PostgreSQL-backed job queue, no Redis needed, cron jobs, batch jobs, task isolation, migrations, LISTEN/NOTIFY, Node.js"
Inngest
"Inngest: event-driven functions, durable workflows, step functions, retries, cron, fan-out, sleep, Next.js integration"
Quirrel
"Quirrel: job queue for serverless/edge, cron jobs, delayed jobs, repeat scheduling, Next.js/Remix/SvelteKit integration, type-safe queues"
Trigger.dev
"Trigger.dev: background jobs for Next.js/Node, long-running tasks, integrations, retry, cron, dashboard, v3 SDK"