Discover available search providers, present a selection UI, collect credentials, and persist the chosen provider into OpenClawConfig.
Search Setup
Configuring and selecting web search providers through the setup wizard
When a user runs the search setup flow or types /docs search, the search setup module discovers all available search providers — both bundled and runtime-contributed — presents a selection prompt, handles API key credential input, and writes the chosen provider into OpenClawConfig. This module bridges the gap between plugin-contributed search providers and the user-facing configuration wizard.
📚 What is this & why does it matter?
Search setup is the configuration gateway for OpenClaw's web search capability. Every search provider — whether it's a bundled provider like Tavily or Brave, or a runtime plugin — must be discovered, selected, and credentialed through this flow before the agent can perform web searches. Understanding this module is essential for adding new search providers, debugging "no search provider configured" errors, or tracing how API keys are stored in the config.
💡 Real-world analogy
Think of setting up a new streaming service on your smart TV. First, the TV scans for all available apps (provider discovery). Then you pick one from the list (selection prompt). Next, you enter your login credentials (API key input). Finally, the TV saves your choice as the default (config persistence). The search setup flow is exactly this process — but for web search providers in an AI assistant.
Contents
Overview
Current OpenClawConfig, RuntimeEnv for environment variable checks, WizardPrompter for interactive prompts, and optional SetupSearchOptions.
Which providers are available? Does the user already have a key? Should the key come from env or manual input? Which provider is selected?
An updated OpenClawConfig with the selected provider, enabled flag set to true, and the credential applied. No direct I/O side effects.
flowchart TD
START["runSearchSetupFlow()"]
DISCOVER["resolveSearchProviderOptions()"]
CONTRIBS["resolveSearchProviderSetupContributions()"]
BUNDLED["Bundled providers"]
RUNTIME["Runtime plugin providers"]
MERGE["Merge & sort by label"]
PROMPT["Show provider selection prompt"]
HASKEY{"hasExistingKey()?"}
ENVKEY{"hasKeyInEnv()?"}
ASKKEY["Prompt for API key"]
USEENV["Use environment key"]
USEEXIST["Use existing key"]
APPLY["applySearchKey()"]
SELECT["applySearchProviderSelection()"]
CONFIG["Updated OpenClawConfig"]
START --> DISCOVER
DISCOVER --> CONTRIBS
CONTRIBS --> BUNDLED
CONTRIBS --> RUNTIME
BUNDLED --> MERGE
RUNTIME --> MERGE
MERGE --> PROMPT
PROMPT --> HASKEY
HASKEY -->|Yes| USEEXIST
HASKEY -->|No| ENVKEY
ENVKEY -->|Yes| USEENV
ENVKEY -->|No| ASKKEY
USEEXIST --> APPLY
USEENV --> APPLY
ASKKEY --> APPLY
APPLY --> SELECT
SELECT --> CONFIG
The diagram shows the complete search setup pipeline. Provider discovery merges bundled and runtime contributions, the wizard prompts for selection, credential resolution checks existing keys and environment variables before falling back to manual input, and the final step applies both the provider selection and key to produce an updated config.
Core Types
The search setup module defines several types that bridge the generic flow system with search-specific configuration. SearchProvider is a deeply-nested type extraction from the config schema, while SearchProviderSetupContribution extends the generic FlowContribution with search-specific metadata.
// src/flows/search-setup.ts — Core type definitions
export type SearchProvider = NonNullable<
NonNullable<
NonNullable<
NonNullable<OpenClawConfig["tools"]>["web"]
>["search"]
>["provider"]
>;
type SearchConfig = NonNullable<
NonNullable<
NonNullable<OpenClawConfig["tools"]>["web"]
>["search"]
>;
type MutableSearchConfig = SearchConfig & Record<string, unknown>;
The SearchProvider type uses a chain of NonNullable unwrapping to drill through the optional config nesting and extract the literal union of valid provider names. MutableSearchConfig extends the readonly config with an index signature so that provider-specific credential setters can write arbitrary keys.
// src/flows/search-setup.ts — Contribution types
export type SearchProviderSetupOption = FlowOption & {
value: SearchProvider;
};
export type SearchProviderSetupContribution = FlowContribution & {
kind: "search";
surface: "setup";
provider: PluginWebSearchProviderEntry;
option: SearchProviderSetupOption;
source: "bundled" | "runtime";
};
| Type | Purpose | Key fields |
|---|---|---|
SearchProvider | Literal union of valid search provider identifiers | String literal type extracted from config schema |
SearchConfig | The tools.web.search config section with nulls removed | provider, enabled, credential fields |
MutableSearchConfig | Writable variant allowing dynamic credential key assignment | All of SearchConfig plus Record<string, unknown> |
SearchProviderSetupOption | A selectable option in the wizard prompt | value: SearchProvider, inherited label/description |
SearchProviderSetupContribution | A fully-resolved provider contribution with metadata | kind, surface, provider, option, source |
Decision Points
| Decision | What controls it | Why it matters |
|---|---|---|
| Bundled vs. runtime provider | The source field on SearchProviderSetupContribution | Determines whether the provider is built into OpenClaw or was registered by a plugin at runtime |
| Provider ordering | resolveSearchProviderSetupContributions sorts by label | Ensures consistent, alphabetical display in the selection prompt across runs |
| Existing key detection | hasExistingKey() checks the current config for a stored credential | Avoids prompting for a key the user has already provided in a previous setup |
| Environment key fallback | hasKeyInEnv() checks RuntimeEnv for provider-specific env vars | Allows zero-prompt setup in CI/CD or containerized environments where keys are injected |
| Credential application strategy | providerEntry.setConfiguredCredentialValue presence | Some providers use a two-phase credential write (pre-config then post-config); this flag controls the path |
| Provider entry resolution | resolveSearchProviderEntry() lookup by provider name | If the provider entry is not found (e.g., plugin unloaded), applySearchKey returns config unchanged |
How It Works
Search setup operates in four distinct phases: provider discovery, selection flow, credential application, and docs search integration. Each phase is intentionally decoupled so that provider discovery can be reused by other setup flows.
Phase 1 — Provider Discovery
The resolveSearchProviderOptions function is the public entry point for discovering available providers. It delegates to resolveSearchProviderSetupContributions, which merges bundled providers shipped with OpenClaw and runtime providers registered by plugins, then sorts the merged list alphabetically by label.
// src/flows/search-setup.ts — Provider discovery
export function resolveSearchProviderOptions(
config?: OpenClawConfig,
): readonly PluginWebSearchProviderEntry[] {
return resolveSearchProviderSetupContributions(config)
.map((contribution) => contribution.provider);
}
export function resolveSearchProviderSetupContributions(
config?: OpenClawConfig,
): SearchProviderSetupContribution[] {
// 1. Collect bundled providers (source: "bundled")
// 2. Collect runtime plugin providers (source: "runtime")
// 3. Merge into a single array
// 4. Sort by contribution.option.label for stable UI ordering
// Returns: SearchProviderSetupContribution[]
}
FlowContribution pattern as other setup flows (model setup, API key setup). This means the search setup wizard can be composed with other wizards in a unified onboarding experience.
Phase 2 — Selection Flow
The runSearchSetupFlow orchestrates the full interactive wizard. It resolves available providers, renders a selection prompt via WizardPrompter, checks for existing credentials, and applies the result.
sequenceDiagram
participant User
participant Wizard as runSearchSetupFlow
participant Resolver as resolveSearchProviderOptions
participant Prompter as WizardPrompter
participant KeyCheck as hasExistingKey / hasKeyInEnv
participant Apply as applySearchKey
participant Config as OpenClawConfig
User->>Wizard: Initiate search setup
Wizard->>Resolver: resolveSearchProviderOptions(config)
Resolver-->>Wizard: PluginWebSearchProviderEntry[]
Wizard->>Prompter: Show provider selection list
Prompter-->>Wizard: Selected SearchProvider
Wizard->>KeyCheck: hasExistingKey(config, provider)?
KeyCheck-->>Wizard: false
Wizard->>KeyCheck: hasKeyInEnv(runtime, provider)?
KeyCheck-->>Wizard: false
Wizard->>Prompter: Prompt for API key
Prompter-->>Wizard: SecretInput (API key)
Wizard->>Apply: applySearchKey(config, provider, key)
Apply->>Config: Spread new search config
Apply->>Config: Set provider + enabled: true
Apply->>Config: Write credential value
Config-->>Wizard: Updated OpenClawConfig
Wizard-->>User: Setup complete
// src/flows/search-setup.ts — Main setup flow
export async function runSearchSetupFlow(
config: OpenClawConfig,
runtime: RuntimeEnv,
prompter: WizardPrompter,
opts?: SetupSearchOptions,
): Promise<OpenClawConfig> {
// 1. Resolve available providers
const providers = resolveSearchProviderOptions(config);
// 2. Show provider selection prompt
const selected = await prompter.select({
message: "Choose a search provider",
options: providers.map(p => ({ label: p.label, value: p.id })),
});
// 3. Handle credential input
// - Check hasExistingKey() first
// - Then hasKeyInEnv() for env-injected keys
// - Fall back to interactive prompt
// 4. Apply selection + key to config
return applySearchKey(config, selected, key);
}
Phase 3 — Credential Application
The applySearchKey function is the core of credential persistence. It performs an immutable config update using object spreads, handles the two-phase credential write pattern, and applies provider-specific selection config.
Resolve the provider entry — Look up the PluginWebSearchProviderEntry by provider name. If the entry is not found (e.g., a plugin was unloaded between selection and application), return the config unchanged as a safety guard.
// src/flows/search-setup.ts — applySearchKey
export function applySearchKey(
config: OpenClawConfig,
provider: SearchProvider,
key: SecretInput,
): OpenClawConfig {
const providerEntry = resolveSearchProviderEntry(config, provider);
if (!providerEntry) return config;
Build the new search config — Spread the existing search config (if any), set the provider field to the selected provider, and enable search by setting enabled: true.
const search: MutableSearchConfig = {
...config.tools?.web?.search,
provider,
enabled: true,
};
Phase-1 credential write — If the provider does not have a setConfiguredCredentialValue method, call the standard setCredentialValue to write the key into the mutable search config object before it is frozen into the next config.
if (!providerEntry.setConfiguredCredentialValue) {
providerEntry.setCredentialValue(search, key);
}
Assemble the next config — Perform an immutable deep spread to produce the new OpenClawConfig with the updated tools.web.search section. Then apply any provider-specific selection config (e.g., default model, region settings).
const nextBase: OpenClawConfig = {
...config,
tools: {
...config.tools,
web: { ...config.tools?.web, search },
},
};
const next = applySearchProviderSelectionConfig(
nextBase, providerEntry,
);
Phase-2 credential write — If the provider supports the two-phase pattern (setConfiguredCredentialValue exists), call it now with the fully-assembled config. This allows providers that need the complete config context (e.g., to derive a storage path) to write their credential correctly.
providerEntry.setConfiguredCredentialValue?.(next, key);
return next;
}
setCredentialValue / setConfiguredCredentialValue split exists because some providers need the final config object (with provider-specific selection applied) to determine where to store the credential. If your provider only needs the search config section, implement setCredentialValue alone. Only implement setConfiguredCredentialValue if you need the full OpenClawConfig context.
Provider Selection Without Credentials
The applySearchProviderSelection function handles the case where only the provider choice changes but no new credential is provided. This is used when switching between providers that share a credential or when re-selecting the current provider.
// src/flows/search-setup.ts
export function applySearchProviderSelection(
config: OpenClawConfig,
provider: SearchProvider,
): OpenClawConfig
// Applies provider selection + provider-specific config
// without touching credentials. Used by the /model command
// when changing search providers via a shortcut.
Phase 4 — Docs Search Integration
The docsSearchCommand in src/commands/docs.ts uses the configured search provider to perform documentation searches via the MCP tool interface. It joins query parts, invokes the mcporter tool with the configured search tool, and applies a timeout.
// src/commands/docs.ts — docsSearchCommand
export async function docsSearchCommand(
queryParts: string[],
runtime: RuntimeEnv,
) {
const query = queryParts.join(" ").trim();
const res = await runTool(
"mcporter",
["call", SEARCH_TOOL, "--args", payload, "--output", "text"],
{ timeoutMs: SEARCH_TIMEOUT_MS },
);
}
onboard-search.ts module re-exports key functions from search-setup.ts under cleaner names for use by the onboarding wizard:
// src/commands/onboard-search.ts — re-exports
export {
SEARCH_PROVIDER_OPTIONS,
applySearchKey,
applySearchProviderSelection,
hasExistingKey,
hasKeyInEnv,
resolveExistingKey,
resolveSearchProviderOptions,
runSearchSetupFlow as setupSearch,
} from "../flows/search-setup.js";
Edge Cases
| Scenario | Behavior | Rationale |
|---|---|---|
| Provider entry not found | applySearchKey returns config unchanged | Defensive guard against plugins being unloaded between provider selection and config write |
| No providers available | resolveSearchProviderOptions returns empty array | The wizard prompt receives an empty options list and can show a "no providers" message |
| Key already exists | hasExistingKey() returns true; wizard skips key prompt | Avoids asking for a key the user already entered, improving re-setup UX |
| Key in environment | hasKeyInEnv() returns true; uses env value | Enables zero-touch setup in CI/Docker environments with pre-injected secrets |
Provider without setConfiguredCredentialValue | Only phase-1 setCredentialValue is called | Simple providers that only need the search config section for credential storage |
| Provider with both credential setters | Phase-1 is skipped; only phase-2 setConfiguredCredentialValue runs | Providers needing full config context get the complete assembled config |
| Docs search timeout | runTool throws after SEARCH_TIMEOUT_MS | Prevents hanging on unresponsive search provider APIs |
| Empty search query | queryParts.join(" ").trim() produces empty string | Caller should validate before invoking; the MCP tool will return an error |
Configuration
| Setting | Location | Default | Description |
|---|---|---|---|
tools.web.search.provider | OpenClawConfig | none | The selected search provider identifier (e.g., "tavily", "brave") |
tools.web.search.enabled | OpenClawConfig | false | Whether web search is active; set to true by applySearchKey |
SEARCH_TOOL | src/commands/docs.ts | Constant | The MCP tool name used by docsSearchCommand for search invocations |
SEARCH_TIMEOUT_MS | src/commands/docs.ts | Constant | Timeout in milliseconds for MCP search tool calls |
| Provider-specific env vars | RuntimeEnv | Varies | Environment variables like TAVILY_API_KEY or BRAVE_API_KEY checked by hasKeyInEnv |
Related Topics
How OpenClaw discovers which providers are available based on config and runtime environment.
The parallel setup flow for switching AI model providers, using the same FlowContribution pattern.
How slash commands like /docs search are parsed before reaching docsSearchCommand.
Source Files
Key files involved in search setup
src/
├── flows/
│ └── search-setup.ts ← Core types, discovery, applySearchKey, runSearchSetupFlow
├── commands/
│ ├── docs.ts ← docsSearchCommand() MCP tool invocation
│ └── onboard-search.ts ← Re-exports for onboarding wizard
└── plugins/
└── web-search-providers/ ← Bundled provider entries (Tavily, Brave, etc.)