Skip to main content

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 package brings Nimble Web Search Agent research into the Cloudflare Agents SDK. 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.

Nimble MCP Server

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.

This package

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.

Prerequisites

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.
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.
Required for the build and test toolchain. The published code targets the Workers runtime, not Node.
Get a key from the dashboard. Store it with wrangler secret put. See Handling the API key for the rules that keep it out of model input.
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 for the Cloudflare half.

Quick Start

1

Install

0.1.0 is the current release.
2

Store the API key as a Worker secret

Never put the key in wrangler.jsonc, in a vars block, or in a committed .dev.vars.
3

Bind the agent as a SQLite Durable Object

wrangler.jsonc
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.
4

Export the agent and route to it

src/index.ts
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.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.
5

Run it locally first

/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.
6

Deploy

Deploy once /start is behind your own authentication. See the warning in step 4.

Example response

startRun accepts the work and returns identity, not an answer:
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:
Once the run is terminal, read the result:

How it works

1

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.
2

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.
3

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.
4

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.
5

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, nothing is left to recover and you read the run yourself.

Run identity

Three identifiers travel with a run. They are not interchangeable: Keep the fiberKey client-side or in your own store. It is the only argument the status and result tools need.

The three tools

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. 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 before you do anything with this.
  • NimbleAgentAPIError for every other API failure. Carries statusCode and refId.
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.
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 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.
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: 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 for how the grades are assigned. Both return grounded answers. They ground them in different things. 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. Compute, retrieval, and storage are included. 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.

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.
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.

The asynchronous contract

Creation returns identity, not an answer. Everything after that is a read. 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.
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.

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.
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.
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

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.
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:
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 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:
Everything after custom-{slug}/ is appended, so /v2/agents/... routes correctly.
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.
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 over Streamable HTTP. The Agents SDK connects to it directly:
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.
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 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

Web Search Agent

What a research run is, and how to shape one.

Efforts

Effort levels, run times, and prices.

Trust and citations

How confidence grades and per-claim citations are produced.

Nimble MCP Server

Transports, authentication, and the full tool table.

Package on npm

Release notes and the published surface.

Cloudflare Agents

Durable execution, fibers, and the Agents SDK.