Use when implementing cancellation logic in Temporal TypeScript workflows, including cancellation scopes, timeouts, cleanup after cancellation, shielding operations from cancellation, or resetting workflow executions. Covers CancellationScope, nonCancellable, withTimeout, CancelledFailure handling, and activity cancellation via heartbeats.
Instructions for implementing cancellation logic in Temporal TypeScript Workflows using CancellationScopes, nonCancellable shielding, timeouts, and graceful cleanup patterns.
Before writing any Temporal cancellation code, confirm these with the user:
Where is Temporal running?
localhost:7233)temporal.example.com:7233)What is the full Temporal gRPC address? (if not local)
Is there ingress set up for the Temporal HTTP API?
https://flow.remodl.ai:443)--address will workWhat namespace are they using? (default: "default")
Use these answers to configure all connection and CLI snippets.
Temporal TypeScript workflows use a tree of CancellationScopes to manage cancellation. Every workflow runs in a root scope. You create child scopes to control which operations get cancelled and which are shielded. Cancellation propagates from outer scopes to inner scopes. You handle cancellation by catching CancelledFailure errors.
This skill covers:
| API | Import | Purpose |
|---|---|---|
CancellationScope.cancellable(fn) | @temporalio/workflow | Run fn in a scope that auto-cancels children when cancelled |
CancellationScope.nonCancellable(fn) | @temporalio/workflow | Run fn in a scope that shields children from cancellation |
CancellationScope.withTimeout(ms, fn) | @temporalio/workflow | Cancel scope automatically after ms milliseconds |
CancellationScope.current() | @temporalio/workflow | Get the current scope |
scope.cancel() | @temporalio/workflow | Manually cancel a scope and its children |
scope.cancelRequested | @temporalio/workflow | Promise that resolves when cancellation is requested |
scope.run(fn) | @temporalio/workflow | Run async function inside the scope |
isCancellation(err) | @temporalio/workflow | Check if an error is a cancellation (works for Activity/ChildWorkflow wrappers too) |
CancelledFailure | @temporalio/common | The error class thrown by cancelled timers and triggers |
Context.current().heartbeat() | @temporalio/activity | Send heartbeat from activity (required for activity cancellation) |
When a CancellationScope is cancelled, these operations inside it are cancelled:
proxyActivities)sleep())new Trigger())Cancellation propagates to nested cancellable child scopes.
| Operation | Error Thrown | How to Check |
|---|---|---|
sleep() / Timer | CancelledFailure | err instanceof CancelledFailure |
| Trigger | CancelledFailure | err instanceof CancelledFailure |
| Activity | ActivityFailure with cause: CancelledFailure | isCancellation(err) |
| Child Workflow | ChildWorkflowFailure with cause: CancelledFailure | isCancellation(err) |
Always use isCancellation(err) for a unified check -- it handles all wrapper types.
import { CancellationScope, CancelledFailure, sleep } from '@temporalio/workflow';
export async function cancelTimerExample(): Promise<void> {
try {
await CancellationScope.cancellable(async () => {
const promise = sleep(60_000);
CancellationScope.current().cancel(); // cancel immediately
await promise; // must await for CancelledFailure to throw
});
} catch (e) {
if (e instanceof CancelledFailure) {
// Timer was cancelled -- handle as needed
} else {
throw e;
}
}
}
Alternative using the constructor:
const scope = new CancellationScope();
const promise = scope.run(() => sleep(60_000));
scope.cancel();
await promise; // throws CancelledFailure
When an external client calls handle.cancel(), the workflow's root scope is cancelled. Use nonCancellable to run cleanup:
import { CancellationScope, proxyActivities, isCancellation } from '@temporalio/workflow';
import type * as activities from './activities';
const { processOrder, refundOrder } = proxyActivities<typeof activities>({
startToCloseTimeout: '10m',
});
export async function orderWorkflow(orderId: string): Promise<void> {
try {
await processOrder(orderId);
} catch (err) {
if (isCancellation(err)) {
// MUST use nonCancellable -- root scope is already cancelled
await CancellationScope.nonCancellable(() => refundOrder(orderId));
}
throw err; // re-throw to mark workflow as cancelled
}
}
import { CancellationScope, proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { fetchData } = proxyActivities<typeof activities>({
startToCloseTimeout: '30s',
});
export async function fetchWithDeadline(urls: string[]): Promise<any[]> {
// All activities cancelled if total time exceeds 60s
return CancellationScope.withTimeout(
60_000,
() => Promise.all(urls.map((url) => fetchData(url)))
);
}
import { CancellationScope, proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { criticalWrite } = proxyActivities<typeof activities>({
startToCloseTimeout: '5m',
});
export async function shieldedWorkflow(data: any): Promise<void> {
// This activity will complete even if the workflow is cancelled
await CancellationScope.nonCancellable(() => criticalWrite(data));
}
Use cancelRequested to detect cancellation while waiting on a shielded operation:
import { CancellationScope, CancelledFailure, proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { longRunningTask } = proxyActivities<typeof activities>({
startToCloseTimeout: '10m',
});
export async function raceExample(input: string): Promise<any> {
let result: any;
const scope = new CancellationScope({ cancellable: false });
const promise = scope.run(() => longRunningTask(input));
try {
result = await Promise.race([scope.cancelRequested, promise]);
} catch (err) {
if (!(err instanceof CancelledFailure)) throw err;
// Workflow was cancelled but we still wait for the shielded activity
result = await promise;
}
return result;
}
import { CancellationScope, proxyActivities, isCancellation } from '@temporalio/workflow';
import type * as activities from './activities';
const { setup, doWork, cleanup } = proxyActivities<typeof activities>({
startToCloseTimeout: '10m',
});
export async function nestedScopeWorkflow(url: string): Promise<void> {
// Setup is shielded from cancellation
await CancellationScope.nonCancellable(() => setup());
try {
// Work has a 30s deadline
await CancellationScope.withTimeout(30_000, () => doWork(url));
} catch (err) {
if (isCancellation(err)) {
// Cleanup is shielded from cancellation
await CancellationScope.nonCancellable(() => cleanup(url));
}
throw err;
}
}
Operations are cancelled by the scope they were created in, not the scope they are awaited in:
import { CancellationScope, proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { fetchData } = proxyActivities<typeof activities>({
startToCloseTimeout: '5m',
});
export async function sharedPromiseExample(): Promise<any> {
// Activities created in root scope
const p1 = fetchData('http://url1.example.com');
const p2 = fetchData('http://url2.example.com');
const result = await CancellationScope.cancellable(async () => {
const first = await Promise.race([p1, p2]);
// Cancelling this scope does NOT cancel p1/p2 (they belong to root scope)
CancellationScope.current().cancel();
return first;
});
return result;
}
For an activity to receive a cancellation request, it must:
Context.current().heartbeat()proxyActivities// In workflow
const { longActivity } = proxyActivities<typeof activities>({
startToCloseTimeout: '1h',
heartbeatTimeout: '30s', // REQUIRED for cancellation to work
});
// In activity
import { Context } from '@temporalio/activity';
export async function longActivity(): Promise<void> {
for (let i = 0; i < 1000; i++) {
Context.current().heartbeat(i); // send progress
// If cancelled, heartbeat() will throw CancelledFailure
await doChunk(i);
}
}
Exception: Local Activities can be cancelled without heartbeats because they run in the same Worker process.
Use reset when a workflow is stuck due to non-determinism errors or bugs that have been fixed:
temporal workflow reset \
--workflow-id <workflow-id> \
--event-id <event-id> \
--reason "Description of fix applied"
With namespace and TLS:
temporal workflow reset \
--workflow-id my-workflow \
--event-id 4 \
--reason "Fixed non-deterministic code" \
--namespace my-namespace \
--tls-cert-path /path/to/cert.pem \
--tls-key-path /path/to/key.pem
Reset copies the Event History up to the reset point into a new execution and replays from there with current code. Progress after the reset point is discarded.
| Mistake | Why It Fails | Fix |
|---|---|---|
Running cleanup outside nonCancellable | Cleanup activity is immediately cancelled because root scope is already cancelled | Wrap cleanup in CancellationScope.nonCancellable() |
| Not awaiting the cancelled promise | CancelledFailure is only thrown when the promise is awaited | Always await the promise after calling scope.cancel() |
Catching CancelledFailure but not re-throwing | Workflow appears to succeed instead of being marked cancelled | Re-throw after cleanup: throw err |
Using err instanceof CancelledFailure for activities | Activity cancellation throws ActivityFailure, not CancelledFailure directly | Use isCancellation(err) instead |
| Activity not heartbeating | Activity never receives the cancellation signal | Add heartbeat() calls and set heartbeatTimeout |
Forgetting heartbeatTimeout in proxy config | Even with heartbeat calls, server won't track without timeout | Set heartbeatTimeout in proxyActivities options |
| Starting activity in already-cancelled scope | Activity immediately throws CancelledFailure without executing | Check scope state or use nonCancellable if the activity must run |
heartbeat() calls in the activity AND heartbeatTimeout in the proxy config. Without both, cancellation will not propagate.isCancellation(err) which handles ActivityFailure and ChildWorkflowFailure wrappers.CancellationScope.nonCancellable(). After the root scope is cancelled, any new operation in a cancellable scope is immediately cancelled.withTimeout inside nonCancellable.If this skill does not answer your question, use Context7 to search /temporalio/sdk-typescript or /temporalio/samples-typescript for more details.
Example queries:
context7 query /temporalio/sdk-typescript "CancellationScope API and options"context7 query /temporalio/samples-typescript "cancellation heartbeating example"context7 query /temporalio/sdk-typescript "how to cancel a child workflow"