> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nimbleway.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloudflare Agents SDK

> Run Nimble Web Search Agent research from a Cloudflare Worker. Durable Object-backed start, status, and result tools survive a restart without losing the run.

## Overview

Give a Cloudflare Worker the ability to research the live web. Ask a question in one sentence. Get back a written answer with sources, per-claim citations, and a confidence grade.

The [`@nimble-way/cloudflare-agents`](https://www.npmjs.com/package/@nimble-way/cloudflare-agents) package brings [Nimble Web Search Agent](/nimble-sdk/web-search-agents/overview) research into the [Cloudflare Agents SDK](https://developers.cloudflare.com/agents/). Nimble plans the research, searches, and opens pages. It reads JavaScript-heavy sites, cross-checks what it finds, and returns a cited answer. Your Worker sends a task and collects the result.

That research takes minutes, not milliseconds. A `high` effort run takes 5 to 15 minutes, far longer than any Worker request. So the package parks the run in a Durable Object and hands you a key to collect it later.

Without this, you would store the run ID in Durable Object storage, set an alarm to poll it, and build your own create-idempotency so a retry does not bill twice.

* **A cited answer from the live open web**: prose or JSON, with sources, per-claim citations, and confidence grades, read at run time.
* **Gets cheaper the more you run it**: a reused agent remembers which sources worked. Recurring research runs up to 50% cheaper.
* **Start now, collect later**: start in one request, read the result in another. An eviction or a redeploy resumes the same run rather than creating a new one.
* **Never bills you twice**: within one agent instance, the same request can never start a second run.

### Which path to take

Two ways to get Nimble into a Cloudflare agent. Pick one in ten seconds.

<CardGroup cols={2}>
  <Card title="Nimble MCP Server" icon="plug" href="#using-the-nimble-mcp-server-instead">
    The fastest start. No package, no Durable Object binding, no migration. Tools arrive over the wire and the model calls them. Best for interactive agents and short tasks.
  </Card>

  <Card title="This package" icon="hard-drive" href="#quick-start">
    For runs that must survive. Run identity lives in durable storage and resumes after a restart or a redeploy. Best when losing a 15-minute research run is unacceptable.
  </Card>
</CardGroup>

## Prerequisites

<AccordionGroup>
  <Accordion title="A Cloudflare Workers project" icon="cloud">
    A Wrangler version that supports SQLite Durable Objects, and a Workers account with Durable Objects enabled. The agent binds as a SQLite Durable Object, so the project needs a `new_sqlite_classes` migration.
  </Accordion>

  <Accordion title="agents 0.20.0 or later" icon="cube">
    `agents` is a peer dependency, range `>=0.20.0 <1`. Your project supplies it. The package uses `startFiber()` and `onFiberRecovered()`, which land in `0.20.0`.
  </Accordion>

  <Accordion title="Node.js 20 or later" icon="node-js">
    Required for the build and test toolchain. The published code targets the Workers runtime, not Node.
  </Accordion>

  <Accordion title="NIMBLE_API_KEY (Worker secret)" icon="key">
    Get a key from the [dashboard](https://online.nimbleway.com/settings/api-keys). Store it with `wrangler secret put`. See [Handling the API key](#handling-the-api-key) for the rules that keep it out of model input.
  </Accordion>

  <Accordion title="Optional: A Web Search Agent ID or name" icon="robot">
    Pass `nimbleAgentId` to run against a Web Search Agent you already created, for example `wsa_01j9x8...`. Or pass `agent_name` in the request to create one on first use and reuse it by name after that.

    Pass one of them for anything that runs more than once. A reused agent keeps its memory, so it gets faster, more accurate, and up to 50% cheaper on recurring work.

    Omit both and Nimble provisions a fresh, disposable agent for that run. No memory carries over, so every run starts from zero at full price. Both paths return a Web Search Agent ID you can read back.

    Reusing the agent is the Nimble half. For scheduled work, see [Scheduled and recurring runs](#scheduled-and-recurring-runs) for the Cloudflare half.
  </Accordion>
</AccordionGroup>

## Quick Start

<Steps>
  <Step title="Install">
    ```bash theme={"system"}
    npm install @nimble-way/cloudflare-agents@0.1.0 agents
    ```

    `0.1.0` is the current release.
  </Step>

  <Step title="Store the API key as a Worker secret">
    ```bash theme={"system"}
    wrangler secret put NIMBLE_API_KEY
    ```

    Never put the key in `wrangler.jsonc`, in a `vars` block, or in a committed `.dev.vars`.
  </Step>

  <Step title="Bind the agent as a SQLite Durable Object">
    ```jsonc wrangler.jsonc theme={"system"}
    {
      "name": "my-research-worker",
      "main": "src/index.ts",
      "compatibility_date": "2026-09-01",
      "compatibility_flags": ["nodejs_compat"],
      "durable_objects": {
        "bindings": [
          { "name": "NIMBLE_RUN_AGENT", "class_name": "NimbleRunAgent" }
        ]
      },
      "migrations": [
        { "tag": "v1", "new_sqlite_classes": ["NimbleRunAgent"] }
      ]
    }
    ```

    The binding name must be `NIMBLE_RUN_AGENT`. The package reads it from `env`.

    `nodejs_compat` is required, not optional. The Agents SDK imports `node:async_hooks` for the `AsyncLocalStorage` behind every fiber. Without the flag `wrangler deploy` fails to resolve built-in Node modules.
  </Step>

  <Step title="Export the agent and route to it">
    ```ts src/index.ts theme={"system"}
    import { getAgentByName } from "agents";
    import type { Env as NimbleEnv } from "@nimble-way/cloudflare-agents";

    export { NimbleRunAgent } from "@nimble-way/cloudflare-agents";

    interface Env extends NimbleEnv {}

    export default {
      async fetch(request: Request, env: Env): Promise<Response> {
        const url = new URL(request.url);

        // One instance per trusted principal. See the warning below.
        const runAgent = await getAgentByName(env.NIMBLE_RUN_AGENT, "research-session");

        if (url.pathname === "/start") {
          const outcome = await runAgent.startRun({
            request: {
              input: "Summarize how the EU AI Act is being enforced in 2026, with sources.",
              effort: "high",
              use_case: "research",
            },
          });
          return Response.json(outcome);
        }

        const fiberKey = url.searchParams.get("key");
        if (!fiberKey) {
          return new Response("Missing key", { status: 400 });
        }

        if (url.pathname === "/status") {
          return Response.json(await runAgent.getRunLifecycleStatus(fiberKey));
        }

        if (url.pathname === "/result") {
          return Response.json(await runAgent.getRunTypedResult(fiberKey));
        }

        return new Response("Not found", { status: 404 });
      },
    };
    ```

    <Warning>
      This example has no authentication. `/start` bills a Nimble run per request. Add your own auth check before `wrangler deploy`, and give each authenticated principal its own agent instance name instead of the fixed `"research-session"`. See [Handling the API key](#handling-the-api-key).

      `startRun` accepts the work and returns before the create is attempted, so a missing `NIMBLE_API_KEY` still returns a successful-looking receipt. The failure appears on the next status read.

      Add `@cloudflare/workers-types` to your tsconfig `types`.
    </Warning>
  </Step>

  <Step title="Run it locally first">
    ```bash theme={"system"}
    wrangler dev
    curl -s http://localhost:8787/start
    ```

    `/start` returns a queued receipt with a `fiberKey`. Read `/status?key=...` a few times
    to watch the run progress, then `/result?key=...` once the status is terminal.

    Stay local until you have added an auth check. `wrangler dev` exercises the Durable
    Object, the ledger, and all three tools without exposing a billable endpoint.
  </Step>

  <Step title="Deploy">
    ```bash theme={"system"}
    wrangler deploy
    ```

    Deploy once `/start` is behind your own authentication. See the warning in step 4.
  </Step>
</Steps>

### Example response

`startRun` accepts the work and returns identity, not an answer:

```json theme={"system"}
{
  "fiberKey": "nimble-run:generated:b0f1c7d4e2a98f6315c0d7b8a4e6f2913d5c8a70e1b4f9c2a6d3e8b1f704c295",
  "accepted": true,
  "runId": null,
  "status": "queued"
}
```

`runId` is `null` on that first response because creation happens in the background. The Cloudflare Agents SDK calls that background task a fiber. It survives a restart.

`accepted` is `true` when this call started a new run, and `false` when an identical request had already been submitted to this agent instance. On `false` the other fields describe the run that already exists. Check it before treating a response as a new run.

Read the status to get the durable identifiers:

```bash theme={"system"}
curl -s "https://my-research-worker.example.workers.dev/status?key=nimble-run:generated:b0f1c7d4..."
```

```json theme={"system"}
{
  "fiberKey": "nimble-run:generated:b0f1c7d4e2a98f6315c0d7b8a4e6f2913d5c8a70e1b4f9c2a6d3e8b1f704c295",
  "runId": "task_run_01k5m9v3x8zqhpc2n7t4wr6bde",
  "agentId": "wsa_01k5m9v3wq2fhtn8b4r7cxz1sd",
  "status": "running",
  "pollAttempts": 7,
  "recoveredCount": 0,
  "lastError": null
}
```

Once the run is terminal, read the result:

```bash theme={"system"}
curl -s "https://my-research-worker.example.workers.dev/result?key=nimble-run:generated:b0f1c7d4..."
```

```json theme={"system"}
{
  "fiberKey": "nimble-run:generated:b0f1c7d4e2a98f6315c0d7b8a4e6f2913d5c8a70e1b4f9c2a6d3e8b1f704c295",
  "runId": "task_run_01k5m9v3x8zqhpc2n7t4wr6bde",
  "agentId": "wsa_01k5m9v3wq2fhtn8b4r7cxz1sd",
  "status": "completed",
  "result": {
    "outputType": "text",
    "content": "National market surveillance authorities began issuing formal notices in 2026 [1] ...",
    "trust": {
      "confidence": "high",
      "reasoning": "Primary sources include the official EU register and two national authority notices.",
      "claims": [
        {
          "confidence": "high",
          "reasoning": "Stated directly in the enforcement notice.",
          "callout": 1,
          "citations": [
            {
              "url": "https://standards-authority.example.com/notices/2026-04",
              "title": "Market surveillance notice, April 2026",
              "excerpts": ["Enforcement powers apply from ..."]
            }
          ]
        }
      ],
      "sources": [
        {
          "type": "primary",
          "url": "https://standards-authority.example.com/notices/2026-04",
          "title": "Market surveillance notice, April 2026"
        }
      ]
    },
    "error": null
  },
  "error": null
}
```

## How it works

<Steps>
  <Step title="The Worker routes to a named agent instance">
    `getAgentByName(env.NIMBLE_RUN_AGENT, name)` returns a stub for one Durable Object. Every run started through that stub is recorded in that object's storage.
  </Step>

  <Step title="A deterministic key deduplicates the create">
    The package hashes the full create request with SHA-256: the target agent ID plus `input`, `agent_name`, `effort`, `enable_events`, `input_data`, `output_schema`, `previous_interaction_id`, `skill`, `sources`, and `use_case`. The `fiberKey` is `nimble-run:`, then the agent ID or `generated`, then that hash. It is also the `startFiber()` idempotency key. Calling `startRun` twice with an identical request joins the existing run rather than billing a second one. Change `effort` and you get a different key, because Nimble bills that as different work.
  </Step>

  <Step title="Creation runs in a durable fiber, never retried">
    The create call reaches Nimble through `@nimble-way/nimble-js` configured with `maxRetries: 0`. The run ID and Web Search Agent ID are written to the durable ledger before polling starts.
  </Step>

  <Step title="Polling waits for a terminal state">
    The fiber polls `GET /v2/agents/{agent_id}/runs/{run_id}` every 10 seconds by default. Safe reads retry with bounded exponential backoff and honor `Retry-After`.
  </Step>

  <Step title="Recovery resumes the same run">
    Say the Durable Object is evicted mid-run, or the Worker is redeployed. Recovery runs off a persisted alarm rather than waiting for an inbound request, and resumes polling from the stored run ID. It never re-issues a create.

    That alarm exists because a fiber is in flight. Once the fiber settles, including at the [polling deadline](#the-polling-deadline-is-local-not-terminal), nothing is left to recover and you read the run yourself.
  </Step>
</Steps>

### Run identity

Three identifiers travel with a run. They are not interchangeable:

| Identifier | Shape                         | What it is                                                                      |
| ---------- | ----------------------------- | ------------------------------------------------------------------------------- |
| `agentId`  | `wsa_...`                     | The durable Web Search Agent. Reused across runs when you pass `nimbleAgentId`. |
| `runId`    | `task_run_...`                | One research run against that agent.                                            |
| `fiberKey` | `nimble-run:<agent>:<64 hex>` | The package's local handle for the run. Pass it to `status` and `result`.       |

Keep the `fiberKey` client-side or in your own store. It is the only argument the status and result tools need.

## The three tools

<AccordionGroup>
  <Accordion title="startRun(input)" icon="play">
    Starts a research run and returns immediately.

    **Input** (`StartRunInput`):

    * `request` (required, `CreateRunRequest`): `input` is the task text and is required. Optional fields are `agent_name`, `effort`, `input_data`, `output_schema`, `previous_interaction_id`, `skill`, `sources`, `use_case`, and `enable_events`.
    * `nimbleAgentId` (optional): a `wsa_...` ID. Omit it and Nimble provisions the agent for the run.
    * `apiKeyOverride` (optional): an ephemeral per-request key that takes precedence over `env.NIMBLE_API_KEY`. It is never persisted.

    **Output** (`StartRunOutcome`): `fiberKey`, `accepted`, `runId`, `status`. `runId` is `null` until the background create lands. `accepted` is `false` when an identical request already exists on this instance.

    `use_case` is locked once the agent exists. A later run against the same agent must omit it or repeat its current value, or Nimble returns `422`. `skill`, `sources`, and `output_schema` stay overridable per run. `agent_name` applies only when `nimbleAgentId` is omitted, and `sources` is a `SourceGuidance` object, not a list of domains.

    **How create failures surface.** `startFiber` accepts work and returns without waiting for it, so `startRun` resolves before the create is attempted and none of these reach the caller as exceptions. Each one lands as `status: "failed"` with its message in `lastError` on the next `getRunLifecycleStatus`. Status is the error channel.

    * `GatedFeatureError` when `effort` is `max`. `max` is a custom-budget tier that is [coming soon](/nimble-sdk/web-search-agents/efforts#max). The package never silently substitutes `x-high`, so ask for `x-high` explicitly if you want it now.
    * `CreateRateLimitedError` on a definite `429`. Carries `retryAfterSeconds`. Never retried.
    * `AmbiguousCreateError` on a transport failure or a `5xx`, where the write may or may not have landed. Read [Duplicate protection](#duplicate-protection) before you do anything with this.
    * `NimbleAgentAPIError` for every other API failure. Carries `statusCode` and `refId`.
  </Accordion>

  <Accordion title="getRunLifecycleStatus(fiberKey, apiKeyOverride?)" icon="gauge">
    Reads the durable ledger for one run.

    **Output** (`StatusOutcome`): `fiberKey`, `runId`, `agentId`, `status`, `pollAttempts`, `recoveredCount`, `lastError`.

    `status` is one of `queued`, `running`, `completed`, `failed`, or `cancelled`. Two more values come from the package itself: `unknown` when no ledger row exists, and `recovery-blocked` when a run created with `apiKeyOverride` lost that key across a restart. Anything else fails closed rather than being guessed at.

    A `recovery-blocked` row never resolves on its own. Background reconciliation skips it deliberately. Only `resumeWithOverride` clears it.

    This is a durable read, not a live call, so it is cheap. If the row looks stale, the package makes one safe `GET` to bring it current. It never issues a create. `apiKeyOverride` is only needed to reconcile a row that was started with an override key.
  </Accordion>

  <Accordion title="getRunTypedResult(fiberKey, apiKeyOverride?)" icon="file-lines">
    Fetches the Nimble result once the run is terminal.

    **Output** (`ResultOutcome`): `fiberKey`, `runId`, `agentId`, `status`, `result`, `error`.

    `result` is `null` while the run is still active. Poll `getRunLifecycleStatus` until `status` is terminal, then call this. See [The result surface](#the-result-surface) for what `result` contains.

    If the run was started with `apiKeyOverride`, re-supply the same key here. The server fallback key is never substituted, and the result reads as `null` without it.
  </Accordion>
</AccordionGroup>

Two recovery methods sit alongside the three tools. Neither is part of the model-facing surface:

* `reconcileRunLifecycleStatus(fiberKey, apiKeyOverride?)` forces one safe `GET` regardless of the poll throttle, then returns the ledger. Use it for operator repair of a stuck row.
* `resumeWithOverride(fiberKey, apiKeyOverride)` clears a `recovery-blocked` row once the caller re-supplies the key. It resumes an existing run and can never create one.

## The result surface

`getRunTypedResult` normalizes the Nimble envelope into `NimbleResult` without flattening it:

| Field        | Type                                  | Meaning                                                                          |
| ------------ | ------------------------------------- | -------------------------------------------------------------------------------- |
| `outputType` | `"text"` \| `"json"` \| `"unknown"`   | Which shape `content` holds.                                                     |
| `content`    | `string` \| object \| array \| `null` | Prose for text output, structured data for JSON output. `null` on a failed run.  |
| `trust`      | `NimbleTrust` \| `null`               | Confidence, reasoning, per-claim citations, and sources. `null` on a failed run. |
| `error`      | `{ message, refId? }` \| `null`       | Structured failure detail. `null` on a completed run.                            |

`trust.claims[]` carries a `confidence` grade, `reasoning`, and `citations[]` with `url`, `title`, and `excerpts`. Text output marks each claim with a `callout` number matching a marker in the prose. JSON output uses a `path` into the structure instead. `trust.sources[]` labels each source `primary` or `secondary`.

Run identity travels with the result. `ResultOutcome` carries `runId` and `agentId` alongside `status` and `error`. A stored result stays traceable back to the run that produced it. See [Trust and citations](/nimble-sdk/web-search-agents/trust) for how the grades are assigned.

## Nimble research vs. Cloudflare AI Search

Both return grounded answers. They ground them in different things.

| Dimension | Nimble Web Search Agent                          | Cloudflare AI Search                              |
| --------- | ------------------------------------------------ | ------------------------------------------------- |
| Source    | The live open web                                | Content you indexed into a Cloudflare data source |
| Freshness | Read at run time                                 | As fresh as your last index run                   |
| Coverage  | Anything publicly reachable                      | Only what you put in                              |
| Shape     | Multi-step research across many pages            | Retrieval in a single request                     |
| Latency   | Seconds to minutes, by effort                    | Sub-second                                        |
| Output    | Answer with per-claim trust grades and citations | Retrieved chunks, optionally generated over       |

When the answer is already in your corpus, AI Search is the better tool: faster, cheaper, and already wired into your account.

Reach for a Nimble run when the answer sits on the public web and nobody indexed it for you. Competitor pricing this morning. A regulatory change last week. A company profile assembled from twenty sources.

They compose. Index your own documents in AI Search, and commission a Nimble run when the corpus comes up short.

## Cost and latency

Nimble bills per run, at a flat price per [effort level](/nimble-sdk/web-search-agents/efforts). Compute, retrieval, and storage are included.

| Effort   | Typical time     | Price         |
| -------- | ---------------- | ------------- |
| `low`    | 10 to 30 seconds | \$0.025 / run |
| `medium` | 1 to 3 minutes   | \$0.10 / run  |
| `high`   | 5 to 15 minutes  | \$0.50 / run  |
| `x-high` | 15 to 30 minutes | \$2.00 / run  |

`high` is the default for a run with no agent identity. A persistent Web Search Agent uses its own stored default, which templates set as high as `x-high` (\$2.00/run). Set `effort` explicitly if cost matters. `max` is a custom-budget tier and is coming soon; requesting it raises `GatedFeatureError`.

Cloudflare charges separately for Durable Object requests, duration, and storage. A run parked between polls costs very little, but the ledger row persists until you remove it. See [Cleanup](#cleanup).

## Running this in production

Everything below is what to get right once the example above works.

### Handling the API key

`NIMBLE_API_KEY` is a Worker secret. It must never appear in:

* model-facing tool schemas or tool inputs
* browser or client state
* run outputs, `lastError` strings, or fixtures
* Worker logs, Gateway logs, screenshots, or evidence
* code examples, including the ones in your own docs

The tool inputs above carry a task and safe overrides. They carry no credential unless your application supplies `apiKeyOverride` server-side. That value is deliberately excluded from the durable ledger and from the fiber recovery snapshot.

<Warning>
  If a Worker serves more than one authenticated principal, route each trusted principal to a **separately named** `NimbleRunAgent` instance. Deduplication and the run ledger are scoped to one Durable Object instance. Multiplexing untrusted users into one instance lets them observe and join each other's runs. HTTP authentication is the consuming Worker's job.
</Warning>

### The asynchronous contract

Creation returns identity, not an answer. Everything after that is a read.

| Setting                          | Value                                                                |
| -------------------------------- | -------------------------------------------------------------------- |
| Default status poll interval     | 10 seconds (`DEFAULT_POLL_INTERVAL_MS`)                              |
| Minimum production poll interval | 10 seconds; shorter values are rejected                              |
| Safe-read retry policy           | 3 attempts, 500 ms base delay, 8 second cap (`DEFAULT_RETRY_POLICY`) |
| `429 Retry-After` on a safe read | Honored, capped at the 8 second maximum delay                        |
| `429` on a create                | Fails immediately as `CreateRateLimitedError`, never retried         |
| Background polling budget        | 5 minutes per fiber                                                  |

Override the interval with the `NIMBLE_POLL_INTERVAL_MS` environment variable. Values below 10 seconds are rejected in production. Point the base URL elsewhere with `NIMBLE_BASE_URL`; it defaults to `https://sdk.nimbleway.com`.

### The polling deadline is local, not terminal

Background polling stops after 5 minutes. A `high` run takes 5 to 15 minutes. An `x-high` run takes 15 to 30. A long run routinely outlives that budget. This is expected, and it is not a failure.

When the deadline hits, the package records `"Polling deadline exceeded"` in `lastError` and **leaves `status` at the last real provider status**. It does not mark the run `failed`. The run is still going at Nimble.

After the deadline no background poll is running. Poll `getRunLifecycleStatus` yourself. Each call at least one poll interval after the last update issues one safe `GET` and brings the row current. `reconcileRunLifecycleStatus` forces that read immediately.

<Note>
  Read the deadline as "this Worker stopped watching", not "the run died". On a non-terminal row, `lastError: "Polling deadline exceeded"` means read status again later. It is not a reason to start a second run.
</Note>

### Duplicate protection

A Nimble run is billable, and running the same request twice bills twice. The package refuses to create one twice by accident:

* **No automatic retry of creation.** Not on `429`, not on `5xx`, not on a transport failure. The underlying client runs with `maxRetries: 0`.
* **Durable deduplication.** The `fiberKey` hash covers the target agent ID and the full create request. It is registered as the Agents SDK idempotency key, so an identical `startRun` joins the existing run and returns `accepted: false`.
* **Ambiguous creates are never resubmitted.** A transport failure or a `5xx` gives no signal about whether the write landed. The package raises `AmbiguousCreateError` and stops.

<Warning>
  On `AmbiguousCreateError`, do not resubmit. Reconcile first. List the runs for the agent, or `GET` the run if an ID is already known. Only create a new run once you have established that none exists. Blind resubmission bills a second run and produces two answers to one question.
</Warning>

Recovery follows the same rule. Say the Durable Object is evicted between the create and the confirmation. `onFiberRecovered()` marks the row rather than retrying. `lastError` then reads `Recovered before create was confirmed; not retried (ambiguous write)`.

### Scheduled and recurring runs

<Warning>
  The Agents SDK keeps fiber idempotency keys permanently, with no expiry and no automatic pruning. So a scheduled Worker that sends the same prompt to the same instance joins the first run every time. It returns the original answer, with no error and no second charge, and every status field reads healthy.

  Give each scheduled run its own Durable Object instance name. This is required for recurring work, not a tidiness measure.
</Warning>

Keep passing the same `nimbleAgentId`. Agent memory and the recurring-cost reduction live on the Nimble side and are unaffected. Rotate only the Cloudflare-side instance name:

```ts theme={"system"}
const today = new Date().toISOString().slice(0, 10);
const runAgent = await getAgentByName(env.NIMBLE_RUN_AGENT, `monitor-${today}`);
```

The dedupe ledger is scoped per instance, so a fresh instance means a fresh run. Rotating also bounds ledger growth and pairs with `destroy()` once the result is stored.

To keep one long-lived instance, you must prune the ledger yourself with `deleteFibers({ status: ["completed", "error", "aborted"], settledBefore })`. Nothing prunes it for you.

### Cleanup

The run ledger lives in the Durable Object's SQLite storage and does not expire on its own. Two habits keep it bounded:

* **Scope instance names.** Name the agent per session, per user, or per job rather than using one global instance forever. The ledger grows with the runs recorded in it.
* **Destroy finished instances.** `Agent` exposes `destroy()`, inherited by `NimbleRunAgent`. Call it once every run in that instance is terminal and the results are stored somewhere durable. Treat it as fire-and-forget.
* **Clear settled dedupe rows.** `deleteFibers({ status, settledBefore, limit })` removes terminal rows only. It defaults to 100 per call and caps at 500, so a busy instance needs repeated calls. Nothing sweeps them automatically.

Do not destroy an instance with a non-terminal run in it. The run continues at Nimble and is billed either way. The local handle is gone, so the result has to be reconciled by run ID.

### Attribution

Every request the package sends to Nimble carries `X-Client-Source: cloudflare-agents`. The package sets it. Do not set it yourself, and do not expect to see it in your own code.

## Optional: routing through AI Gateway

[Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/) can sit in front of Nimble as a Custom Provider. It gives you Gateway analytics and logging over the same traffic.

This is an optional path, not the recommended one. Agent API V2 is a multi-endpoint asynchronous lifecycle, not a chat-completions provider. The Gateway is a transparent HTTP hop, not a lifecycle-aware provider. It understands nothing about runs. The direct route is the default for good reason.

Set the Custom Provider's own `base_url` to `https://sdk.nimbleway.com`, then point the package at the gateway:

```bash theme={"system"}
NIMBLE_BASE_URL=https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/custom-{slug}
```

Everything after `custom-{slug}/` is appended, so `/v2/agents/...` routes correctly.

<Warning>
  **Set Retry requests to zero attempts.** A gateway-level retry of the create POST bills a second run, and the package's own `maxRetries: 0` cannot stop it. This is the setting that costs money if you get it wrong.

  **Leave Cache Responses off.** It is off by default. A cached status response reports a stale lifecycle state, and a cached create response hides whether a run was actually created.

  **Use an unauthenticated gateway.** The package sends a fixed header set and exposes no hook to add one, so `cf-aig-authorization` can never be sent. An authenticated gateway rejects every request.
</Warning>

These are gateway-side settings, configured in the Cloudflare dashboard rather than in your Worker. Decide explicitly whether Gateway logs retain request and response bodies before you route research traffic through it.

## Using the Nimble MCP Server instead

If you want Nimble tools in a Cloudflare agent without adding a dependency, connect the [Nimble MCP Server](/integrations/mcp-server/mcp-server) over Streamable HTTP. The Agents SDK connects to it directly:

```ts theme={"system"}
import { Agent } from "agents";

interface Env {
  NIMBLE_API_KEY: string;
}

export class ResearchAgent extends Agent<Env> {
  async onStart() {
    await this.addMcpServer("nimble", "https://mcp.nimbleway.com/mcp", {
      transport: {
        type: "streamable-http",
        headers: {
          Authorization: `Bearer ${this.env.NIMBLE_API_KEY}`,
        },
      },
    });
  }
}
```

<Warning>
  Pass the credential in the `Authorization` header, never in the URL. A key in a URL leaks into server access logs, browser history, and `Referer` headers sent to third parties. Header values are not logged the same way.
</Warning>

This is the shortest path to Nimble tools in a Worker. It trades durability for setup speed: nothing here survives an eviction mid-run.

The MCP server page covers the full transport options, the authentication options including OAuth, and the complete tool table. This section does not repeat them.

[Code Mode](https://developers.cloudflare.com/agents/api-reference/code-mode/) is a third option: the model writes TypeScript against your tools instead of calling them one at a time. It works with either path above and is not documented here.

## Next steps

<CardGroup cols={2}>
  <Card title="Web Search Agent" icon="robot" href="/nimble-sdk/web-search-agents/overview">
    What a research run is, and how to shape one.
  </Card>

  <Card title="Efforts" icon="gauge-high" href="/nimble-sdk/web-search-agents/efforts">
    Effort levels, run times, and prices.
  </Card>

  <Card title="Trust and citations" icon="shield-check" href="/nimble-sdk/web-search-agents/trust">
    How confidence grades and per-claim citations are produced.
  </Card>

  <Card title="Nimble MCP Server" icon="plug" href="/integrations/mcp-server/mcp-server">
    Transports, authentication, and the full tool table.
  </Card>

  <Card title="Package on npm" icon="npm" href="https://www.npmjs.com/package/@nimble-way/cloudflare-agents">
    Release notes and the published surface.
  </Card>

  <Card title="Cloudflare Agents" icon="cloud" href="https://developers.cloudflare.com/agents/">
    Durable execution, fibers, and the Agents SDK.
  </Card>
</CardGroup>
