← Files TemporalARCHIVED FILE

skills/temporal-developer/references/typescript/patterns.md

11.4 KB · Oct 2, 2026 · 00:08 UTC

↓ Download file

# TypeScript SDK Patterns

## Signals

```typescript
import { defineSignal, setHandler, condition } from '@temporalio/workflow';

const approveSignal = defineSignal<[boolean]>('approve');
const addItemSignal = defineSignal<[string]>('addItem');

export async function orderWorkflow(): Promise<string> {
  let approved = false;
  const items: string[] = [];

  setHandler(approveSignal, (value) => {
    approved = value;
  });

  setHandler(addItemSignal, (item) => {
    items.push(item);
  });

  await condition(() => approved);
  return `Processed ${items.length} items`;
}
```

## Dynamic Signal Handlers

For handling signals with names not known at compile time. Use cases for this pattern are rare — most workflows should use statically defined signal handlers.

```typescript
import { setDefaultSignalHandler, condition } from '@temporalio/workflow';

export async function dynamicSignalWorkflow(): Promise<Record<string, unknown[]>> {
  const signals: Record<string, unknown[]> = {};

  setDefaultSignalHandler((signalName: string, ...args: unknown[]) => {
    if (!signals[signalName]) {
      signals[signalName] = [];
    }
    signals[signalName].push(args);
  });

  await condition(() => signals['done'] !== undefined);
  return signals;
}
```

## Queries

**Important:** Queries must NOT modify workflow state or have side effects.

```typescript
import { defineQuery, setHandler } from '@temporalio/workflow';

const statusQuery = defineQuery<string>('status');
const progressQuery = defineQuery<number>('progress');

export async function progressWorkflow(): Promise<void> {
  let status = 'running';
  let progress = 0;

  setHandler(statusQuery, () => status);
  setHandler(progressQuery, () => progress);

  for (let i = 0; i < 100; i++) {
    progress = i;
    await doWork();
  }
  status = 'completed';
}
```

## Dynamic Query Handlers

For handling queries with names not known at compile time. Use cases for this pattern are rare — most workflows should use statically defined query handlers.

```typescript
import { setDefaultQueryHandler } from '@temporalio/workflow';

export async function dynamicQueryWorkflow(): Promise<void> {
  const state: Record<string, unknown> = {
    status: 'running',
    progress: 0,
  };

  setDefaultQueryHandler((queryName: string) => {
    return state[queryName];
  });

  // ... workflow logic
}
```

## Updates

```typescript
import { defineUpdate, setHandler, condition } from '@temporalio/workflow';

// Define the update - specify return type and argument types
export const addItemUpdate = defineUpdate<number, [string]>('addItem');
export const addItemValidatedUpdate = defineUpdate<number, [string]>('addItemValidated');

export async function orderWorkflow(): Promise<string> {
  const items: string[] = [];
  let completed = false;

  // Simple update handler - returns new item count
  setHandler(addItemUpdate, (item: string) => {
    items.push(item);
    return items.length;
  });

  // Update handler with validator - rejects invalid input before execution
  setHandler(
    addItemValidatedUpdate,
    (item: string) => {
      items.push(item);
      return items.length;
    },
    {
      validator: (item: string) => {
        if (!item) throw new Error('Item cannot be empty');
        if (items.length >= 100) throw new Error('Order is full');
      },
    }
  );

  await condition(() => completed);
  return `Order with ${items.length} items completed`;
}
```

**Important:** Validators must NOT mutate workflow state or do anything blocking (no activities, sleeps, or other commands). They are read-only, similar to query handlers. Throw an error to reject the update; return normally to accept.

## Child Workflows

```typescript
import { executeChild } from '@temporalio/workflow';

export async function parentWorkflow(orders: Order[]): Promise<string[]> {
  const results: string[] = [];

  for (const order of orders) {
    const result = await executeChild(processOrderWorkflow, {
      args: [order],
      workflowId: `order-${order.id}`,
    });
    results.push(result);
  }

  return results;
}
```

### Child Workflow Options

```typescript
import { executeChild, ParentClosePolicy, ChildWorkflowCancellationType } from '@temporalio/workflow';

const result = await executeChild(childWorkflow, {
  args: [input],
  workflowId: `child-${workflowInfo().workflowId}`,

  // ParentClosePolicy - what happens to child when parent closes
  // TERMINATE (default), ABANDON, REQUEST_CANCEL
  parentClosePolicy: ParentClosePolicy.TERMINATE,

  // ChildWorkflowCancellationType - how cancellation is handled
  // WAIT_CANCELLATION_COMPLETED (default), WAIT_CANCELLATION_REQUESTED, TRY_CANCEL, ABANDON
  cancellationType: ChildWorkflowCancellationType.WAIT_CANCELLATION_COMPLETED,
});
```

## Handles to External Workflows

```typescript
import { getExternalWorkflowHandle } from '@temporalio/workflow';
import { mySignal } from './other-workflows';

export async function coordinatorWorkflow(targetWorkflowId: string): Promise<void> {
  const handle = getExternalWorkflowHandle(targetWorkflowId);

  // Signal the external workflow
  await handle.signal(mySignal, { data: 'payload' });

  // Or cancel it
  await handle.cancel();
}
```

## Parallel Execution

```typescript
export async function parallelWorkflow(items: string[]): Promise<string[]> {
  return await Promise.all(
    items.map((item) => processItem(item))
  );
}
```

## Continue-as-New

```typescript
import { continueAsNew, workflowInfo } from '@temporalio/workflow';

export async function longRunningWorkflow(state: State): Promise<string> {
  while (true) {
    state = await processNextBatch(state);

    if (state.isComplete) {
      return 'done';
    }

    const info = workflowInfo();
    if (info.continueAsNewSuggested || info.historyLength > 10000) {
      await continueAsNew<typeof longRunningWorkflow>(state);
    }
  }
}
```

## Saga Pattern

**Important:** Compensation activities should be idempotent.

```typescript
import { CancellationScope, log } from '@temporalio/workflow';

export async function sagaWorkflow(order: Order): Promise<string> {
  const compensations: Array<() => Promise<void>> = [];

  try {
    // IMPORTANT: Save compensation BEFORE calling the activity
    // If activity fails after completing but before returning,
    // compensation must still be registered
    compensations.push(() => releaseInventory(order));
    await reserveInventory(order);

    compensations.push(() => refundPayment(order));
    await chargePayment(order);

    await shipOrder(order);
    return 'Order completed';
  } catch (err) {
    // nonCancellable ensures compensations run even if the workflow is cancelled
    await CancellationScope.nonCancellable(async () => {
      for (const compensate of compensations.reverse()) {
        try {
          await compensate();
        } catch (compErr) {
          log.warn('Compensation failed', { error: compErr });
        }
      }
    });
    throw err;
  }
}
```

## Cancellation Scopes

Cancellation scopes control how cancellation propagates to activities and child workflows. Use them for cleanup logic, timeouts, and manual cancellation.

```typescript
import { CancellationScope, sleep } from '@temporalio/workflow';

export async function scopedWorkflow(): Promise<void> {
  // Non-cancellable scope - runs even if workflow cancelled
  await CancellationScope.nonCancellable(async () => {
    await cleanupActivity();
  });

  // Timeout scope
  await CancellationScope.withTimeout('5 minutes', async () => {
    await longRunningActivity();
  });

  // Manual cancellation
  const scope = new CancellationScope();
  const promise = scope.run(() => someActivity());
  scope.cancel();
}
```

## Triggers (Promise-like Signals)

**WHY**: Triggers provide a one-shot promise that resolves when a signal is received. Cleaner than condition() for single-value signals.

**WHEN to use**:

- Waiting for a single response (approval, completion notification)
- Converting signal-based events into awaitable promises

```typescript
import { Trigger } from '@temporalio/workflow';

export async function triggerWorkflow(): Promise<string> {
  const approvalTrigger = new Trigger<boolean>();

  setHandler(approveSignal, (approved) => {
    approvalTrigger.resolve(approved);
  });

  const approved = await approvalTrigger;
  return approved ? 'Approved' : 'Rejected';
}
```

## Wait Condition with Timeout

```typescript
import { condition, CancelledFailure } from '@temporalio/workflow';

export async function approvalWorkflow(): Promise<string> {
  let approved = false;

  setHandler(approveSignal, () => {
    approved = true;
  });

  // Wait for approval with 24-hour timeout
  const gotApproval = await condition(() => approved, '24 hours');

  if (gotApproval) {
    return 'approved';
  } else {
    return 'auto-rejected due to timeout';
  }
}
```

## Waiting for All Handlers to Finish

Signal and update handlers should generally be non-async (avoid running activities from them). Otherwise, the workflow may complete before handlers finish their execution. However, making handlers non-async sometimes requires workarounds that add complexity.

When async handlers are necessary, use `condition(allHandlersFinished)` at the end of your workflow (or before continue-as-new) to prevent completion until all pending handlers complete.

```typescript
import { condition, allHandlersFinished } from '@temporalio/workflow';

export async function handlerAwareWorkflow(): Promise<string> {
  // ... main workflow logic ...

  // Before exiting, wait for all handlers to finish
  await condition(allHandlersFinished);
  return 'done';
}
```

## Activity Heartbeat Details

### WHY:

- **Support activity cancellation** - Cancellations are delivered via heartbeat; activities that don't heartbeat won't know they've been cancelled
- **Resume progress after worker failure** - Heartbeat details persist across retries

### WHEN:

- **Cancellable activities** - Any activity that should respond to cancellation
- **Long-running activities** - Track progress for resumability
- **Checkpointing** - Save progress periodically

```typescript
import { heartbeat, activityInfo, CancelledFailure } from '@temporalio/activity';

export async function processLargeFile(filePath: string): Promise<string> {
  const info = activityInfo();
  // Get heartbeat details from previous attempt (if any)
  const startLine: number = info.heartbeatDetails ?? 0;

  const lines = await readFileLines(filePath);

  try {
    for (let i = startLine; i < lines.length; i++) {
      await processLine(lines[i]);
      // Heartbeat with progress
      // If activity is cancelled, heartbeat() throws CancelledFailure
      heartbeat(i + 1);
    }
    return 'completed';
  } catch (e) {
    if (e instanceof CancelledFailure) {
      // Perform cleanup on cancellation
      await cleanup();
    }
    throw e;
  }
}
```

## Timers

```typescript
import { sleep } from '@temporalio/workflow';

export async function timerWorkflow(): Promise<string> {
  await sleep('1 hour');
  return 'Timer fired';
}
```

## Local Activities

**Purpose**: Reduce latency for short, lightweight operations by skipping the task queue. ONLY use these when necessary for performance. Do NOT use these by default, as they are not durable and distributed.

```typescript
import { proxyLocalActivities } from '@temporalio/workflow';
import type * as activities from './activities';

const { quickLookup } = proxyLocalActivities<typeof activities>({
  startToCloseTimeout: '5 seconds',
});

export async function localActivityWorkflow(): Promise<string> {
  const result = await quickLookup('key');
  return result;
}
```

SHA-256: 8bc84381f9d6432301f3011b60a64f0170bc2584fca170cff9e55469cb958613