Skip to the page

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

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 on the Search API screen of your SpecStream workspace. New accounts start with $10 of credit, no card.
  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
    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. 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
    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. 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
    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. 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
    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. 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
    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. 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
    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. 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
    What did Snowflake announce in its latest Cortex Agents release notes? Read the page and list each change with its link.

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.

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.

CallPrice
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. 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. It reads the request, quotes the most it can cost, and delivers the rows after you approve, through the REST API or the MCP server.

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: [email protected].

More

SpecStream for Snowflake Cortex Agents covers what it does in Snowflake Cortex Agents on one page. Every other tool has its guide in Connect your tools. You see the most a request can cost before it starts, and the matches are paid from your balance. Billing has the details. For help, email [email protected].