惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
Microsoft Security Blog
Microsoft Security Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Vercel News
Vercel News
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
Check Point Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
The Hacker News
The Hacker News
L
LINUX DO - 热门话题
T
Tenable Blog
Hugging Face - Blog
Hugging Face - Blog
Google Online Security Blog
Google Online Security Blog
博客园 - Franky
P
Proofpoint News Feed
H
Hacker News: Front Page
P
Privacy & Cybersecurity Law Blog
月光博客
月光博客
P
Proofpoint News Feed
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
博客园_首页
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
CERT Recently Published Vulnerability Notes
Forbes - Security
Forbes - Security
I
InfoQ
Stack Overflow Blog
Stack Overflow Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
博客园 - 叶小钗
T
Threat Research - Cisco Blogs
aimingoo的专栏
aimingoo的专栏
D
Darknet – Hacking Tools, Hacker News & Cyber Security
小众软件
小众软件
大猫的无限游戏
大猫的无限游戏
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Hacker News - Newest:
Hacker News - Newest: "LLM"
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
O
OpenAI News
G
Google Developers Blog
Martin Fowler
Martin Fowler
罗磊的独立博客
S
SegmentFault 最新的问题
T
Tor Project blog
量子位

Workflow SDK Documentation

Patterns for Defining Tools Human-in-the-Loop Building Durable AI Agents Queueing User Messages Resumable Streams Sleep, Suspense, and Scheduling Streaming Updates from Tools API Reference Workflow Globals Changelog Resilient run start Cookbook Building a World Deploying Astro Express Fastify Hono Getting Started NestJS Next.js Nitro Nuxt Python SvelteKit Vite corrupted-event-log fetch-in-workflow hook-conflict Errors node-js-module-in-workflow serialization-failed start-invalid-workflow-function step-not-registered timeout-in-workflow webhook-invalid-respond-with-value webhook-response-not-sent workflow-not-registered Errors & Retrying Hooks & Webhooks Idempotency Foundations Serialization Starting Workflows Streaming Versioning Workflows and Steps How the Directives Work Encryption Event Sourcing Framework Integrations Understanding Directives Migration Guides Migrating from AWS Step Functions Migrating from Inngest Migrating from Temporal Observability Testing Server-Based Testing createHook createWebhook defineHook FatalError fetch getStepMetadata getWorkflowMetadata getWritable workflow RetryableError sleep @workflow/vitest DurableAgent @workflow/ai WorkflowChatTransport getHookByToken getRun getWorld workflow/api resumeHook resumeWebhook Chat Session Modeling runtime-decryption-failed Upgrading Workflows abort-signal-timeout-in-workflow Cancellation How Cancellation Works Internal Serializable AbortController and AbortSignal Eager Processing of Steps & Incremental Event Replay TanStack Start Sequential & Parallel Execution Workflow Composition Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK Migrating from trigger.dev Secure Credential Handling Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK
Agent Cancellation
2026-04-29 · via Workflow SDK Documentation

Cancel a running agent from the outside — either immediately via run.cancel() or gracefully via a stop signal hook.

Cancel a running agent from the outside — for example, a "Stop" button in a chat UI, an admin cancellation endpoint, or a timeout fallback. Two patterns are available depending on whether you need the agent to exit cleanly or just need the run to stop: Hard Cancellation via getRun(runId).cancel() for immediate forced termination, or Stop Signal via a hook + Promise.race for a graceful exit that runs cleanup and notifies streaming clients before returning.

  • Chat stop buttons — let users cancel a long-running agent from the browser
  • Admin cancellation — stop an agent from a different process or API
  • Timeout fallback — combine with sleep() to auto-stop after a deadline

Pick the option that matches what your endpoint needs to deliver to the caller:

  • Hard Cancellation — terminates the run immediately with no opportunity for cleanup or client notification. A single line of code, but the workflow throws WorkflowRunCancelledError and any streaming clients see an abrupt connection close.
  • Stop Signal — the workflow exits as soon as the hook fires, runs any pending cleanup, emits a final data-stopped part to the stream so the client can render cleanly, and returns a real result.

The trade-offs at a glance:

Hard CancellationStop Signal
MechanismgetRun(runId).cancel()Hook + Promise.race
Speed to terminateImmediateAt the next await boundary in the workflow
Runs finally / cleanupNoYes
Final stream notificationNo (abrupt close)Yes (data-stopped part)
run.returnValueThrows WorkflowRunCancelledErrorReturns the workflow's result
Code complexityOne lineHook + race + signal step
Best forStuck or unresponsive runs, forced terminationUser-facing stop, admin cancel, timeouts

Call .cancel() on a run to terminate it immediately:

import { getRun } from "workflow/api";

export async function POST(
  _request: Request,
  { params }: { params: Promise<{ runId: string }> }
) {
  const { runId } = await params;
  await getRun(runId).cancel(); 
  return Response.json({ success: true });
}

This is an abrupt termination — the run is stopped mid-step with no opportunity to exit cleanly:

  • No cleanup runsfinally blocks, defer-style step cleanup, and any logic after the current step are all skipped
  • No final notification to the client — the writable closes abruptly, so a streaming UI just sees the connection drop with no data-stopped part to render a clean ending
  • run.returnValue throws — anyone awaiting the result receives WorkflowRunCancelledError instead of a meaningful payload
  • Underlying step keeps running — same caveat as the Stop Signal pattern below: the model stream or HTTP call inside the current step continues to completion in the background

Hard Cancellation is the appropriate choice when the run is stuck or unresponsive, has exceeded its expected runtime, or you don't need a clean exit. For everything else — chat stop buttons, admin "stop" actions, timeout fallbacks — you typically want the Stop Signal pattern: the agent finishes its current step, emits a final stream part so the client renders a clean ending, and returns a real result.

Limitation: This pattern does not cancel the underlying model stream. The agent step writing to the writable continues running in the background until it completes — tokens generated after the stop signal are still produced (and billed by your model provider). What this pattern does is exit the workflow function as soon as the hook fires and emit a data-stopped part so the client can stop rendering. For hard cross-process cancellation that signals the inner step to bail out, see Distributed Abort Controller.

Example

import { DurableAgent } from "@workflow/ai/agent";
import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
import { z } from "zod";
import type { ModelMessage, UIMessageChunk } from "ai";

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

async function searchWeb({ query }: { query: string }) {
  "use step";
  await new Promise((r) => setTimeout(r, 1500));
  return { results: [{ title: `${query} - Wikipedia`, snippet: `Overview of ${query}...` }] };
}

async function analyzeData({ topic }: { topic: string }) {
  "use step";
  await new Promise((r) => setTimeout(r, 1200));
  return { summary: `Analysis of ${topic}: significant developments found.`, confidence: 0.85 };
}

async function emitStopSignal(details: { reason?: string }) { 
  "use step";
  const writer = getWritable<UIMessageChunk>().getWriter();
  try {
    await writer.write({ type: "data-stopped", id: "stop-signal", data: details } as UIMessageChunk);
  } finally {
    writer.releaseLock();
  }
}

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

  const { workflowRunId } = getWorkflowMetadata();
  const hook = stopHook.create({ token: `stop:${workflowRunId}` }); 

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

  const result = await Promise.race([ 
    agent
      .stream({ messages, writable: getWritable<UIMessageChunk>(), maxSteps: 15 })
      .then((r) => ({ type: "complete" as const, messages: r.messages })),
    hook.then(({ reason }) => ({ type: "stopped" as const, reason })), 
  ]);

  if (result.type === "stopped") {
    await emitStopSignal({ reason: result.reason }); 
  }

  return result;
}

API Route to Trigger Stop

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

"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>
  );
}
  1. A hook is created with token stop:${workflowRunId} when the workflow starts
  2. Promise.race runs the agent stream and the stop hook concurrently
  3. When the stop API resumes the hook, the race resolves immediately — the workflow exits
  4. Before returning, emitStopSignal writes a data-stopped part to the stream so the client knows the agent was stopped (not just disconnected)
  5. The client detects data-stopped and updates the UI accordingly

This is the same pattern used by the Distributed Abort Controller — race a long-running operation against a hook signal.

  • Add a timeout — race a third sleep() promise to auto-stop after a deadline
  • Audit logging — include a reason field in the stop schema to record who stopped and why
  • Cross-process — the hook token is deterministic, so any process can call stopHook.resume() with the run ID
  • Step limits — combine with maxSteps on the agent to cap execution even without manual stop
  • Hard Cancellation as a fallback — wire your stop endpoint to fall back to getRun(runId).cancel() if the hook resume errors with not found / expired (for example, the hook was already consumed). This guarantees the run is terminated even when the Stop Signal path is unavailable.