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
This package
Prerequisites
A Cloudflare Workers project
A Cloudflare Workers project
new_sqlite_classes migration.agents 0.20.0 or later
agents 0.20.0 or later
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.Node.js 20 or later
Node.js 20 or later
NIMBLE_API_KEY (Worker secret)
NIMBLE_API_KEY (Worker secret)
wrangler secret put. See Handling the API key for the rules that keep it out of model input.Optional: A Web Search Agent ID or name
Optional: A Web Search Agent ID or name
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
Install
0.1.0 is the current release.Store the API key as a Worker secret
wrangler.jsonc, in a vars block, or in a committed .dev.vars.Bind the agent as a SQLite Durable Object
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.Export the agent and route to it
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.Deploy
/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:
How it works
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.A deterministic key deduplicates the create
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.Creation runs in a durable fiber, never retried
@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.Polling waits for a terminal state
GET /v2/agents/{agent_id}/runs/{run_id} every 10 seconds by default. Safe reads retry with bounded exponential backoff and honor Retry-After.Recovery resumes the same run
Run identity
Three identifiers travel with a run. They are not interchangeable:fiberKey client-side or in your own store. It is the only argument the status and result tools need.
The three tools
startRun(input)
startRun(input)
StartRunInput):request(required,CreateRunRequest):inputis the task text and is required. Optional fields areagent_name,effort,input_data,output_schema,previous_interaction_id,skill,sources,use_case, andenable_events.nimbleAgentId(optional): awsa_...ID. Omit it and Nimble provisions the agent for the run.apiKeyOverride(optional): an ephemeral per-request key that takes precedence overenv.NIMBLE_API_KEY. It is never persisted.
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.GatedFeatureErrorwheneffortismax.maxis a custom-budget tier that is coming soon. The package never silently substitutesx-high, so ask forx-highexplicitly if you want it now.CreateRateLimitedErroron a definite429. CarriesretryAfterSeconds. Never retried.AmbiguousCreateErroron a transport failure or a5xx, where the write may or may not have landed. Read Duplicate protection before you do anything with this.NimbleAgentAPIErrorfor every other API failure. CarriesstatusCodeandrefId.
getRunLifecycleStatus(fiberKey, apiKeyOverride?)
getRunLifecycleStatus(fiberKey, apiKeyOverride?)
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.getRunTypedResult(fiberKey, apiKeyOverride?)
getRunTypedResult(fiberKey, apiKeyOverride?)
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.reconcileRunLifecycleStatus(fiberKey, apiKeyOverride?)forces one safeGETregardless of the poll throttle, then returns the ledger. Use it for operator repair of a stuck row.resumeWithOverride(fiberKey, apiKeyOverride)clears arecovery-blockedrow 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.
Nimble research vs. Cloudflare AI Search
Both return grounded answers. They ground them in different things.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,
lastErrorstrings, or fixtures - Worker logs, Gateway logs, screenshots, or evidence
- code examples, including the ones in your own docs
apiKeyOverride server-side. That value is deliberately excluded from the durable ledger and from the fiber recovery snapshot.
The asynchronous contract
Creation returns identity, not an answer. Everything after that is a read.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. Ahigh 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.
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 on5xx, not on a transport failure. The underlying client runs withmaxRetries: 0. - Durable deduplication. The
fiberKeyhash covers the target agent ID and the full create request. It is registered as the Agents SDK idempotency key, so an identicalstartRunjoins the existing run and returnsaccepted: false. - Ambiguous creates are never resubmitted. A transport failure or a
5xxgives no signal about whether the write landed. The package raisesAmbiguousCreateErrorand stops.
onFiberRecovered() marks the row rather than retrying. lastError then reads Recovered before create was confirmed; not retried (ambiguous write).
Scheduled and recurring runs
Keep passing the samenimbleAgentId. Agent memory and the recurring-cost reduction live on the Nimble side and are unaffected. Rotate only the Cloudflare-side instance name:
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.
Agentexposesdestroy(), inherited byNimbleRunAgent. 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.
Attribution
Every request the package sends to Nimble carriesX-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 ownbase_url to https://sdk.nimbleway.com, then point the package at the gateway:
custom-{slug}/ is appended, so /v2/agents/... routes correctly.
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.