Search Setup

Focus Deep-Dive

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

Primary role

Discover available search providers, present a selection UI, collect credentials, and persist the chosen provider into OpenClawConfig.

Main inputs

Current OpenClawConfig, RuntimeEnv for environment variable checks, WizardPrompter for interactive prompts, and optional SetupSearchOptions.

Key decisions

Which providers are available? Does the user already have a key? Should the key come from env or manual input? Which provider is selected?

Outputs / side effects

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";
};
TypePurposeKey fields
SearchProviderLiteral union of valid search provider identifiersString literal type extracted from config schema
SearchConfigThe tools.web.search config section with nulls removedprovider, enabled, credential fields
MutableSearchConfigWritable variant allowing dynamic credential key assignmentAll of SearchConfig plus Record<string, unknown>
SearchProviderSetupOptionA selectable option in the wizard promptvalue: SearchProvider, inherited label/description
SearchProviderSetupContributionA fully-resolved provider contribution with metadatakind, surface, provider, option, source

Decision Points

DecisionWhat controls itWhy it matters
Bundled vs. runtime providerThe source field on SearchProviderSetupContributionDetermines whether the provider is built into OpenClaw or was registered by a plugin at runtime
Provider orderingresolveSearchProviderSetupContributions sorts by labelEnsures consistent, alphabetical display in the selection prompt across runs
Existing key detectionhasExistingKey() checks the current config for a stored credentialAvoids prompting for a key the user has already provided in a previous setup
Environment key fallbackhasKeyInEnv() checks RuntimeEnv for provider-specific env varsAllows zero-prompt setup in CI/CD or containerized environments where keys are injected
Credential application strategyproviderEntry.setConfiguredCredentialValue presenceSome providers use a two-phase credential write (pre-config then post-config); this flag controls the path
Provider entry resolutionresolveSearchProviderEntry() lookup by provider nameIf 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[]
}
🔎 Design note: The contribution model uses the same 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;
}
⚠ Two-phase credential pattern: The dual 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 },
  );
}
✅ Re-export pattern: The 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

ScenarioBehaviorRationale
Provider entry not foundapplySearchKey returns config unchangedDefensive guard against plugins being unloaded between provider selection and config write
No providers availableresolveSearchProviderOptions returns empty arrayThe wizard prompt receives an empty options list and can show a "no providers" message
Key already existshasExistingKey() returns true; wizard skips key promptAvoids asking for a key the user already entered, improving re-setup UX
Key in environmenthasKeyInEnv() returns true; uses env valueEnables zero-touch setup in CI/Docker environments with pre-injected secrets
Provider without setConfiguredCredentialValueOnly phase-1 setCredentialValue is calledSimple providers that only need the search config section for credential storage
Provider with both credential settersPhase-1 is skipped; only phase-2 setConfiguredCredentialValue runsProviders needing full config context get the complete assembled config
Docs search timeoutrunTool throws after SEARCH_TIMEOUT_MSPrevents hanging on unresponsive search provider APIs
Empty search queryqueryParts.join(" ").trim() produces empty stringCaller should validate before invoking; the MCP tool will return an error

Configuration

SettingLocationDefaultDescription
tools.web.search.providerOpenClawConfignoneThe selected search provider identifier (e.g., "tavily", "brave")
tools.web.search.enabledOpenClawConfigfalseWhether web search is active; set to true by applySearchKey
SEARCH_TOOLsrc/commands/docs.tsConstantThe MCP tool name used by docsSearchCommand for search invocations
SEARCH_TIMEOUT_MSsrc/commands/docs.tsConstantTimeout in milliseconds for MCP search tool calls
Provider-specific env varsRuntimeEnvVariesEnvironment variables like TAVILY_API_KEY or BRAVE_API_KEY checked by hasKeyInEnv
Provider Detection

How OpenClaw discovers which providers are available based on config and runtime environment.

Model Switching

The parallel setup flow for switching AI model providers, using the same FlowContribution pattern.

Command Parsing

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