TypeScript circuit breaker example with dependency injection
// activities.ts — opossum
import CircuitBreaker from 'opossum';
export const createActivities = (breaker: CircuitBreaker) => ({
async chargeCustomer(orderId: string, amount: number): Promise<string> {
// Rejects with an EOPENBREAKER error when the breaker is open.
return breaker.fire(orderId, amount) as Promise<string>;
},
});
// worker.ts
import { Worker } from '@temporalio/worker';
import CircuitBreaker from 'opossum';
import { createActivities } from './activities';
import { PaymentAPI } from './payment-api';
async function run() {
// Construct the breaker once at Worker startup so its failure
// counters are shared across all Activity executions.
const paymentApi = new PaymentAPI('https://api.example.com');
const breaker = new CircuitBreaker(
(orderId: string, amount: number) => paymentApi.charge(orderId, amount),
{ errorThresholdPercentage: 50, resetTimeout: 30000 }, // cool-down in ms
);
const worker = await Worker.create({
taskQueue: 'payment',
workflowsPath: require.resolve('./workflows'),
activities: createActivities(breaker),
});
await worker.run();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
TypeScript Activity Dependency Injection implementation
In TypeScript, use a factory function that closes over dependencies and returns an object of Activity functions. Call proxyActivities with ReturnType<typeof createActivities> to infer the Activity types from the factory function's return type. Register the result of calling the factory function with real dependencies as the activities option when creating the Worker.
Pitfall: Forgetting to bind methods in TypeScript
When using a class instead of a factory function, class methods must be defined as arrow functions or explicitly bound in the constructor. Otherwise, this is undefined when Temporal invokes the Activity.
TypeScript Activity Dependency Injection code example
// activities.ts
export interface DB {
processPayment(orderId: string, amount: number): Promise<string>;
}
export interface EmailClient {
send(to: string, subject: string, body: string): Promise<void>;
}
export const createActivities = (db: DB, emailClient: EmailClient) => ({
async chargeCustomer(orderId: string, amount: number): Promise<string> {
const receiptId = await db.processPayment(orderId, amount);
return receiptId;
},
async sendReceipt(email: string, receiptId: string): Promise<void> {
await emailClient.send(email, 'Payment Receipt', receiptId);
},
});
// workflows.ts
import { proxyActivities } from '@temporalio/workflow';
import type { createActivities } from './activities';
// Use ReturnType to extract the Activity types from the factory function
const { chargeCustomer, sendReceipt } = proxyActivities<
ReturnType<typeof createActivities>
>({
startToCloseTimeout: '30s',
});
export async function paymentWorkflow(
orderId: string,
amount: number,
email: string
): Promise<void> {
const receiptId = await chargeCustomer(orderId, amount);
await sendReceipt(email, receiptId);
}
// worker.ts
import { Worker } from '@temporalio/worker';
import { createActivities } from './activities';
async function run() {
// Initialize dependencies at Worker startup
const db = new PostgresClient('postgres://localhost:5432/payments');
const emailClient = new SMTPClient('smtp://mail.example.com');
const worker = await Worker.create({
taskQueue: 'payment',
workflowsPath: require.resolve('./workflows'),
// Inject dependencies through the factory function
activities: createActivities(db, emailClient),
});
await worker.run();
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
Batch Iterator TypeScript implementation
import { continueAsNew, log, proxyActivities } from "@temporalio/workflow";
import type * as activities from "./activities";
import { PAGE_SIZE } from "./shared";
const { fetchPage, processRecord } = proxyActivities<typeof activities>({
startToCloseTimeout: "10 seconds",
});
export async function batchIteratorWorkflow(
offset: number = 0,
totalProcessed: number = 0
): Promise<number> {
const page = await fetchPage(offset, PAGE_SIZE);
for (const record of page) {
await processRecord(record);
totalProcessed++;
}
log.info(`Processed page at offset ${offset} (${page.length} records, running total: ${totalProcessed})`);
if (page.length === PAGE_SIZE) {
await continueAsNew<typeof batchIteratorWorkflow>(offset + PAGE_SIZE, totalProcessed);
}
return totalProcessed;
}
Asynchronous child workflow execution in TypeScript
To start a child workflow asynchronously in TypeScript, use startChild(childWorkflow, { args: [input], parentClosePolicy: ParentClosePolicy.PARENT_CLOSE_POLICY_ABANDON }). This returns a handle once the child has started without waiting for completion.
Parallel child workflows in TypeScript
To start multiple child workflows in parallel in TypeScript, use Promise.all() with an array of executeChild() calls. This starts all children concurrently and awaits all results.
Synchronous child workflow execution in TypeScript
To execute a child workflow synchronously in TypeScript, use executeChild(childWorkflow, { args: [input] }). This starts the child and awaits its completion.
TypeScript Continue-As-New implementation
In TypeScript, call `await continueAsNew<typeof dataProcessorWorkflow>(cursor, totalProcessed)` to trigger a Continue-As-New transition. The `continueAsNew()` function throws a special error that the runtime intercepts. Use `workflowInfo().continueAsNewSuggested` to check if history is approaching the limit.
TypeScript: Workflow configuration with retry options for Delayed Retry pattern
Example showing how to configure a Workflow with retry options when using the Delayed Retry pattern. The nextRetryDelay set in the Activity overrides the interval only for the retry following that specific failure:
```typescript
// workflows.ts
import * as wf from '@temporalio/workflow';
import type * as activities from './activities';
const { callApi } = wf.proxyActivities<typeof activities>({
startToCloseTimeout: '10s',
retry: {
initialInterval: '1s',
backoffCoefficient: 2,
maximumAttempts: 10,
},
});
export async function apiWorkflow(endpoint: string): Promise<string> {
return await callApi(endpoint);
}
```
TypeScript: ApplicationFailure with nextRetryDelay from HTTP 429 response
Example showing how to extract the Retry-After header from an HTTP 429 response and pass it to ApplicationFailure.create() to override the retry delay:
```typescript
// activities.ts
import { ApplicationFailure } from '@temporalio/activity';
export async function callApi(endpoint: string): Promise<string> {
const response = await fetch(endpoint);
if (response.status === 429) {
const retryAfterHeader = response.headers.get('Retry-After');
const retryAfterSeconds = retryAfterHeader != null ? parseInt(retryAfterHeader, 10) : undefined;
throw ApplicationFailure.create({
message: retryAfterSeconds != null
? `Rate limited — retrying after ${retryAfterSeconds}s`
: 'Rate limited — retrying per RetryPolicy',
type: 'RateLimitError',
// Only override the interval when the header is present; fall back to RetryPolicy otherwise
nextRetryDelay: retryAfterSeconds != null ? `${retryAfterSeconds}s` : undefined,
});
}
return response.text();
}
```
TypeScript: Attempt-proportional delay using nextRetryDelay
Example showing how to set nextRetryDelay dynamically based on the attempt number to implement a custom backoff:
```typescript
// activities.ts
import { ApplicationFailure, activityInfo } from '@temporalio/activity';
export async function process(input: string): Promise<string> {
const { attempt } = activityInfo();
try {
return await downstreamService.call(input);
} catch (e) {
// Custom delay: 3 seconds × attempt number (3s, 6s, 9s, …)
throw ApplicationFailure.create({
message: `Service unavailable on attempt ${attempt}`,
type: 'ServiceUnavailable',
cause: e as Error,
nextRetryDelay: `${3 * attempt}s`,
});
}
}
```
Delayed Start TypeScript API
In TypeScript, use `client.workflow.start()` with an options object containing a `startDelay` field as a string:
```typescript
const handle = await client.workflow.start(delayedStartWorkflow, {
workflowId: WORKFLOW_ID,
taskQueue: TASK_QUEUE,
startDelay: '30 seconds',
});
```
Early Return TypeScript implementation example
Example showing Early Return pattern in TypeScript using Update-with-Start:
```typescript
// workflow.ts
import { defineUpdate, setHandler, condition } from '@temporalio/workflow';
import * as activities from './activities';
const { initTransaction, completeTransaction, cancelTransaction } =
proxyLocalActivities<typeof activities>({
scheduleToCloseTimeout: '5s',
});
export const returnInitResultUpdate = defineUpdate<Transaction>('returnInitResult');
export async function transactionWorkflow(txRequest: TransactionRequest): Promise<Transaction> {
let tx: Transaction | undefined;
let initDone = false;
let initError: Error | undefined;
// Register update handler that waits for initialization
setHandler(returnInitResultUpdate, async () => {
await condition(() => initDone);
if (initError) {
throw initError;
}
return tx!;
});
// Phase 1: Fast synchronous initialization (local activity)
try {
tx = await initTransaction(txRequest);
} catch (err) {
initError = err as Error;
} finally {
initDone = true; // Signal update handler
}
// Phase 2: Slow asynchronous completion
if (initError) {
await cancelTransaction(tx!);
throw initError;
}
await completeTransaction(tx);
return tx;
}
// client.ts
const startWorkflowOperation = new WithStartWorkflowOperation(
transactionWorkflow,
{
workflowId: 'transaction-123',
args: [txRequest],
taskQueue: 'transactions',
workflowIdConflictPolicy: 'FAIL',
},
);
const tx = await client.workflow.executeUpdateWithStart(
returnInitResultUpdate,
{ startWorkflowOperation },
);
const wfHandle = await startWorkflowOperation.workflowHandle();
// Use transaction ID immediately while workflow continues
console.log(`Transaction initialized: ${tx.id}`);
// Optionally wait for the workflow to complete
const finalResult = await wfHandle.result();
```
Entity Workflow TypeScript implementation
import { condition, allHandlersFinished, defineUpdate, defineSignal, defineQuery, setHandler, continueAsNew, workflowInfo, proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { validateProfile } = proxyActivities<typeof activities>({
startToCloseTimeout: '30s',
});
interface UserState {
status: string;
profile?: ProfileData;
pendingEmail?: string;
createdAt: number;
updatedAt: number;
}
interface UserAccountInput {
userId: string;
state?: UserState;
}
export const updateProfileUpdate = defineUpdate<void, [ProfileData]>('updateProfile');
export const suspendSignal = defineSignal('suspend');
export const deleteSignal = defineSignal('delete');
export const getStateQuery = defineQuery<UserState>('getState');
export async function userAccountWorkflow(input: UserAccountInput): Promise<void> {
const state: UserState = input.state ?? {
status: 'ACTIVE',
createdAt: Date.now(),
updatedAt: Date.now(),
};
let deleted = false;
let operationCount = 0;
setHandler(updateProfileUpdate, async (data: ProfileData) => {
if (deleted) {
throw new Error('User account is deleted');
}
await validateProfile(data);
state.profile = data;
state.updatedAt = Date.now();
operationCount++;
});
setHandler(suspendSignal, () => {
if (!deleted && state.status !== 'SUSPENDED') {
state.status = 'SUSPENDED';
state.updatedAt = Date.now();
operationCount++;
}
});
setHandler(deleteSignal, () => {
deleted = true;
});
setHandler(getStateQuery, () => state);
await condition(() => deleted || workflowInfo().continueAsNewSuggested);
if (!deleted && workflowInfo().continueAsNewSuggested) {
await condition(allHandlersFinished);
await continueAsNew<typeof userAccountWorkflow>({ userId: input.userId, state });
}
state.status = 'DELETED';
}
Fan-Out TypeScript implementation
```typescript
// workflows.ts
import {
executeChild,
proxyActivities,
workflowInfo,
} from "@temporalio/workflow";
import type * as activities from "./activities";
import { TASK_QUEUE, CHUNK_SIZE } from "./shared";
const { processRecord } = proxyActivities<typeof activities>({
startToCloseTimeout: "10 seconds",
});
export async function fanOutWorkflow(
totalRecords: number,
chunkSize: number = CHUNK_SIZE
): Promise<number> {
const children: Promise<number>[] = [];
for (let offset = 0; offset < totalRecords; offset += chunkSize) {
const length = Math.min(chunkSize, totalRecords - offset);
children.push(
executeChild(recordBatchWorkflow, {
args: [offset, length],
taskQueue: TASK_QUEUE,
workflowId: `${workflowInfo().workflowId}/batch-${offset}`,
})
);
}
const results = await Promise.all(children);
return results.reduce((sum, n) => sum + n, 0);
}
export async function recordBatchWorkflow(
offset: number,
length: number
): Promise<number> {
let processed = 0;
for (let i = offset; i < offset + length; i++) {
await processRecord(i);
processed++;
}
return processed;
}
```
Fast/Slow Retries TypeScript example
```typescript
import * as wf from '@temporalio/workflow';
import type * as activities from './activities';
const fastDownstream = wf.proxyActivities<typeof activities>({
startToCloseTimeout: '30s',
retry: {
initialInterval: '1s',
backoffCoefficient: 1.5,
maximumInterval: '30s',
maximumAttempts: 10,
},
});
const slowDownstream = wf.proxyActivities<typeof activities>({
startToCloseTimeout: '30s',
retry: {
initialInterval: '5m',
backoffCoefficient: 1,
// maximumAttempts defaults to unlimited
},
});
export async function fastSlowRetryWorkflow(request: string): Promise<string> {
// Phase 1: fast retries
try {
return await fastDownstream.callDownstream(request);
} catch {
wf.log.warn('Fast retries exhausted — switching to slow retry phase', { request });
// Phase 2: slow retries
return await slowDownstream.callDownstream(request);
}
}
```
This example shows a Workflow implementing fast retries with 1-second initial interval and 10 max attempts, transitioning to slow retries with 5-minute interval and unlimited attempts when fast phase is exhausted.
TypeScript Fixed Count Retries example
import * as wf from '@temporalio/workflow';
import type * as activities from './activities';
const { chargePaymentApi } = wf.proxyActivities<typeof activities>({
startToCloseTimeout: '10s',
retry: { maximumAttempts: 3 },
});
export async function paymentWorkflow(orderId: string): Promise<string> {
try {
return await chargePaymentApi(orderId);
} catch (err) {
if (err instanceof wf.ActivityFailure && err.retryState === wf.RetryState.MAXIMUM_ATTEMPTS_REACHED) {
wf.log.error('Payment failed: all 3 attempts exhausted', { orderId });
}
throw err;
}
}
This example shows how to set maximumAttempts=3 on the retry policy for a TypeScript workflow and catch ActivityFailure when retries are exhausted.
Short SLA without per-attempt timeout TypeScript
For a 30-second authorization window, omit StartToCloseTimeout and let ScheduleToCloseTimeout act as the only bound:
```typescript
const { authorizeTransaction } = wf.proxyActivities<typeof activities>({
scheduleToCloseTimeout: '30s',
retry: {
initialInterval: '3s',
backoffCoefficient: 1.5,
},
});
```
Fixed Wall-Time Retries TypeScript implementation
Example of enforcing a 2-minute SLA with per-attempt 30-second timeout:
```typescript
import * as wf from '@temporalio/workflow';
import type * as activities from './activities';
const { authorizeTransaction } = wf.proxyActivities<typeof activities>({
scheduleToCloseTimeout: '2m', // total budget
startToCloseTimeout: '30s', // per attempt
retry: {
initialInterval: '5s',
backoffCoefficient: 1.5,
maximumInterval: '30s',
},
});
export async function paymentAuthWorkflow(transactionId: string): Promise<string> {
try {
return await authorizeTransaction(transactionId);
} catch (err) {
if (err instanceof wf.ActivityFailure) {
const cause = err.cause;
if (cause instanceof wf.TimeoutFailure && cause.type === wf.TimeoutType.SCHEDULE_TO_CLOSE) {
wf.log.error('Authorization failed — 2-minute SLA breached', { transactionId });
}
}
throw err;
}
}
```
Local Activity TypeScript example
import { proxyLocalActivities } from "@temporalio/workflow";
import type * as activities from "./activities";
const { validateTransaction, reserveFunds, settleTransaction } =
proxyLocalActivities<typeof activities>({ scheduleToCloseTimeout: "10s" });
export async function transactionWorkflow(
req: TransactionRequest,
): Promise<Transaction> {
// All three activities run in-process — no server round-trips.
let tx = await validateTransaction(req);
tx = await reserveFunds(tx);
return settleTransaction(tx);
}
Local Activity TypeScript API
In TypeScript, use `proxyLocalActivities()` to create a proxy for in-process Activity execution. Pass a `scheduleToCloseTimeout` option (format: "10s") to the proxy.
TypeScript Activity Heartbeat - basic progress tracking
Example in TypeScript showing file processing with heartbeating every 100 lines. Uses activityInfo().heartbeatDetails to retrieve the last checkpoint (defaulting to 0 if none), and heartbeat(currentLine) to store progress.
TypeScript Activity Heartbeat - cancellation handling
In TypeScript, cancellation is delivered as a CancelledFailure thrown from sleep() or Context.current().cancelled. The Activity should catch CancelledFailure, perform cleanup via cleanupResources(), and re-throw the error.
TypeScript retry policy nonRetryableErrorTypes field
In TypeScript, pass a retry object to wf.proxyActivities() with nonRetryableErrorTypes set to an array of error type name strings. Example: retry: {nonRetryableErrorTypes: ['OrderNotFoundError', 'ValidationError']}
TypeScript ApplicationFailure non-retryable method
In TypeScript, use @temporalio/activity.ApplicationFailure.nonRetryable() to mark an error as non-retryable at the throw site. Pass the error message and type name as parameters. Example: throw ApplicationFailure.nonRetryable(`Order ${orderId} not found`, 'OrderNotFoundError')
Catching and handling ActivityError in TypeScript workflows
In TypeScript, catch wf.ActivityFailure in a try-catch block. Check if err.cause instanceof wf.ApplicationFailure. Inspect the type and message properties to route to the appropriate compensation or escalation path.
TypeScript Eager Workflow Start limitation
Eager Workflow Start is not available in the TypeScript SDK, but the latency gap is small at ~30–50 ms per Workflow start. Local Activities and Early Return + Local Activities are fully supported in TypeScript and achieve ~275 ms total latency and ~160 ms first-response latency respectively.
Pattern selection for TypeScript implementations
For TypeScript SDK implementations, use Local Activities and Early Return + Local Activities. Eager Workflow Start is not available in the TypeScript SDK.
Periodic sequence polling example in TypeScript
```typescript
// workflows.ts
import { proxyActivities, sleep, continueAsNew } from '@temporalio/workflow';
import type * as activities from './activities';
const { doPoll } = proxyActivities<typeof activities>({
startToCloseTimeout: '10s',
});
export async function pollingChildWorkflow(
pollingIntervalSeconds: number
): Promise<string> {
const maxAttempts = 10;
for (let i = 0; i < maxAttempts; i++) {
const result = await doPoll();
if (result === 'COMPLETED') {
return result;
}
await sleep(`${pollingIntervalSeconds}s`);
}
// Continue-as-new to prevent unbounded history
await continueAsNew<typeof pollingChildWorkflow>(pollingIntervalSeconds);
return ''; // unreachable
}
export async function periodicPollingWorkflow(): Promise<string> {
return await executeChild(pollingChildWorkflow, {
args: [5],
workflowId: 'ChildWorkflowPoll',
});
}
```
This example shows a Child Workflow that polls up to 10 times with a configurable interval between attempts, then calls Continue-As-New to start a fresh execution. The parent Workflow executes the Child Workflow with a specific workflow ID.
Frequent polling example in TypeScript
```typescript
// activities.ts
import { heartbeat, sleep } from '@temporalio/activity';
export async function doPoll(): Promise<string> {
while (true) {
heartbeat();
const result = await externalService.checkStatus();
if (result === 'COMPLETED') {
return result;
}
await sleep('1s');
}
}
// workflows.ts
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { doPoll } = proxyActivities<typeof activities>({
startToCloseTimeout: '60s',
heartbeatTimeout: '2s',
});
export async function frequentPollingWorkflow(): Promise<string> {
return await doPoll();
}
```
This example shows a frequent polling Activity that loops indefinitely with heartbeats every iteration, and a Workflow that executes the Activity with a 60-second start-to-close timeout and 2-second heartbeat timeout.
Infrequent polling example in TypeScript
```typescript
// activities.ts
import { ApplicationFailure } from '@temporalio/activity';
export async function doPoll(): Promise<string> {
const result = await externalService.checkStatus();
if (result !== 'COMPLETED') {
throw ApplicationFailure.retryable('Service not ready, will retry');
}
return result;
}
// workflows.ts
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { doPoll } = proxyActivities<typeof activities>({
startToCloseTimeout: '2s',
retry: {
backoffCoefficient: 1,
initialInterval: '60s',
},
});
export async function infrequentPollingWorkflow(): Promise<string> {
return await doPoll();
}
```
This example shows an infrequent polling Activity that performs a single poll and throws a retryable error if the service is not ready, and a Workflow that executes the Activity with a fixed 60-second retry interval (backoff_coefficient=1).
TypeScript task assignment workflow with Updates
```typescript
// workflow.ts
import * as wf from '@temporalio/workflow';
interface AssignmentResult {
assignmentId: string;
taskName: string;
totalTasks: number;
}
export const assignTaskUpdate = wf.defineUpdate<AssignmentResult, [string]>('assignTask');
export const getTasksQuery = wf.defineQuery<string[]>('getTasks');
const MAX_TASKS = 10;
export async function taskWorkflow(): Promise<void> {
const tasks: string[] = [];
wf.setHandler(
assignTaskUpdate,
(taskName: string): AssignmentResult => {
const assignmentId = wf.uuid4();
tasks.push(taskName);
return { assignmentId, taskName, totalTasks: tasks.length };
},
{
validator: (taskName: string): void => {
if (tasks.length >= MAX_TASKS) {
throw new Error('Task limit reached');
}
},
}
);
wf.setHandler(getTasksQuery, (): string[] => tasks);
await wf.condition(() => false);
}
```
Resumable Activity TypeScript implementation
TypeScript Resumable Activity Workflow implementation:
```typescript
import * as wf from '@temporalio/workflow';
import type * as activities from './activities';
export interface TransferInput {
fromAccount: string;
toAccount: string;
amount: number;
}
export const retryWithCorrectionSignal = wf.defineSignal<[string]>('retryWithCorrection');
export const approveSignal = wf.defineSignal<[boolean]>('approve');
export const getStatusQuery = wf.defineQuery<string>('getStatus');
const { executeTransfer } = wf.proxyActivities<typeof activities>({
startToCloseTimeout: '30s',
retry: { maximumAttempts: 3 },
});
export async function transferWorkflow(input: TransferInput): Promise<string> {
let status = 'PENDING';
let correctedAccount: string | undefined;
let approval: boolean | undefined;
wf.setHandler(retryWithCorrectionSignal, (account: string) => {
correctedAccount = account;
});
wf.setHandler(approveSignal, (decision: boolean) => {
approval = decision;
});
wf.setHandler(getStatusQuery, () => status);
let account = input.toAccount;
let correctionCount = 0;
while (true) {
status = 'TRANSFERRING';
try {
await executeTransfer({ ...input, toAccount: account });
break;
} catch (err) {
correctionCount++;
if (correctionCount > 5) {
status = 'FAILED';
wf.upsertSearchAttributes({ TransferStatus: [status] });
throw err;
}
status = 'AWAITING_CORRECTION';
wf.upsertSearchAttributes({ TransferStatus: [status] });
wf.log.warn('Transfer failed — waiting for account correction', { account });
await wf.condition(() => correctedAccount !== undefined);
account = correctedAccount!;
correctedAccount = undefined;
}
}
status = 'AWAITING_APPROVAL';
await wf.condition(() => approval !== undefined);
if (approval) {
status = 'COMPLETED';
return `Transfer of ${input.amount} to ${account} completed`;
}
status = 'REJECTED';
return 'Transfer rejected by client';
}
```
Resumable Activity: TypeScript activity with non-retryable error
TypeScript Activity implementation that distinguishes permanent from transient failures:
```typescript
import { ApplicationFailure } from '@temporalio/activity';
import type { TransferInput } from './workflows';
export async function executeTransfer(transfer: TransferInput): Promise<string> {
// Non-retryable: bad account number requires a human correction, not a retry.
const accountExists = await accountService.exists(transfer.toAccount);
if (!accountExists) {
throw ApplicationFailure.nonRetryable(
`Account ${transfer.toAccount} not found`,
'AccountNotFoundError',
);
}
// Other exceptions propagate as retryable so the RetryPolicy handles them.
return paymentService.transfer(transfer.fromAccount, transfer.toAccount, transfer.amount);
}
```
Signal with Start TypeScript implementation
In TypeScript, use client.workflow.signalWithStart() with parameters: the workflow function, and an options object containing workflowId, taskQueue, signal (the signal name as a string), and signalArgs (array of signal arguments). Example: client.workflow.signalWithStart(shoppingCartWorkflow, { workflowId: `cart-${cartId}`, taskQueue: 'carts', signal: 'addItem', signalArgs: [itemId, productId, quantity] }).
Saga pattern pitfall: TypeScript ContinueAsNew exception handling
In TypeScript, continueAsNew works by throwing a special exception. A catch block that does not re-throw it, or a finally block that returns a value, silently prevents Continue-As-New.
TypeScript Saga pattern implementation
type Compensation = () => Promise<void>;
export async function openAccount(req: OpenAccountRequest): Promise<string> {
const compensations: Compensation[] = [];
try {
await acts.createAccount(req);
compensations.unshift(() => acts.clearPostalAddresses(req));
await acts.addAddress(req);
compensations.unshift(() => acts.removeClient(req));
await acts.addClient(req);
compensations.unshift(() => acts.disconnectBankAccounts(req));
await acts.addBankAccount(req);
return `Account ${req.accountId} opened`;
} catch (err) {
for (const compensate of compensations) {
await compensate();
}
throw err;
}
}
This example shows how to implement the Saga pattern in TypeScript using an array with unshift() to maintain LIFO order and manual iteration on error.
Sliding Window TypeScript implementation example
Example TypeScript implementation of Sliding Window pattern:
```typescript
import {
ApplicationFailure,
ParentClosePolicy,
condition,
continueAsNew,
defineSignal,
getExternalWorkflowHandle,
proxyActivities,
setHandler,
startChild,
workflowInfo,
} from "@temporalio/workflow";
const { processRecord } = proxyActivities<typeof activities>({
startToCloseTimeout: "30 seconds",
});
export const completionSignal = defineSignal<[string]>(COMPLETION_SIGNAL);
// Child Workflow: processes one record and signals the parent on completion.
export async function recordProcessorWorkflow(recordId: string): Promise<void> {
await processRecord(recordId);
try {
const parent = getExternalWorkflowHandle(workflowInfo().parent!.workflowId);
await parent.signal(completionSignal, recordId);
} catch (err) {
if (!(err instanceof ApplicationFailure && err.type === "NOT_FOUND")) throw err;
}
}
// Parent Workflow: maintains a fixed window of concurrent Child Workflows.
export async function slidingWindowWorkflow(input: SlidingWindowInput): Promise<number> {
const { recordIds, windowSize = WINDOW_SIZE, startIndex = 0 } = input;
const parentId = workflowInfo().workflowId;
let totalProcessed = input.totalProcessed ?? 0;
let dispatched = 0;
let active = input.active ?? 0;
setHandler(completionSignal, () => {
active--;
totalProcessed++;
});
let nextIndex = startIndex;
while (nextIndex < recordIds.length) {
await condition(() => active < windowSize);
await startChild(recordProcessorWorkflow, {
args: [recordIds[nextIndex]],
workflowId: `${parentId}/record-${recordIds[nextIndex]}`,
taskQueue: TASK_QUEUE,
parentClosePolicy: ParentClosePolicy.ABANDON,
});
nextIndex++;
dispatched++;
active++;
if (dispatched >= windowSize) {
await continueAsNew<typeof slidingWindowWorkflow>({ recordIds, windowSize, startIndex: nextIndex, totalProcessed, active });
}
}
await condition(() => active === 0);
return totalProcessed;
}
```
Updatable Timer TypeScript implementation
```typescript
// updatable-timer.ts
import * as wf from '@temporalio/workflow';
export class UpdatableTimer implements PromiseLike<void> {
deadlineUpdated = false;
#deadline: number;
constructor(deadline: number) {
this.#deadline = deadline;
}
private async run(): Promise<void> {
while (true) {
this.deadlineUpdated = false;
if (
!(await wf.condition(
() => this.deadlineUpdated,
this.#deadline - Date.now(),
))
) {
break; // Timer expired
}
// Timer was updated, loop to recalculate
}
}
then<TResult1 = void, TResult2 = never>(
onfulfilled?: (value: void) => TResult1 | PromiseLike<TResult1>,
onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>,
): PromiseLike<TResult1 | TResult2> {
return this.run().then(onfulfilled, onrejected);
}
set deadline(value: number) {
this.#deadline = value;
this.deadlineUpdated = true;
}
get deadline(): number {
return this.#deadline;
}
}
```
TypeScript Worker-Specific Task Queues example workflow
```typescript
// workflows.ts
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { download } = proxyActivities<typeof activities>({
startToCloseTimeout: '20s',
});
export async function fileProcessingWorkflow(
source: string,
destination: string,
): Promise<void> {
const downloaded = await download(source);
const hostSpecificActivities = proxyActivities<typeof activities>({
taskQueue: downloaded.hostTaskQueue,
scheduleToStartTimeout: '10s',
startToCloseTimeout: '20s',
});
const processed = await hostSpecificActivities.process(downloaded.fileName);
await hostSpecificActivities.upload(processed, destination);
}
```
This example shows a workflow that downloads a file on any worker, then processes and uploads it on the same host-specific worker.
TypeScript Worker-Specific Task Queues example activity
```typescript
// activities.ts
export interface TaskQueueFileNamePair {
hostTaskQueue: string;
fileName: string;
}
let hostSpecificTaskQueue: string;
export function initActivities(taskQueue: string) {
hostSpecificTaskQueue = taskQueue;
}
function getHostSpecificTaskQueue(): string {
return hostSpecificTaskQueue;
}
export async function download(source: string): Promise<TaskQueueFileNamePair> {
const localFile = await downloadToLocalDisk(source);
return {
hostTaskQueue: getHostSpecificTaskQueue(),
fileName: localFile,
};
}
export async function process(fileName: string): Promise<string> {
return await processLocalFile(fileName);
}
export async function upload(fileName: string, destination: string): Promise<void> {
await uploadFromLocalDisk(fileName, destination);
}
```
The host-specific Task Queue name is captured via closure when defining the activity functions. The download function returns both the file path and the host-specific queue name.
TypeScript Worker-Specific Task Queues example worker setup
```typescript
// worker.ts
import { Worker, NativeConnection } from '@temporalio/worker';
import * as activities from './activities';
import { v4 as uuid } from 'uuid';
import os from 'os';
async function run() {
const defaultTaskQueue = 'FileProcessing';
const hostTaskQueue = `FileProcessing-${os.hostname()}-${uuid()}`;
activities.initActivities(hostTaskQueue);
const defaultWorker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: defaultTaskQueue,
});
const hostWorker = await Worker.create({
activities,
taskQueue: hostTaskQueue,
});
await Promise.all([defaultWorker.run(), hostWorker.run()]);
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
```
Each Worker registers with both the default Task Queue and its own host-specific Task Queue. The host-specific queue name includes hostname and UUID for uniqueness.
Task Queue constant definition - TypeScript example
In TypeScript, define a Task Queue name constant: const TASK_QUEUE_NAME = 'my-taskqueue-name';. Export from a shared module and import in both the workflow client code (in client.workflow.start() taskQueue option) and worker configuration (in Worker.create() taskQueue option).
Workflow Definition syntax in TypeScript
A Workflow Definition in TypeScript is an async function that takes typed arguments and returns a Promise. Example: export async function WorkflowExample(args: BasicWorkflowArgs): Promise<{ result: string }> { ... }
TypeScript SDK does not support Eager Workflow Start
The TypeScript SDK does not currently support Eager Workflow Start. For latency-sensitive TypeScript workflows, use Local Activities or Early Return + Local Activities instead.
Early Return + Local Activities TypeScript example
```typescript
// workflows.ts
import { proxyLocalActivities, proxyActivities, defineUpdate, setHandler, condition } from "@temporalio/workflow";
import type * as activities from "./activities";
import type { TransactionRequest, Transaction } from "./shared";
// Phase 1: local activities — no server round-trips on the hot path.
const { validateTransaction, initTransaction } =
proxyLocalActivities<typeof activities>({ scheduleToCloseTimeout: "5s" });
// Phase 2: regular activities — background settlement.
const { completeTransaction, cancelTransaction } =
proxyActivities<typeof activities>({ startToCloseTimeout: "30s" });
export const getResultUpdate = defineUpdate<Transaction, [TransactionRequest]>("getResult");
export async function transactionWorkflow(req: TransactionRequest): Promise<void> {
let tx: Transaction | undefined;
let phase1Done = false;
let phase1Error: unknown;
setHandler(getResultUpdate, async () => {
// The Update handler waits for Phase 1 before returning.
await condition(() => phase1Done);
if (phase1Error) throw phase1Error;
return tx!;
});
try {
// Phase 1: Local Activities run in-process.
tx = await validateTransaction(req);
tx = await initTransaction(tx);
} catch (err) {
phase1Error = err;
} finally {
phase1Done = true;
}
if (phase1Error) {
if (tx !== undefined) {
await cancelTransaction(tx);
}
return;
}
// Phase 2: Regular Activity runs in the background.
await completeTransaction(tx!);
}
```
Event Accumulator TypeScript implementation
TypeScript implementation uses setHandler to register signal handlers and condition() with a string duration for the sliding window. continueAsNew<typeof accumulatorWorkflow>() carries accumulated state to the next run.
Event Accumulator TypeScript example workflow
export async function accumulatorWorkflow(
bucketKey: string,
accumulated: OrderItem[] = [],
seenKeys: string[] = [],
): Promise<string> {
const seenSet = new Set(seenKeys);
const items: OrderItem[] = [...accumulated];
const unprocessed: OrderItem[] = [];
let flushRequested = false;
setHandler(addItemSignal, (item: OrderItem) => {
unprocessed.push(item);
});
setHandler(flushSignal, () => {
flushRequested = true;
});
do {
// Sliding window: wait for a signal or let the inactivity timer fire
const timedOut = !(await condition(
() => unprocessed.length > 0 || flushRequested,
"10 seconds",
));
// Drain and deduplicate incoming signals
while (unprocessed.length > 0) {
const item = unprocessed.shift()!;
if (item.orderId === bucketKey && !seenSet.has(item.itemId)) {
seenSet.add(item.itemId);
items.push(item);
}
}
if (timedOut || flushRequested) {
const result = await processItems(bucketKey, items);
if (unprocessed.length === 0) return result;
// More signals arrived after timeout/flush — loop to process them
}
} while (unprocessed.length > 0 || !workflowInfo().continueAsNewSuggested);
// History growing large — continue as new, carrying accumulated state forward
await continueAsNew<typeof accumulatorWorkflow>(bucketKey, items, [...seenSet]);
return ""; // unreachable
}
MapReduce Tree TypeScript implementation
TypeScript implementation using leafWorkflow and nodeWorkflow functions. The leafWorkflow calls processLeaf Activity and signals result back to parent using getExternalWorkflowHandle(parentWorkflowId).signal(). The nodeWorkflow checks record count against LEAF_THRESHOLD; if below threshold starts one leaf per record, otherwise splits into N chunks and starts N child node workflows recursively. Uses condition() to await all expected signals, then signals aggregated results up to parent if not root.
Pick First pattern implementation in TypeScript
In TypeScript, the Pick First pattern uses `Promise.race()` within a `CancellationScope.cancellable()` block. Activities are defined with `proxyActivities()`. Multiple Activities are started concurrently, and `Promise.race()` waits for the first to complete. After capturing the first result, `CancellationScope.current().cancel()` cancels all Activities started in that scope. To wait for cancellation cleanup, set `cancellationType: ActivityCancellationType.WAIT_CANCELLATION_COMPLETED` in Activity options and await all promises while catching cancellation errors with `isCancellation()`.
Retry Alerting via Metrics implementation in TypeScript
```typescript
import { Context } from '@temporalio/activity';
const ALERT_THRESHOLD = 5;
export async function callDownstreamService(endpoint: string): Promise<string> {
const ctx = Context.current();
if (ctx.info.attempt > ALERT_THRESHOLD) {
ctx.metricMeter
.createCounter('high_activity_error_count')
.add(1);
}
const response = await downstream.call(endpoint);
return response.data;
}
```
This example shows accessing the current attempt number via Context and using metricMeter to emit a counter.
Retry Alerting with dimension tags in TypeScript
```typescript
if (ctx.info.attempt > ALERT_THRESHOLD) {
ctx.metricMeter
.createCounter('high_activity_error_count')
.add(1, { activity_type: ctx.info.activityType, endpoint });
}
```
Add tags to the metric to identify which Activity type and endpoint are producing high attempt counts.
Parallel Execution in TypeScript
In TypeScript, Activity proxy functions return native Promise objects. Use Promise.all() to wait for all of them to complete.
TypeScript parallel Activities example
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { process } = proxyActivities<typeof activities>({
startToCloseTimeout: '30s',
});
export async function processInParallel(items: string[]): Promise<string[]> {
const promises = items.map((item) => process(item));
return await Promise.all(promises);
}
This example starts one Activity per item in a list and waits for all of them to complete.
TypeScript batch processing example
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { process } = proxyActivities<typeof activities>({
startToCloseTimeout: '30s',
});
export async function processBatch(
items: string[],
maxParallel: number
): Promise<string[]> {
const results: string[] = [];
for (let i = 0; i < items.length; i += maxParallel) {
const batch = items.slice(i, i + maxParallel);
const batchResults = await Promise.all(batch.map((item) => process(item)));
results.push(...batchResults);
}
return results;
}
This example implements controlled parallelism by processing items in batches.
Error handling in parallel execution - TypeScript
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { process } = proxyActivities<typeof activities>({
startToCloseTimeout: '30s',
});
interface Result {
item: string;
output?: string;
error?: string;
}
export async function processWithErrorHandling(items: string[]): Promise<Result[]> {
const settled = await Promise.allSettled(items.map((item) => process(item)));
return settled.map((outcome, i) => {
if (outcome.status === 'fulfilled') {
return { item: items[i], output: outcome.value };
}
return { item: items[i], error: String(outcome.reason) };
});
}
This example uses Promise.allSettled() so that individual failures do not prevent other Activities from completing.