How to trigger an activity during external cancellation

Hello!

I have a workflow that sometimes can be cancelled externally from the Temporal UI using the Request Cancel button and I’m unable to do it without receiving a Temporal error

I copied the same lines of code that we have in the TypeScript SDK tutorial here: Interrupt a Workflow - TypeScript SDK | Temporal Platform Documentation

It works to detect an external cancel, but doesn’t works when I try to execute an activity during cancelation.

Example:

import * as wf from "@temporalio/workflow";
import { CancellationScope, isCancellation, proxyActivities } from "@temporalio/workflow";

import type { actionActivities } from "@temporal/activities";

const {
  recordAction,
  completeAction,
  rejectAction,
  cancelAction,
  expireAction
} = proxyActivities<typeof actionActivities>({
  taskQueue: "action",
  startToCloseTimeout: "10 seconds",
});

export async function myActionWorkflow(workflowId: string, args: ActionWorkflowArgs) {
  // Example code
  await recordAction(args);

  // This one works perfect
  wf.setHandler(cancelSignal, async (input: CancelArgs) => {
    await cancelAction(args, input);
  });
  
  wf.setHandler(rejectSignal, async (input: RejectArgs) => {
    await rejectAction(args, input);
  });

  try {
    const timer = createTimer(86000);
    const timerExpiredPromise = timer.then(async () => {
      await expireAction(args);
    });

    // Just an example to don't show unnecessary code
    await wf.sleep("3 day");
  } catch (error) {
    if (isCancellation(error)) {
      wf.log.info("Workflow cancelled externally"); // I see this log

      await CancellationScope.nonCancellable(() =>
        cancelAction(args, {}),
      );

      wf.log.info("Cleanup: cancelAction completed"); // I don't see this log
    }
  }
}

The code in cancelAction is just a SQL insert in Drizzle.

export const cancelAction = async (...) => {
  const [inserted] = await db
    .insert(traces)
    .values({
      id: uuid(),
      actionId: actionId,
      status: "CANCEL"
    })
    .returning();

  if (!inserted) {
    throw new Error("Failed to create trace");
  }

  return { traceId: inserted.id };
} 

The error that I’m seeing is the following:

2025-12-23T19:59:05.306Z [INFO] Workflow cancelled externally - {
  sdkComponent: 'workflow',
  taskQueue: '...',
  namespace: '...',
  workflowId: 'my-workflow-name-workflow_id',
  runId: '...uuid...',
  workflowType: 'myWorkflowName'
}

2025-12-23T19:59:05.306407Z  WARN temporal_sdk_core::worker::workflow: Failing workflow task run_id=...uuid... failure=Failure { failure: Some(Failure { message: "Workflow cancelled", source: "TypeScriptSDK", stack_trace: "CancelledFailure: Workflow cancelled\n    at RootCancellationScope.cancel (/Project/route/node_modules/@temporalio/workflow/src/cancellation-scope.ts:264:16)\n    at Activator.cancelWorkflow (/Project/route/node_modules/@temporalio/workflow/src/internals.ts:481:19)\n    at /Project/route/node_modules/@temporalio/workflow/src/worker-interface.ts:136:29\n    at Object.activate (/Project/route/node_modules/@temporalio/workflow/src/worker-interface.ts:160:2)\n    at evalmachine.<anonymous>:1:18", encoded_attributes: None, cause: None, failure_info: Some(CanceledFailureInfo(CanceledFailureInfo { details: None })) }), force_cause: Unspecified }

The only think that I can think of based on the message is that I can’t trigger an activity if the workflow has been cancelled but as I’m new in Temporal I have no idea.

Just in case I want to use the same activity when I receive a cancel signal and when I execute Request cancel from the UI.

Thanks in advance for your insights!

I think issue is not your cancelation logic its this timer you create:

const timer = createTimer(86000);
const timerExpiredPromise = timer.then(async () => {
await expireAction(args);
});

You need to handle promise error here as this timer also canceled.

I think also in your outer try/catch

} catch (error) {
if (isCancellation(error)) {
wf.log.info("Workflow cancelled externally");

Here you can re-throw error if you want your workflow execution to end up in Canceled status. Otherwise what you have now think will end up as Completed which also might be fine for you, your choice whats best to later on track this situation if use visibility queries.