> SpecStream is the live web data engine for teams and agents. This is the Markdown copy of https://specstream.ai/docs/integrations/snowflake-cortex-agents. Every page, with what SpecStream does and costs: https://specstream.ai/llms.txt

# Snowflake Cortex Agents

Give a Snowflake Cortex Agent web search and page reading as two custom tools. One SQL script creates the network rule, the secret and the External Access Integration, two Python functions that call SpecStream's Search API and Extract API, and the agent that uses them.

[SpecStream for Snowflake Cortex AgentsAgent frameworks & builders](https://specstream.ai/integrations/snowflake-cortex-agents.md)

## What you need

-   A Snowflake account where you can use the `ACCOUNTADMIN` role. Creating an External Access Integration needs the `CREATE INTEGRATION` privilege on the account.
-   A SpecStream key. [Get your key](https://specstream.ai/workspace/search?utm_source=snowflake&utm_medium=howto) on the Search API screen of your SpecStream workspace. New accounts start with $10 of credit, no card.

1.  1

    ## 1. Create the role, database and warehouse

    In a Snowsight worksheet, run this as `ACCOUNTADMIN`. The role is the one your agent's users get; the warehouse runs the two functions.

    Snowflake worksheet

    ```sql
    USE ROLE ACCOUNTADMIN;
    
    CREATE ROLE IF NOT EXISTS specstream_agent_role;
    GRANT DATABASE ROLE SNOWFLAKE.CORTEX_USER TO ROLE specstream_agent_role;
    SET my_user = CURRENT_USER();
    GRANT ROLE specstream_agent_role TO USER IDENTIFIER($my_user);
    
    CREATE DATABASE IF NOT EXISTS specstream;
    CREATE SCHEMA IF NOT EXISTS specstream.tools;
    GRANT USAGE ON DATABASE specstream TO ROLE specstream_agent_role;
    GRANT USAGE ON SCHEMA specstream.tools TO ROLE specstream_agent_role;
    GRANT CREATE AGENT ON SCHEMA specstream.tools TO ROLE specstream_agent_role;
    
    CREATE WAREHOUSE IF NOT EXISTS specstream_agent_wh
      WAREHOUSE_SIZE = 'XSMALL'
      AUTO_SUSPEND = 60
      AUTO_RESUME = TRUE
      INITIALLY_SUSPENDED = TRUE;
    GRANT USAGE ON WAREHOUSE specstream_agent_wh TO ROLE specstream_agent_role;
    ```

2.  2

    ## 2. Allow calls to api.specstream.ai

    Create a network rule for SpecStream's host, a secret that holds your key, and the External Access Integration that binds them. Put your key in place of `YOUR_SPECSTREAM_KEY`. Snowflake stores it encrypted, and no query prints it.

    Snowflake worksheet

    ```sql
    CREATE OR REPLACE NETWORK RULE specstream.tools.specstream_api_rule
      MODE = EGRESS
      TYPE = HOST_PORT
      VALUE_LIST = ('api.specstream.ai:443');
    
    CREATE OR REPLACE SECRET specstream.tools.specstream_api_key
      TYPE = GENERIC_STRING
      SECRET_STRING = 'YOUR_SPECSTREAM_KEY';
    
    CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION specstream_api_access
      ALLOWED_NETWORK_RULES = (specstream.tools.specstream_api_rule)
      ALLOWED_AUTHENTICATION_SECRETS = (specstream.tools.specstream_api_key)
      ENABLED = TRUE;
    ```

3.  3

    ## 3. Create the web\_search function

    A Python function that posts the query to `POST /v1/web-search` and returns the reply's `data` as a `VARIANT`. With `speed` set to `fast`, it runs the fast search, which returns results only.

    Snowflake worksheet

    ```sql
    CREATE OR REPLACE FUNCTION specstream.tools.web_search(
      query STRING,
      num_results INTEGER DEFAULT 10,
      speed STRING DEFAULT 'default'
    )
    RETURNS VARIANT
    LANGUAGE PYTHON
    RUNTIME_VERSION = 3.12
    PACKAGES = ('snowflake-snowpark-python', 'requests')
    HANDLER = 'run'
    EXTERNAL_ACCESS_INTEGRATIONS = (specstream_api_access)
    SECRETS = ('cred' = specstream.tools.specstream_api_key)
    AS
    $$
    import _snowflake
    import requests
    
    session = requests.Session()
    
    def run(query, num_results, speed):
        body = {"q": query, "num": num_results or 10}
        if speed == "fast":
            body["speed"] = "fast"
        reply = session.post(
            "https://api.specstream.ai/v1/web-search",
            headers={"Authorization": "Bearer " + _snowflake.get_generic_secret_string("cred")},
            json=body,
            timeout=60,
        )
        data = reply.json()
        if reply.status_code != 200:
            return {"status": reply.status_code, "error": data.get("error")}
        return data["data"]
    $$;
    
    GRANT USAGE ON FUNCTION specstream.tools.web_search(STRING, INTEGER, STRING)
      TO ROLE specstream_agent_role;
    ```

4.  4

    ## 4. Create the read\_pages function

    The same shape for the Extract API. It takes up to 20 URLs as one comma separated string and returns the main text of each page or document as Markdown.

    Snowflake worksheet

    ```sql
    CREATE OR REPLACE FUNCTION specstream.tools.read_pages(urls STRING)
    RETURNS VARIANT
    LANGUAGE PYTHON
    RUNTIME_VERSION = 3.12
    PACKAGES = ('snowflake-snowpark-python', 'requests')
    HANDLER = 'run'
    EXTERNAL_ACCESS_INTEGRATIONS = (specstream_api_access)
    SECRETS = ('cred' = specstream.tools.specstream_api_key)
    AS
    $$
    import _snowflake
    import requests
    
    session = requests.Session()
    
    def run(urls):
        pages = [u.strip() for u in (urls or "").split(",") if u.strip()]
        reply = session.post(
            "https://api.specstream.ai/v1/extract",
            headers={"Authorization": "Bearer " + _snowflake.get_generic_secret_string("cred")},
            json={"urls": pages[:20], "format": "markdown"},
            timeout=90,
        )
        data = reply.json()
        if reply.status_code != 200:
            return {"status": reply.status_code, "error": data.get("error")}
        return data["data"]
    $$;
    
    GRANT USAGE ON FUNCTION specstream.tools.read_pages(STRING)
      TO ROLE specstream_agent_role;
    ```

5.  5

    ## 5. Try both from SQL

    Both functions are scalar and return `VARIANT`, so you read the reply with `:field` paths and `FLATTEN`, in any `SELECT`:

    Snowflake worksheet

    ```sql
    USE WAREHOUSE specstream_agent_wh;
    
    SELECT specstream.tools.web_search('Who is the CEO of Snowflake?'):answer:value::STRING AS answer;
    
    SELECT s.value:url::STRING AS url,
           s.value:title::STRING AS title,
           s.value:published::STRING AS published
    FROM TABLE(FLATTEN(input => specstream.tools.web_search('Snowflake Cortex Agents release notes', 10, 'fast'):sources)) s;
    
    SELECT p.value:url::STRING AS url,
           p.value:status::STRING AS status,
           LEFT(p.value:text::STRING, 500) AS text
    FROM TABLE(FLATTEN(input => specstream.tools.read_pages('https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-agents'):results)) p;
    ```

6.  6

    ## 6. Create the agent

    Register both functions as custom tools. Each `tool_spec` of type `generic` gives the agent the tool's inputs, and `tool_resources` maps it to its function and the warehouse that runs it. The specification is YAML; this one is written as JSON, which YAML reads as it is.

    Snowflake worksheet

    ```sql
    USE ROLE specstream_agent_role;
    USE WAREHOUSE specstream_agent_wh;
    
    CREATE OR REPLACE AGENT specstream.tools.web_research_agent
      COMMENT = 'Web search and page reading through SpecStream'
      FROM SPECIFICATION
    $$
    {
      "models": {"orchestration": "auto"},
      "orchestration": {"budget": {"seconds": 120, "tokens": 32000}},
      "instructions": {
        "orchestration": "Use web_search for questions about the public web or anything recent. Use read_pages to read the full text of URLs the user gives or that web_search returned. Set speed to fast when a list of links is enough.",
        "response": "Give the URL of every page an answer comes from."
      },
      "tools": [
        {"tool_spec": {
          "type": "generic",
          "name": "web_search",
          "description": "Search the live web through SpecStream. Returns an answer with the page it was read from, and the results with url, title, site, snippet and published date.",
          "input_schema": {
            "type": "object",
            "properties": {
              "query": {"type": "string", "description": "The question or search terms, in any language. site: and quoted phrases work."},
              "num_results": {"type": "integer", "description": "Number of results, 1 to 100. Default 10."},
              "speed": {"type": "string", "description": "fast for results only, at a fifth of the price. Leave it out for an answer."}
            },
            "required": ["query"]
          }
        }},
        {"tool_spec": {
          "type": "generic",
          "name": "read_pages",
          "description": "Read the main text of up to 20 web pages or documents through SpecStream.",
          "input_schema": {
            "type": "object",
            "properties": {
              "urls": {"type": "string", "description": "Comma separated URLs, up to 20."}
            },
            "required": ["urls"]
          }
        }}
      ],
      "tool_resources": {
        "web_search": {"type": "function", "identifier": "SPECSTREAM.TOOLS.WEB_SEARCH", "execution_environment": {"type": "warehouse", "warehouse": "SPECSTREAM_AGENT_WH"}},
        "read_pages": {"type": "function", "identifier": "SPECSTREAM.TOOLS.READ_PAGES", "execution_environment": {"type": "warehouse", "warehouse": "SPECSTREAM_AGENT_WH"}}
      }
    }
    $$;
    ```

7.  7

    ## 7. Ask the agent

    In Snowsight, open **AI & ML**, then **Agents**, and select **WEB\_RESEARCH\_AGENT**. On the agent details page, enter a question in the agent playground:

    Agent playground

    ```text
    What did Snowflake announce in its latest Cortex Agents release notes? Read the page and list each change with its link.
    ```

## Why a search tool of your own when Cortex has web search

Cortex Agents have a built-in **Web search** tool. Snowflake's docs say it uses the Brave Web Search API, and an `ACCOUNTADMIN` turns it on for the whole account before any agent can use it. The agent gets results to blend into its answer.

The SpecStream functions are tools you own. A reply carries an `answer` with the page it was read from and the time it was read, the results with their published dates, and the text of whole pages and PDFs from the Extract API. They run under your own key, with its own spending limit and request log, and the same functions work in any `SELECT`, so a table gets the same search the agent uses. The Search API scores 97.9 F-score on all 1,000 SimpleQA Verified questions and 64.0 item F1 on all 200 WideSearch tasks. Scores and method are on the [benchmarks page](https://specstream.ai/benchmarks.md).

## What comes back

-   `web_search` returns the Search API's `data`: `answer` for a question with one answer (`value`, the `page` it was read from, the `sources` that print it and `seen_at`), `rows` for a request for many results, and `sources`, the results in rank order with `url`, `title`, `site`, `snippet` and `published` when the page states it.
-   With `speed` set to `fast`, the call returns `sources` only, with no `answer` and no `rows`.
-   `read_pages` returns `results`, one for each URL in the order sent, each with `url`, `status` (`ok`, `blocked`, `not_found`, `empty`, `unsupported`, `invalid_url` or `failed`), `text`, `title`, `published` and `read_from`.
-   Both replies carry `request_id` and `price_usd`, what the call cost.
-   A refused call returns `status` and `error` in place of a Python error, so the agent reads why it stopped.

## What each call costs

More results cost in the same proportion for each started block of 10. Calls come out of your SpecStream balance with no quote and no approval, and every call is in your account's request log. Snowflake bills the warehouse that runs the functions, as it bills any warehouse.

| Call | Price |
| --- | --- |
| `web_search` | $5 per 1,000 searches of up to 10 results |
| `web_search` with `speed` `fast` | $1 per 1,000 searches of up to 10 results |
| `read_pages` | $0.80 per 1,000 pages. A page with no text is not charged. |

## Keep the key and the spend in bounds

The role gets `USAGE` on the two functions, not on the integration or the secret, so its users can search and read pages but cannot read the key or reach another host. Snowflake asks the same of every custom tool: users need `USAGE` on its function or procedure.

Give Snowflake a key of its own with a spending limit, made in [Agents & API](https://specstream.ai/workspace/agents?tab=keys&utm_source=snowflake). With `PATCH https://api.specstream.ai/v1/account/keys/{id}`, set `zero_retention` to `true` and no search made with the key keeps its request text or its answer after the reply, or set `eu_egress_only` to `true` and its searches leave only from SpecStream's server in Madrid.

To change the key, run `ALTER SECRET specstream.tools.specstream_api_key SET SECRET_STRING = 'NEW_KEY';`. The functions read it on their next call.

## Limits and errors

-   Custom tools take no parameter of type `object`, so `read_pages` takes its URLs as one comma separated string, and reads the first 20.
-   `status` 401: the key in the secret was refused. Check it and run `ALTER SECRET`.
-   `status` 402: the SpecStream balance is empty. `status` 403: the key has no `finds:spend` scope or is over its limit.
-   `status` 422: the query is empty or over 2,000 characters.
-   `status` 503 with `Retry-After`: the account used its calls for the minute, or search is busy. Nothing is charged, and the agent can call again.
-   A Python error that names `api.specstream.ai`: the network rule does not allow the host. Check it with `SHOW NETWORK RULES` and `DESCRIBE EXTERNAL ACCESS INTEGRATION specstream_api_access`.

## Every match as rows

When the job is a list where every row must meet your conditions, with a source URL and read time on every value, use [Deep Search](https://specstream.ai/deep-search.md). It reads the request, quotes the most it can cost, and delivers the rows after you approve, through the [REST API](https://specstream.ai/docs/api.md) or the [MCP server](https://specstream.ai/docs/mcp.md).

## Go back

Run `DROP AGENT specstream.tools.web_research_agent;`, then `DROP FUNCTION` for `specstream.tools.web_search(STRING, INTEGER, STRING)` and `specstream.tools.read_pages(STRING)`, and `DROP INTEGRATION specstream_api_access;`. Questions: [support@specstream.ai](mailto:support@specstream.ai).

## More

[SpecStream for Snowflake Cortex Agents](https://specstream.ai/integrations/snowflake-cortex-agents.md) covers what it does in Snowflake Cortex Agents on one page. Every other tool has its guide in [Connect your tools](https://specstream.ai/docs/integrations.md). You see the most a request can cost before it starts, and the matches are paid from your balance. [Billing](https://specstream.ai/docs/billing.md) has the details. For help, email [support@specstream.ai](mailto:support@specstream.ai).
