Skip to main content

Overview

Give a Mastra agent 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/mastra package brings Nimble Web Search Agent research into Mastra as three typed tools. Nimble plans the research, searches, and opens pages. It reads JavaScript-heavy sites, cross-checks what it finds, and returns a cited answer. Your agent 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 a single agent turn. So by default none of the three tools block: the run keeps going server-side and the runId is the only handle needed to collect it. Bounded waiting is opt-in, and the wait option is the one way to make the result tool block.
  • A cited answer from the live open web: prose or JSON, with sources, per-claim citations with verbatim excerpts, and confidence grades, read at run time.
  • Gets cheaper the more you run it: a reused Web Search Agent remembers which sources worked. Recurring research runs up to 50% cheaper.
  • Start now, collect later: start the run in one turn, read the result in another turn, another process, or another day.
  • The model never sees your credential: the API key is resolved server-side and sent only as a request header.

Which path to take

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

Nimble MCP Server

The fastest start. No package to install. Tools arrive over the wire and the model calls them. Gives you the full Nimble tool surface, not just research.

This package

Typed tools, a developer-controlled effort ceiling, server-side output schemas, and explicit create-failure semantics. Best when research is the job and cost control matters.

Prerequisites

The package declares engines.node >= 22.13.0.
Peer dependencies, supplied by your project: @mastra/core ^1.56.0 and zod ^3.25.1 || ^4.0.0.The zod floor is 3.25.1 rather than 3.25.0 for a reason. 3.25.0 declares a ./v4 export whose target is missing from its published tarball, and Mastra’s schema layer resolves that path, so the combination fails at import. The declared range excludes it, so npm refuses the install rather than letting it fail at run time.Verified against the published 0.1.1: zod 3.25.1, 3.25.76, and 4.0.0 all work with @mastra/core 1.56.0, as does 4.5.4 with 1.64.0.
Get a key from the dashboard. See Handling the API key for the rules that keep it out of model input.
Mastra resolves the agent’s model at run time, so the provider needs its own key in the environment. The examples here use anthropic/claude-sonnet-4-6, which reads ANTHROPIC_API_KEY.This is separate from NIMBLE_API_KEY. Nimble does the research; your model decides when to ask for it.
Create a Web Search Agent once, then reuse it. Its ID looks like wsa_0a1b2c3d4e5f60718293a4b5c6d7e8f9.A reused agent keeps its memory, so it gets faster, more accurate, and up to 50% cheaper on recurring work. Create one in the dashboard or with POST /v2/agents.The agent is developer configuration, not model input. The model can never choose which agent to run.

Quick Start

1

Install

0.1.1 is the current release. It is published from CI over OIDC and carries a SLSA provenance attestation, so the artifact on npm can be traced to the commit and workflow that built it.
2

Set the environment

Both are read server-side at execute time. Never put either in a committed file.
3

Attach the tools to an agent

src/agents/researcher.ts
createNimbleAgentTools() returns all three tools sharing one credential and one agent configuration. Pass { agentId, apiKey } to override the environment.
4

Ask it something

The model calls nimble-agent-start-run and answers in seconds with a task_run_… ID, which it reports in result.text. The research keeps going server-side for minutes. Collect it later with the same ID.

Example Response

A completed run, as the result tool returns it. This is the prose shape. An agent with a stored output schema returns "type": "json" with a json field instead of text, and its claims carry path rather than callout:

How it works

1

The model starts a run

nimble-agent-start-run sends POST /v2/agents/{agent_id}/runs and returns immediately with a real task_run_… ID. The turn is never blocked for the run’s duration.
2

The run proceeds server-side

Nimble plans the research, searches, opens pages, and cross-checks findings. Minutes, not milliseconds.
3

Anything with the runId can check it

nimble-agent-run-status returns an instant snapshot. nimble-agent-run-result returns the answer, or { ready: false } while the run is still working.
4

The answer arrives with its evidence

A completed run returns prose or structured JSON, plus trust: sources, per-claim citations with verbatim excerpts, and confidence grades.

The three tools

The factory returns them keyed nimbleAgentStartRun, nimbleAgentRunStatus, and nimbleAgentRunResult. Those three inputs are the complete model-facing surface. The credential, the agent, the effort ceiling, the output schema, and the source boundary are all developer configuration.

The resumable lifecycle

The runId is the only handle. Any process configured with the same agent can check or collect a run it did not start.

Parameters

The Web Search Agent to run, for example wsa_0a1b2c3d4e5f60718293a4b5c6d7e8f9. Defaults to process.env.NIMBLE_AGENT_ID. Resolved at execute time, so the model can never choose the agent.Missing it raises NimbleConfigError: Missing Nimble agent id.
Nimble API key. Defaults to process.env.NIMBLE_API_KEY. Read server-side at execute time and sent only as a request header.Missing it raises NimbleConfigError: Missing Nimble API key.
effort is the tier used when the model does not choose one. Leave it unset and the agent instance’s own configured default applies, which is the recommendation.effortCap is the ceiling on what the model may request. It defaults to high, so a model cannot unilaterally reach the x-high and max cost tiers. A developer-set effort is not clamped.See effort tiers for what each tier does and costs.Verified behaviour, reading the request body the package sends:Those are the request bodies the package sends. max is a custom-budget tier that is coming soon, so the API may reject it even though the package forwards it. Ask for x-high if you want the top available tier now.
A JSON Schema sent as the Agent API output_schema. Use it when the run must return structured data.Keep this in developer configuration. Do not ask the model to author JSON, source URLs, or excerpts in its prompt.
A completed run then returns output.type: "json" with output.json instead of output.text.
Server-enforced source guidance, sent as the Agent API sources.
Rejected before the run is created: an empty allow, a group with a blank title, a group with an empty domains array, and a blank domain string.
Opt-in bounded waiting on the result tool. Absent, which is the default, the tool never blocks.
Defaults are timeoutMs: 300_000 and pollIntervalMs: 10_000, with a 100 ms floor. Runs take minutes, so polling faster only spends requests. Waiting respects Mastra’s per-call AbortSignal.When the timeout elapses the tool returns { ready: false } and the run keeps going server-side. A bounded wait never invents a terminal status: it returns the last authoritative one. If the initial status request itself reaches the deadline, status is unknown and activity is omitted, because no lifecycle snapshot was received.
Options forwarded to the Nimble client: baseURL, fetch, timeout, and maxRetries.maxRetries applies to status and result reads only. Run creation is always sent with maxRetries: 0, whatever this value is.

Per-tool configuration

Individual factories are exported when the three tools need different settings:

A long-running research example

This is the shape the package is built for. The run outlives the turn that started it.
src/research.ts
Persist the runId wherever your application keeps state. It is the only handle needed, and it stays valid across processes and restarts.
Keep the “never start a second run” clause in the instructions. A bounded wait can expire before a high run completes, and the result tool returns { ready: false } whenever it does. That is expected, not a failure. Without the clause, restarting is an available response to a not-ready result, and each restart is a new billed run. Nothing in effortCap limits how many runs a model may create.
The three tools are built for the model to call. To drive the lifecycle yourself, server-side and without a model in the loop, use the exported createNimbleClient(apiKey) and call nimble.agents.runs.create, .get, and .result directly.

Citations and trust metadata

Every completed run carries a trust object:
  • confidence and reasoning: the run’s own grade of its answer, and why.
  • sources: every source used, with type and source_category.
  • claims: per-claim confidence, plus citations carrying the url and the verbatim excerpts the claim rests on. In a prose answer, a callout number ties the claim to its marker in the text. Under an output schema there is no prose, so claims carry a path instead: the JSON path of the value they support. Read whichever is present, not callout alone.
trust is passed through verbatim. snake_case field names are preserved and unknown future fields are kept, so nothing is silently dropped and prose citation markers stay aligned with the text.
Trust-provided source URLs, claims, citations, and excerpts are the authoritative record. Render them from trust. Never ask the model to restate them as its own output fields, and never treat a model-authored URL as a citation.

Handling the API key

NIMBLE_API_KEY is a server-side credential. The package resolves it at execute time and sends it only as a request header. Verified against the published package, reading the requests it actually sends: the key appears in Authorization: Bearer … and nowhere else. It is absent from all three tool input schemas, absent from tool outputs, and scrubbed from error messages.
The key must never appear in a model schema, a tool input, a tool output, client state, a log line, a prompt, or an example. The model’s entire input surface is task, effort, and runId. Keep it that way.Do not pass the key through a URL. A credential in a URL leaks into server access logs, browser history, and Referer headers sent to third parties.
If you inject your own client with client, attribution headers and credential handling become your concern. Pass the matching apiKey too, or server error details are withheld, because the package cannot prove they are safe to expose.

Failure and not-ready behaviour

An active run is never an error. Two cases matter, and the package treats them differently on purpose.
The result tool returns { ready: false } with the run’s status and isActive. Not an error. Check again later.Branch on ready, never on status. A not-ready payload can carry any status, including a terminal one, and it never carries output. The same HTTP 409 on a completed run reports differently depending on wait:So if (status === "completed") render(output.text) throws on a payload that is entirely legal. A status reported after a wait deadline is also the last snapshot taken rather than a fresh read, so a run that failed after that snapshot can still report running. Confirm with the status tool, which never waits, before treating a run as alive.
Throws NimbleAgentRunError with reason: "failed", the server’s message, and the runId preserved so the run can still be inspected or reported.Verified message shape: Nimble agent run task_run_… failed: <server message>.
A definite 4xx such as 429 gives createOutcome: "not-created" and reason: "request". No run exists. Retry only after the underlying condition clears.
A timeout, 408, 409, 5xx, or a dropped connection gives createOutcome: "unknown". The run may have been created server-side.An ambiguous create is never retried automatically. Run creation is billed and not idempotent, so the POST is sent once with maxRetries: 0. The transient-retry policy is disabled for creation only. Status and result reads keep normal retries.Reconcile before creating again: list recent runs for the agent, or check the dashboard. An automatic retry here bills a second run.The thrown error carries no runId, because none was returned, so a run that was created cannot be reconciled by ID. Reconcile by agent and timestamp instead.maxRetries: 0 only removes the transport retry. A model that is told to retry a failed start reinstates it one layer up, and the package cannot tell that apart from a new task. Keep the “do not try again” clause in the example instructions.
Surfaces as reason: "protocol" on NimbleAgentRunError, not a TypeError.

Mastra background tasks

Mastra can run a tool call as a background task, so the agent turn returns before the tool finishes. This is optional. By default the Nimble tools already return immediately. Reach for it when you want Mastra to hold the thread open and re-invoke the agent once the answer lands. Enable it on the Mastra instance, then opt the tool in:
With untilIdle, the stream stays open until the background task completes and the agent is re-invoked with the result:
You can also look a task up directly:
Mastra’s taskId and Nimble’s runId are different identifiers for different things. The taskId tracks Mastra’s local execution of the tool call. The runId is the research run itself, and it is the only handle that survives a restart, a redeploy, or a move to another process.Persist and surface the runId. A Mastra taskId cannot collect a Nimble run, and a task that fails or is evicted does not stop or reclaim the run it started.
Mastra ships its own webSearchTool. It is not an alternative to Nimble research, and the difference is worth ten seconds of thought. webSearchTool resolves the active model at runtime and delegates to that provider’s own search. When the provider has none, it throws a MastraError with the id WEB_SEARCH_UNSUPPORTED_PROVIDER. Reach for webSearchTool for a quick in-turn lookup on a supported provider. Reach for a Nimble run when the answer has to be researched rather than recalled, has to carry its evidence, or has to be reproducible across models. They coexist: attach both and let the instructions route between them.

Using the Nimble MCP Server instead

If you want Nimble tools in a Mastra agent without adding a dependency, connect the Nimble MCP Server over Streamable HTTP:
Pass the credential in the Authorization header, through requestInit, 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, and it gives you the whole Nimble tool surface rather than research alone. What it does not give you is the package’s developer-side controls. Those are the effort ceiling that stops a model reaching max, the server-enforced output schema and source allowlist, and the explicit not-created against unknown distinction on a failed create. Choose the MCP server for an interactive agent that needs breadth. Choose the package when research is the job and you need those controls. 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.

Attribution

Every request the package makes carries X-Client-Source: mastra. The package sets this itself, on run creation and on every status and result read. There is nothing to configure, and nothing for you to set. The value is a constant. It carries no credential and no user-identifying data. The one exception is an injected client. The package then makes no requests of its own, so attribution travels only if your client sets the header.

Next Steps

Web Search Agent

What a Web Search Agent is, how effort tiers work, and how agent memory makes recurring research cheaper.

Nimble MCP Server

Transports, the full authentication options including OAuth, and the complete tool table.

Trust

How sources, per-claim citations, excerpts, and confidence grades are produced.

Package on npm

Release notes, the typed exports, and the changelog.