Use GroundRoute in Langflow

Add multi-engine web search to any Langflow agent. Two ways: paste the GroundRoute Search custom component, or connect the hosted GroundRoute MCP server. Both take about two minutes.

Custom component (recommended)

The GroundRoute Search component routes each query across six engines (Serper, Brave, Exa, Tavily, Firecrawl, Perplexity) to the cheapest one that clears a quality bar, and caches repeats so you don't pay for the same search twice.

1. Copy the component code

"""GroundRoute Search: Langflow custom component (Phase 1, paste-able).

Paste into Langflow as a custom component. No SDK required.
"""

from __future__ import annotations

from typing import Any

import httpx
from lfx.custom.custom_component.component import Component
from lfx.inputs.inputs import (
    DropdownInput,
    IntInput,
    MessageTextInput,
    SecretStrInput,
)
from lfx.log.logger import logger
from lfx.schema.data import Data
from lfx.schema.dataframe import DataFrame
from lfx.template.field.base import Output

_BASE_URL = "https://api.groundroute.ai"
_MODES = ["auto", "web", "news", "academic", "answer", "page"]
_FRESHNESS = ["", "fresh", "semi", "static"]
_TIMEOUT_S = 30.0


class GroundRouteSearchComponent(Component):
    display_name = "GroundRoute Search"
    description = (
        "One API across 6 search engines (Serper, Brave, Exa, Tavily, Firecrawl, Perplexity). "
        "Routes each query to the cheapest engine that clears a quality bar and caches repeats, "
        "so you pay no more than going direct."
    )
    documentation = "https://groundroute.ai/docs"
    # Uses a Langflow-bundled icon for the custom-component path (Phase 2 ships the GroundRoute glyph).
    icon = "Search"
    name = "GroundRouteSearch"

    inputs = [
        SecretStrInput(
            name="api_key",
            display_name="GroundRoute API Key",
            required=True,
            info="Get one at https://groundroute.ai/keys?utm_source=langflow",
        ),
        MessageTextInput(
            name="query",
            display_name="Search Query",
            info="What to search for.",
            tool_mode=True,
        ),
        DropdownInput(
            name="mode",
            display_name="Mode",
            options=_MODES,
            value="auto",
            advanced=True,
            info="auto lets GroundRoute classify the query and pick the engine class.",
        ),
        IntInput(
            name="max_results",
            display_name="Max Results",
            value=10,
            advanced=True,
            info="Number of results to return (clamped to 1-50).",
        ),
        DropdownInput(
            name="freshness",
            display_name="Freshness",
            options=_FRESHNESS,
            value="",
            advanced=True,
            info="Override freshness intent. Blank lets GroundRoute auto-detect.",
        ),
        MessageTextInput(
            name="domains",
            display_name="Include Domains",
            advanced=True,
            info="Comma-separated domains to restrict the search to.",
        ),
        MessageTextInput(
            name="lang",
            display_name="Language",
            advanced=True,
            info="ISO 639-1 language code, e.g. en.",
        ),
        MessageTextInput(
            name="country",
            display_name="Country",
            advanced=True,
            info="ISO 3166-1 alpha-2 country code, e.g. us.",
        ),
    ]

    outputs = [
        Output(display_name="Data", name="data", method="search_data"),
        Output(display_name="DataFrame", name="dataframe", method="search_dataframe"),
    ]

    def _build_payload(self) -> dict[str, Any]:
        try:
            max_results = int(self.max_results or 10)
        except (TypeError, ValueError):
            max_results = 10
        max_results = max(1, min(max_results, 50))

        body: dict[str, Any] = {"query": (self.query or "").strip(), "max_results": max_results}
        if self.mode and self.mode != "auto":
            body["mode"] = self.mode
        if getattr(self, "freshness", ""):
            body["freshness"] = self.freshness
        domains = getattr(self, "domains", "") or ""
        parsed = [d.strip() for d in domains.split(",") if d.strip()]
        if parsed:
            body["domains"] = parsed
        if getattr(self, "lang", ""):
            body["lang"] = self.lang
        if getattr(self, "country", ""):
            body["country"] = self.country
        return body

    @staticmethod
    def _to_records(resp: dict[str, Any]) -> list[dict[str, Any]]:
        records: list[dict[str, Any]] = []
        for r in resp.get("results", []) or []:
            records.append({
                "url": r.get("url", ""),
                "title": r.get("title", ""),
                "snippet": r.get("snippet", ""),
                "content": r.get("content"),
                "source_engine": r.get("source_engine", ""),
                "published_at": r.get("published_at"),
            })
        return records

    @staticmethod
    def _meta(resp: dict[str, Any]) -> dict[str, Any]:
        cache_meta = resp.get("cache_meta") or {}
        usage_meta = resp.get("usage_meta") or {}
        return {
            "request_id": resp.get("request_id"),
            "cache_tier": cache_meta.get("cache_tier"),
            "degraded": resp.get("degraded", False),
            "cost_usd": usage_meta.get("cost_usd"),
        }

    def _error(self, message: str) -> list[Data]:
        logger.error(message)
        self.status = message
        return [Data(data={"error": message})]

    def search_data(self) -> list[Data]:
        if not self.api_key:
            return self._error(
                "GroundRoute API key is required. Get one at https://groundroute.ai/keys?utm_source=langflow"
            )
        try:
            response = httpx.post(
                f"{_BASE_URL}/v1/search",
                json=self._build_payload(),
                headers={"Authorization": f"Bearer {self.api_key}"},
                timeout=_TIMEOUT_S,
            )
        except httpx.HTTPError as exc:
            return self._error(f"GroundRoute request failed: {exc}")
        if response.status_code != 200:
            return self._error(
                f"GroundRoute returned HTTP {response.status_code}: {response.text[:300]}"
            )
        try:
            resp: dict[str, Any] = response.json()
        except ValueError as exc:
            return self._error(f"GroundRoute returned a non-JSON body: {exc}")

        records = self._to_records(resp)
        meta = self._meta(resp)
        data_list = [Data(data=rec) for rec in records]

        if resp.get("answer"):
            data_list.insert(
                0,
                Data(data={"answer": resp["answer"], "citations": resp.get("citations", []), **meta}),
            )

        engines = sorted({rec["source_engine"] for rec in records if rec.get("source_engine")})
        suffix = " (degraded)" if meta["degraded"] else ""
        self.status = f"{len(records)} results via {', '.join(engines) or 'cache'}{suffix}"
        return data_list or [Data(data={"warning": "no results", **meta})]

    def search_dataframe(self) -> DataFrame:
        return DataFrame(self.search_data())

2. Paste into Langflow

  1. Open Langflow and go to Custom Components.
  2. Click New Custom Component, paste the code above, and click Save.
  3. The component appears in your component library as GroundRoute Search.

3. Add your API key

Set GROUNDROUTE_API_KEY in your Langflow environment, or paste it directly into the GroundRoute API Key field on the component. Get a key at groundroute.ai/keys?utm_source=langflow. Starting credit included, no card required.

4. Wire it into a flow

Drag the GroundRoute Search component onto the canvas. Connect a Chat Input node to the query input. The component returns a Data output (one record per result) and a DataFrame output. Connect either to your prompt or LLM node.

The source_engine field in each result shows which engine served it, so routing is visible in the flow output.

<!-- GIF slot: 60-second walkthrough recorded post-deploy -->

MCP

Add https://api.groundroute.ai/mcp as an MCP server in Langflow's MCP Tools component, with the header Authorization: Bearer gr_YOUR_KEY.

Setup instructions for Langflow's MCP Tools UI will be verified and expanded here during post-deploy testing. The MCP endpoint itself is live and has been verified with standard MCP clients.

Full MCP reference: MCP server.

Starter flows

In Langflow, open the Store and search GroundRoute. You will find two items:

  • GroundRoute Search: the custom component, ready to paste or clone into any flow.
  • Multi-engine Research Agent: a complete research agent pre-wired with GroundRoute Search. Clone it, add your key, and run.

The research flow ships without an LLM provider selected (standard for Langflow templates). After cloning, open the Agent / Language Model node, pick your provider (OpenAI, Anthropic, or any supported option), and add that provider's API key.

Troubleshooting

"API key is required" error: the GROUNDROUTE_API_KEY environment variable is not set, or the key field on the component is empty. Paste your key directly into the component field.

HTTP 401: your key is invalid or has been revoked. Check groundroute.ai/keys?utm_source=langflow to verify it is active.

Default icon shown on the component: expected for pasted custom components. Langflow shows a default icon until the component is installed via the Store. This does not affect functionality.

Timeout errors: the default timeout is 30 seconds. For slow queries, increase _TIMEOUT_S at the top of the component code.


Get a key and starting credit at groundroute.ai/keys?utm_source=langflow. No card required.