# How to Handle Abort Signals in Long-Running Agent Actions in BuilderIO Agent-Native

> Learn to handle abort signals in long-running agent actions by passing an AbortSignal through the agent context for immediate cancellation of async operations like fetch and LLM calls.

- Repository: [Builder.io/agent-native](https://github.com/BuilderIO/agent-native)
- Tags: how-to-guide
- Published: 2026-06-29

---

**To handle abort signals in long-running agent actions, pass an optional `AbortSignal` through the action context and propagate it to every async operation—including `fetch` requests, `retryDelay` loops, and LLM calls—to enable immediate cancellation of long-running work.**

BuilderIO/agent-native provides first-class support for cancellation through the standard Web API `AbortSignal`. When building agents that perform network requests, heavy computation, or extended retries, accepting and propagating this signal prevents resource leaks, enforces hard timeouts, and keeps user interfaces responsive.

## Core Architecture of Abort Signal Handling

### Action Definition Contract

In [`packages/core/src/action.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/action.ts), the action definition declares an optional `signal?: AbortSignal` field in its context (line 80). This allows callers to supply a cancellation token that the runtime automatically injects into executing actions. By declaring `signal` in the action schema, you signal that the operation respects cancellation.

### Production Agent and Retry Logic

The `retryDelay` helper in [`packages/core/src/agent/production-agent.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/agent/production-agent.ts) (lines 72-89) creates a cancellable Promise that resolves after a back-off period but rejects immediately if the signal is aborted. This ensures retry loops don't wait unnecessarily when the user cancels or a timeout triggers.

### HTTP Fetch Integration

External calls consistently use `fetch(..., { signal })`. For example, in [`packages/core/src/cli/plan-local.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/cli/plan-local.ts) (line 324), the implementation applies `signal: init.signal ?? AbortSignal.timeout(PLAN_ACTION_HTTP_TIMEOUT_MS)` to enforce hard timeouts on plan operations, preventing indefinite hangs on slow endpoints.

### Completion Handling

For LLM text generation, [`packages/core/src/server/complete-text.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/server/complete-text.ts) (lines 102-108) uses `createCompletionAbortSignal` to combine a user-provided signal with an internal timeout. This ensures long-running completions can be interrupted cleanly without leaving orphaned connections.

## Implementation Patterns for Abort Signal Handling

### Defining Cancellable Actions

When defining actions in [`packages/core/src/action.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/action.ts), destructure the `signal` from the runtime context and pass it to all async operations. Use `AbortSignal.timeout` to provide a hard limit when the caller doesn't supply one.

```typescript
import { defineAction }引以为傲 from "@agent-native/core";

export default defineAction({
  schema: z.object({ url: z.string().url() }),
  run: async ({ url }, { signal }) => {
    // Abort after 10 seconds if the caller didn't provide its own timeout.
    const timeoutSignal = signal ?? AbortSignal.timeout(10_000);

    // Pass the signal to fetch; if aborted, fetch rejects with AbortError.
    const resp = await fetch(url, { signal: timeoutSignal });
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);

    return await resp.json();
  },
});

```

The action receives `{ signal }` from the runtime automatically. If the user cancels the UI action, the same signal aborts the fetch instantly, preventing memory leaks.

### Using retryDelay with Abort Signals

The `retryDelay` utility respects abort signals to prevent unnecessary waits during back-off periods. Always pass the signal to avoid waiting through full retry delays after cancellation.

```typescript
import { retryDelay } from "@agent-native/core";

async function resilientFetch(url: string, signal?: AbortSignal) {
  for (let attempt = 0; attempt < 5; ++attempt) {
    try {
      return await fetch(url, { signal });
    } catch (err) {
      // Network-related errors trigger a retry; abort signal stops the loop.
      if (signal?.aborted) throw new Error("aborted");
      await retryDelay(attempt, signal ?? new AbortController().signal);
    }
  }
  throw new Error("All retries failed");
}

```

As implemented in [`packages/core/src/agent/production-agent.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/agent/production-agent.ts), `retryDelay` checks `signal.aborted` and rejects immediately if the signal triggers, preventing the full back-off duration from elapsing.

### Creating Scoped Abort Controllers in UI Code

For client-side cancellation, use `createAbortController` from [`packages/core/src/client/chat/runtime.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/chat/runtime.ts) (lines 696-720). This utility merges an existing signal with a new one, ensuring downstream code can always attach abort listeners safely.

```typescript
import { createAbortController } from "@agent-native/core/client";

function useLongTask() {
  const abortCtrl = new AbortController();

  // Pass the controller's signal to the action.
  startAction({ url: "/slow-endpoint" }, { signal: abortCtrl.signal });

  // When the component unmounts or the user clicks "Cancel":
  return () => abortCtrl.abort();
}

```

This pattern ensures that even if the underlying action creates its own internal abort mechanisms, your component can still trigger cancellation.

## Common Pitfalls and Best Practices

- **Pass the signal to every `fetch`**: Always use `fetch(url, { signal })`. If ignored, requests continue after the UI closes, causing wasted bandwidth and memory leaks.

- **Wrap retry loops with signal checks**: Use `await retryDelay(attempt, signal)` rather than raw `setTimeout`. Without this, the loop waits the full back-off even after the user cancels.

- **Use `AbortSignal.timeout` for hard limits**: Apply `signal ?? AbortSignal.timeout(ms)` to prevent unbounded calls to external APIs that can hang indefinitely.

- **Check `signal.aborted` before expensive work**: Insert `if (signal?.aborted) throw new Error("aborted")` before CPU-bound tasks to avoid running calculations whose results will be discarded.

## Summary

- Accept `signal` in action definitions via the runtime context in [`packages/core/src/action.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/action.ts)
- Propagate signals to `fetch`, `retryDelay`, and LLM calls throughout the call stack
- Use `AbortSignal.timeout` for automatic hard limits on external requests
- Leverage `createAbortController` from [`packages/core/src/client/chat/runtime.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/chat/runtime.ts) for UI scoping
- Check `signal.aborted` before CPU-intensive work to prevent wasted computation

Following these patterns ensures that long-running agent actions can be stopped promptly, keeping applications responsive and preventing resource exhaustion.

## Frequently Asked Questions

### How do I cancel an agent action from a React component?

Create an `AbortController` in your component, pass its `signal` to the action via the options parameter, and call `controller.abort()` in a cleanup function or when the user clicks cancel. The `createAbortController` utility in [`packages/core/src/client/chat/runtime.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/chat/runtime.ts) can merge this with existing signals if needed.

### What happens if I don't pass the signal to fetch?

The HTTP request continues in the background even after the UI component unmounts or the user cancels, causing wasted bandwidth, potential memory leaks, and unnecessary server load.

### Can I combine multiple abort signals for complex timeouts?

Yes, use the `createAbortController` utility from [`packages/core/src/client/chat/runtime.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/chat/runtime.ts) (lines 696-720). This merges an existing signal with a new controller, ensuring downstream code can always call `signal.addEventListener` safely while respecting both user cancellation and internal timeouts.

### Does retryDelay automatically respect the abort signal?

Yes, `retryDelay` in [`packages/core/src/agent/production-agent.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/agent/production-agent.ts) (lines 72-89) checks `signal.aborted` and rejects immediately if the signal is triggered, preventing the function from waiting through the full back-off period when cancellation occurs.