---
title: Agent Cancellation
description: Cancel a running WorkflowAgent from the outside using AbortSignal and a durable stop hook.
type: guide
summary: Cancel a running WorkflowAgent cooperatively with AbortController so the model stream stops and the workflow can return a clean status.
---

<CopyPrompt
  text="Add cancellation to this AI SDK WorkflowAgent. For hard cancellation, expose a server route that receives `runId` and calls `getRun(runId).cancel()` from `workflow/api`. For graceful cancellation, define `stopHook` with `defineHook()` from `workflow`, create it with a stable token such as the workflow run ID, and race `WorkflowAgent.stream()` against the stop hook using `Promise.race`. Pass an `AbortController` signal to the agent, forward the tool execution signal to cancellable I/O, and wait for the agent branch to settle after aborting before returning a stopped status. Wire the UI Stop button to the route that resumes the hook or falls back to `getRun(runId).cancel()`, and verify stale run IDs are handled."
/>

Cancel a running agent from the outside through a **Stop** button in a chat user interface (UI), an admin cancellation endpoint, or a timeout fallback.

## Pattern

Create an `AbortController` in the workflow and race the agent (passing its signal) against a stop hook. When the hook fires, call `controller.abort()`, then wait for the agent branch to observe the signal and settle before the workflow returns. The signal cancels the underlying model stream and is passed to tool execution; each tool must forward it to cancellable I/O. The workflow return value records whether it completed or stopped; read that value through the run API or persist it in application state.

{/* @skip-typecheck: requires AI SDK 7 and @ai-sdk/workflow */}
```typescript lineNumbers
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
import {
  isStepCount,
  tool,
  type ModelMessage,
  type ToolExecutionOptions,
} from "ai";
import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
import { z } from "zod";

export const stopHook = defineHook({
  schema: z.object({ reason: z.string().optional() }),
});

async function searchWeb(
  { query }: { query: string },
  { abortSignal }: ToolExecutionOptions<unknown>,
) {
  "use step";
  const response = await fetch(
    `https://api.example.com/search?q=${encodeURIComponent(query)}`,
    { signal: abortSignal }, // [!code highlight]
  );
  return response.json();
}

async function analyzeData(
  { topic }: { topic: string },
  { abortSignal }: ToolExecutionOptions<unknown>,
) {
  "use step";
  const response = await fetch("https://api.example.com/analyze", {
    method: "POST",
    body: JSON.stringify({ topic }),
    signal: abortSignal, // [!code highlight]
  });
  return response.json();
}

export async function stoppableAgent(messages: ModelMessage[]) {
  "use workflow";

  const { workflowRunId } = getWorkflowMetadata();
  const controller = new AbortController(); // [!code highlight]
  const hook = stopHook.create({ token: `stop:${workflowRunId}` });

  const agent = new WorkflowAgent({
    model: "spacexai/grok-4.6",
    instructions: "You are a research assistant. Search and analyze data as needed.",
    tools: {
      searchWeb: tool({
        description: "Search the web for information",
        inputSchema: z.object({ query: z.string() }),
        execute: searchWeb,
      }),
      analyzeData: tool({
        description: "Analyze a piece of data",
        inputSchema: z.object({ topic: z.string() }),
        execute: analyzeData,
      }),
    },
  });

  const agentPromise = agent.stream({
    messages,
    writable: getWritable<ModelCallStreamPart>(),
    abortSignal: controller.signal, // [!code highlight]
    stopWhen: isStepCount(15),
  });

  const outcome = await Promise.race([
    agentPromise.then((result) => ({
      type: "complete" as const,
      messages: result.messages,
    })),
    hook.then(({ reason }) => ({
      type: "stop-requested" as const,
      reason,
    })),
  ]);

  if (outcome.type === "stop-requested") {
    controller.abort(outcome.reason); // [!code highlight]
    await agentPromise; // Wait until the losing branch has stopped. // [!code highlight]
    return { type: "stopped" as const, reason: outcome.reason };
  }

  return outcome;
}
```

### API route to trigger stop

```typescript lineNumbers
import { stopHook } from "@/workflows/stoppable-agent";

export async function POST(
  request: Request,
  { params }: { params: Promise<{ runId: string }> }
) {
  const { runId } = await params;
  const { reason } = await request.json();

  await stopHook.resume(`stop:${runId}`, {
    reason: reason || "User requested stop",
  });

  return Response.json({ success: true });
}
```

### Client stop button

```tsx lineNumbers
"use client";

export function StopButton({ runId }: { runId: string }) {
  const handleStop = async () => {
    await fetch(`/api/chat/${runId}/stop`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ reason: "User clicked stop" }),
    });
  };

  return (
    <button type="button" onClick={handleStop}>
      Stop Agent
    </button>
  );
}
```

## How it works

1. The workflow creates an `AbortController` when it starts.
2. The workflow creates a hook with the token `stop:${workflowRunId}`.
3. `Promise.race` runs the agent stream and the stop hook concurrently.
4. The agent receives `controller.signal`. When aborted, the signal cancels the active model stream, propagates to tool execution, and prevents another model step from starting. Tool implementations must forward it to operations such as `fetch` that support cancellation.
5. When the stop API resumes the hook, the race reports a stop request. The workflow aborts the controller and awaits `agentPromise`, so it does not return while that branch is still running.
6. After the agent branch settles, the workflow returns a `stopped` result. That return value is not written to the model-call stream automatically; the application can read `run.returnValue` or persist the status separately.

## Adapting this

- **Add a timeout**: Race a third `sleep()` promise to stop automatically after a deadline.
- **Audit logging**: Include a `reason` field in the stop schema to record who stopped the agent and why.
- **Cross-process**: The hook token is deterministic, so any process can call `stopHook.resume()` with the run ID.
- **Step limits**: Combine the pattern with `stopWhen: isStepCount(...)` to cap execution without a manual stop.

## Key APIs

- [`defineHook()`](/docs/api-reference/workflow/define-hook): Defines a type-safe hook for the stop signal.
- [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata): Provides the run ID for deterministic hook tokens.
- [`getWritable()`](/docs/api-reference/workflow/get-writable): Stores durable model-call output from the agent.
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK's durable agent that applies the abort signal to model calls and replaces `DurableAgent`.
