diff --git a/docs/upgrade-to-v4.mdx b/docs/upgrade-to-v4.mdx
index 9c384cb429..5f106ae2b7 100644
--- a/docs/upgrade-to-v4.mdx
+++ b/docs/upgrade-to-v4.mdx
@@ -170,6 +170,107 @@ tasks.onComplete(({ ctx, result }) => {
});
```
+### onCancel
+
+Available in v4.0.0-beta.12 and later.
+
+You can now define an `onCancel` hook that is called when a run is cancelled. This is useful if you want to clean up any resources that were allocated for the run.
+
+```ts
+tasks.onCancel(({ ctx, signal }) => {
+ console.log("Run cancelled", signal);
+});
+```
+
+You can use the `onCancel` hook along with the `signal` passed into the run function to interrupt a call to an external service, for example using the [streamText](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) function from the AI SDK:
+
+```ts
+import { logger, tasks, schemaTask } from "@trigger.dev/sdk";
+import { streamText } from "ai";
+import { z } from "zod";
+
+export const interruptibleChat = schemaTask({
+ id: "interruptible-chat",
+ description: "Chat with the AI",
+ schema: z.object({
+ prompt: z.string().describe("The prompt to chat with the AI"),
+ }),
+ run: async ({ prompt }, { signal }) => {
+ const chunks: TextStreamPart<{}>[] = [];
+
+ // 👇 This is a global onCancel hook, but it's inside of the run function
+ tasks.onCancel(async () => {
+ // We have access to the chunks here, and can save them to the database
+ await saveChunksToDatabase(chunks);
+ });
+
+ try {
+ const result = streamText({
+ model: getModel(),
+ prompt,
+ experimental_telemetry: {
+ isEnabled: true,
+ },
+ tools: {},
+ abortSignal: signal, // 👈 Pass the signal to the streamText function, which aborts with the run is cancelled
+ onChunk: ({ chunk }) => {
+ chunks.push(chunk);
+ },
+ });
+
+ const textParts = [];
+
+ for await (const part of result.textStream) {
+ textParts.push(part);
+ }
+
+ return textParts.join("");
+ } catch (error) {
+ if (error instanceof Error && error.name === "AbortError") {
+ // streamText will throw an AbortError if the signal is aborted, so we can handle it here
+ } else {
+ throw error;
+ }
+ }
+ },
+});
+```
+
+The `onCancel` hook can optionally wait for the `run` function to finish, and access the output of the run:
+
+```ts
+import { logger, task } from "@trigger.dev/sdk";
+import { setTimeout } from "node:timers/promises";
+
+export const cancelExampleTask = task({
+ id: "cancel-example",
+ // Signal will be aborted when the task is cancelled 👇
+ run: async (payload: { message: string }, { signal }) => {
+ try {
+ // We pass the signal to setTimeout to abort the timeout if the task is cancelled
+ await setTimeout(10_000, undefined, { signal });
+ } catch (error) {
+ // Ignore the abort error
+ }
+
+ // Do some more work here
+
+ return {
+ message: "Hello, world!",
+ };
+ },
+ onCancel: async ({ runPromise }) => {
+ // You can await the runPromise to get the output of the task
+ const output = await runPromise;
+ },
+});
+```
+
+
+ You will have up to 30 seconds to complete the `runPromise` in the `onCancel` hook. After that
+ point the process will be killed.
+
+
### Improved middleware and locals
Our task middleware system is now much more useful. Previously it only ran "around" the `run` function, but now we've hoisted it to the top level and it now runs before/after all the other hooks.
@@ -704,7 +805,7 @@ export const myTask = task({
id: "my-task",
onStart: ({ payload, ctx }) => {},
// The run function still uses separate parameters
- run: async ( payload, { ctx }) => {},
+ run: async (payload, { ctx }) => {},
});
```
@@ -760,4 +861,4 @@ const batchHandle = await tasks.batchTrigger([
// Now you need to call runs.list()
const runs = await batchHandle.runs.list();
console.log(runs);
-```
\ No newline at end of file
+```