# Connecting Snowflake CoWork to Treasure AI CDP

Snowflake CoWork lets business users ask natural-language questions inside Snowflake. By connecting a Snowflake Cortex Agent to the Treasure AI CDP API through a Python stored procedure and an External Access Integration, business users in CoWork can inspect parent segments, child segments, and journeys directly from Snowsight—without leaving Snowflake or exposing Treasure AI API keys.

In this integration pattern, all AI reasoning and token consumption run inside Snowflake and are billed as **Snowflake AI Credits**. Treasure AI serves standard REST API calls over HTTPS, so no Treasure AI Credits are consumed.

## How the Integration Works

When a business user asks a question in Snowflake CoWork, the `TREASURE_CDP_AGENT` Cortex Agent calls the `td_cdp_lookup` custom tool. That tool runs the `TREASURE_AI.COWORK.TD_CDP_LOOKUP` Python stored procedure on a Snowflake warehouse, sends a read-only `GET` request to the Treasure AI CDP API using a secret stored in Snowflake, and returns trimmed JSON for CoWork to summarize.

| Snowflake Object  | Name  | Purpose  |
|  --- | --- | --- |
| **Warehouse** | `<YOUR_WAREHOUSE>` (for example, `TREASURE_COWORK_WH`) | Executes the stored procedure for a few seconds per question |
| **Database and schema** | `TREASURE_AI.COWORK` | Holds the network rule, secret, stored procedure, and Cortex Agent |
| **Network rule** | `TD_CDP_API_RULE` | Restricts outbound egress to your regional Treasure AI CDP API host on port `443` |
| **Secret** | `TD_API_KEY` | Stores the Treasure AI Master API key so business users never see it |
| **External Access Integration** | `TD_CDP_API_EAI` | Binds the network rule and secret so the stored procedure can call the CDP API |
| **Stored procedure** | `TD_CDP_LOOKUP` | Calls the CDP API (`list_parent_segments`, `list_segments`, `list_journeys`) and returns trimmed JSON |
| **Role** | `COWORK_USERS` | Grants business users read-only access to invoke the agent in CoWork |
| **Cortex Agent** | `TREASURE_CDP_AGENT` (`Treasure AI CDP`) | The CoWork agent exposed to business users |


## Prerequisites

- **Snowflake CoWork enabled** on your Snowflake account.
- **Snowflake roles and warehouse**: Access to `SYSADMIN` and `ACCOUNTADMIN`, plus a warehouse (such as `TREASURE_COWORK_WH`). A dedicated Extra Small (`X-SMALL`) warehouse with a 60-second auto-suspend is recommended because each lookup runs for only a few seconds.
- **Dedicated Treasure AI user**: Create a dedicated Treasure AI user scoped only to the parent segments that CoWork users should be allowed to inspect. All CoWork users query Treasure AI through this single identity.
- **Regional CDP API host**: Identify the CDP API hostname for your Treasure AI region:


| Region  | Network Rule `VALUE_LIST` Host  | Python `CDP_HOST` URL  |
|  --- | --- | --- |
| **US (AWS us-east-1)** | `api-cdp.treasuredata.com:443` | `https://api-cdp.treasuredata.com` |
| **EU (eu01)** | `api-cdp.eu01.treasuredata.com:443` | `https://api-cdp.eu01.treasuredata.com` |
| **Japan (ap01)** | `api-cdp.treasuredata.co.jp:443` | `https://api-cdp.treasuredata.co.jp` |
| **Asia Pacific (ap02)** | `api-cdp.ap02.treasuredata.com:443` | `https://api-cdp.ap02.treasuredata.com` |


## Phase 1: Configure Treasure AI API Access in Snowflake

In this phase, you generate a Treasure AI API key, store it in a Snowflake secret, and create the `TD_CDP_LOOKUP` stored procedure.

### Step 1: Generate a Treasure AI Master API Key

1. Sign in to the [Treasure AI Console](/tools/td-console/log-in-to-td-console) as the dedicated read-only user created for CoWork.
2. Navigate to **My Settings → API Keys** and select **Add API Key** (see [Getting Your API Keys](/products/my-settings/getting-your-api-keys)).
3. Select **Master Key** as the key type, save the key, and copy the value (`<account_id>/<hex_secret>`).


Scope the dedicated user and rotate the key regularly
The Treasure AI CDP API requires a Master Key rather than a write-only key to read audiences, segments, and journeys. Always issue this key under a dedicated Treasure AI user restricted to read-only access on the specific parent segments CoWork should query, store it only in a Snowflake `SECRET`, and rotate it on a regular schedule.

### Step 2: Create the Database, Network Rule, and Secret

In Snowsight, open a SQL worksheet, set your role to `SYSADMIN` and your warehouse to `<YOUR_WAREHOUSE>` (for example, `TREASURE_COWORK_WH`), and create the schema, network rule, and secret:

```sql
USE ROLE SYSADMIN;
USE WAREHOUSE <YOUR_WAREHOUSE>;

CREATE DATABASE IF NOT EXISTS TREASURE_AI;
CREATE SCHEMA IF NOT EXISTS TREASURE_AI.COWORK;
USE SCHEMA TREASURE_AI.COWORK;

-- Replace the host if your Treasure AI account is in EU, Japan, or AP02.
CREATE OR REPLACE NETWORK RULE TD_CDP_API_RULE
  MODE = EGRESS
  TYPE = HOST_PORT
  VALUE_LIST = ('api-cdp.treasuredata.com:443');

CREATE OR REPLACE SECRET TD_API_KEY
  TYPE = GENERIC_STRING
  SECRET_STRING = '<TREASURE_MASTER_API_KEY>';
```

Match the regional host in both places
If you change the host in `TD_CDP_API_RULE` for EU, Japan, or AP02, you must also set `CDP_HOST` in Step 4 to the matching HTTPS hostname.

### Step 3: Create the External Access Integration

Switch to `ACCOUNTADMIN` to create the External Access Integration and grant `USAGE` to `SYSADMIN`:

```sql
USE ROLE ACCOUNTADMIN;

CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION TD_CDP_API_EAI
  ALLOWED_NETWORK_RULES = (TREASURE_AI.COWORK.TD_CDP_API_RULE)
  ALLOWED_AUTHENTICATION_SECRETS = (TREASURE_AI.COWORK.TD_API_KEY)
  ENABLED = TRUE;

GRANT USAGE ON INTEGRATION TD_CDP_API_EAI TO ROLE SYSADMIN;

USE ROLE SYSADMIN;
USE SCHEMA TREASURE_AI.COWORK;
```

### Step 4: Create the `TD_CDP_LOOKUP` Stored Procedure

The stored procedure exposes three read-only actions against the Treasure AI CDP API:

| Action  | Input  | Treasure AI CDP API Endpoints Called  |
|  --- | --- | --- |
| `list_parent_segments` | None | `GET /audiences` |
| `list_segments` | `parent_segment_id` | `GET /audiences/{id}/segments` |
| `list_journeys` | `parent_segment_id` | `GET /audiences/{id}/folders`, then `GET /entities/journeys?folder_id=…` per folder (deduplicated by journey ID) |


Run the following SQL as `SYSADMIN` to create the stored procedure:

```sql
CREATE OR REPLACE PROCEDURE TREASURE_AI.COWORK.TD_CDP_LOOKUP(
    ACTION VARCHAR,
    PARENT_SEGMENT_ID VARCHAR DEFAULT ''
)
RETURNS STRING
LANGUAGE PYTHON
RUNTIME_VERSION = '3.11'
PACKAGES = ('snowflake-snowpark-python', 'requests')
HANDLER = 'main'
EXTERNAL_ACCESS_INTEGRATIONS = (TD_CDP_API_EAI)
SECRETS = ('td_key' = TD_API_KEY)
COMMENT = 'Read-only lookups against the Treasure AI CDP API for Snowflake CoWork'
EXECUTE AS OWNER
AS
$$
import json
import re

import _snowflake
import requests

# Must match the regional host in TD_CDP_API_RULE.
CDP_HOST = "https://api-cdp.treasuredata.com"
TIMEOUT_SECONDS = 30
MAX_ITEMS = 200


def _limit(rows):
    return rows[:MAX_ITEMS], len(rows) > MAX_ITEMS


def _get(path):
    key = _snowflake.get_generic_secret_string("td_key")
    resp = requests.get(
        CDP_HOST + path,
        headers={"Authorization": "TD1 " + key, "Accept": "application/json"},
        timeout=TIMEOUT_SECONDS,
    )
    resp.raise_for_status()
    return resp.json()


def _list_parent_segments(_parent_segment_id):
    rows = _get("/audiences")
    results = [
        {
            "parent_segment_id": str(r.get("id")),
            "name": r.get("name"),
            "description": r.get("description"),
            "population": r.get("population"),
            "updated_at": r.get("updatedAt"),
        }
        for r in rows
    ]
    return _limit(results)


def _list_segments(parent_segment_id):
    if not re.fullmatch(r"\d+", parent_segment_id):
        raise ValueError(
            "parent_segment_id must be a number. Call list_parent_segments first."
        )
    rows = _get("/audiences/" + parent_segment_id + "/segments")
    results = [
        {
            "segment_id": str(r.get("id")),
            "name": r.get("name"),
            "description": r.get("description"),
            "population": r.get("population"),
            "kind": r.get("kind"),
            "updated_at": r.get("updatedAt"),
        }
        for r in rows
    ]
    return _limit(results)


def _list_journeys(parent_segment_id):
    if not re.fullmatch(r"\d+", parent_segment_id):
        raise ValueError(
            "parent_segment_id must be a number. Call list_parent_segments first."
        )
    folders = _get("/audiences/" + parent_segment_id + "/folders")
    results = {}
    for folder in folders:
        body = _get("/entities/journeys?folder_id=" + str(folder.get("id")))
        for item in body.get("data") or []:
            a = item.get("attributes") or {}
            results[item.get("id")] = {
                "journey_id": item.get("id"),
                "name": a.get("name"),
                "state": a.get("state"),
                "paused": a.get("paused"),
                "parent_segment_id": a.get("audienceId"),
                "launched_at": a.get("launchedAt"),
                "updated_at": a.get("updatedAt"),
            }
    return _limit(list(results.values()))


ACTIONS = {
    "list_parent_segments": _list_parent_segments,
    "list_segments": _list_segments,
    "list_journeys": _list_journeys,
}

HTTP_HINTS = {
    401: "API key rejected (wrong key, wrong region, or a write-only key)",
    403: "the Treasure user behind the API key lacks permission",
    404: "not found (check the parent segment id)",
}


def main(session, action, parent_segment_id=""):
    action = (action or "").strip().lower()
    if action not in ACTIONS:
        return json.dumps({"error": "Unknown action. Use one of: " + ", ".join(ACTIONS)})
    try:
        results, truncated = ACTIONS[action]((parent_segment_id or "").strip())
        return json.dumps({
            "action": action,
            "count": len(results),
            "truncated": truncated,
            "results": results,
        })
    except requests.HTTPError as e:
        code = e.response.status_code if e.response is not None else None
        return json.dumps({"error": f"HTTP {code}: {HTTP_HINTS.get(code, 'Treasure API error')}"})
    except Exception as e:
        return json.dumps({"error": str(e)})
$$;
```

### Step 5: Test the Stored Procedure

Verify all three actions directly in your worksheet (pass a `parent_segment_id` returned by the first call into the second and third calls):

```sql
CALL TREASURE_AI.COWORK.TD_CDP_LOOKUP('list_parent_segments');
CALL TREASURE_AI.COWORK.TD_CDP_LOOKUP('list_segments', '<PARENT_SEGMENT_ID>');
CALL TREASURE_AI.COWORK.TD_CDP_LOOKUP('list_journeys', '<PARENT_SEGMENT_ID>');
```

## Phase 2: Create and Configure the Cortex Agent

Once the stored procedure returns JSON from Treasure AI, create the Cortex Agent in Agent Studio and bind `TD_CDP_LOOKUP` as its tool.

### Step 1: Create the `COWORK_USERS` Role and Grant Permissions

As `ACCOUNTADMIN`, create the role for business users and grant usage on the warehouse, database, schema, and stored procedure:

```sql
USE ROLE ACCOUNTADMIN;

CREATE ROLE IF NOT EXISTS COWORK_USERS;
GRANT ROLE COWORK_USERS TO USER <USER_1>;
GRANT ROLE COWORK_USERS TO USER <USER_2>;

GRANT USAGE ON WAREHOUSE <YOUR_WAREHOUSE> TO ROLE COWORK_USERS;
GRANT USAGE ON DATABASE TREASURE_AI TO ROLE COWORK_USERS;
GRANT USAGE ON SCHEMA TREASURE_AI.COWORK TO ROLE COWORK_USERS;
GRANT USAGE ON PROCEDURE TREASURE_AI.COWORK.TD_CDP_LOOKUP(VARCHAR, VARCHAR) TO ROLE COWORK_USERS;
```

### Step 2: Create the Agent in Snowsight Agent Studio

1. In the Snowsight navigation menu, select **AI → Agent Studio**.
2. On the **Agents** tab, select **Create agent**.
3. Enter the agent details:
  - **Database and schema**: `TREASURE_AI.COWORK`
  - **Agent object name**: `TREASURE_CDP_AGENT` (the agent's API URL is derived from this name)
  - **Display name**: `Treasure AI CDP`
4. Select **Create agent**.


### Step 3: Configure the Agent Specification via SQL

Run the following `ALTER AGENT` statement in your SQL worksheet to set the orchestration instructions, response formatting rules, and `td_cdp_lookup` tool specification in one step, then grant `USAGE` on the agent to `COWORK_USERS`:

```sql
USE ROLE SYSADMIN;
USE SCHEMA TREASURE_AI.COWORK;

ALTER AGENT TREASURE_AI.COWORK.TREASURE_CDP_AGENT MODIFY LIVE VERSION SET SPECIFICATION = $$
models:
  orchestration: auto
instructions:
  orchestration: |
    Use the td_cdp_lookup tool for any question about Treasure AI parent segments, segments, audiences or journeys.
    - To list parent segments, call it with action = list_parent_segments.
    - To list the segments inside a parent segment, first get its parent_segment_id from list_parent_segments, then call action = list_segments with that id.
    - To list journeys, first get the parent_segment_id from list_parent_segments, then call action = list_journeys with that id.
    Never guess ids or numbers. If the tool returns an error, tell the user what it says.
  response: |
    Answer briefly, using a table when there is more than one item. "population" is the number of customer profiles; show it with thousands separators. If a population is empty, say the size hasn't been calculated yet. If "truncated" is true, say that only the first 200 items are shown.
tools:
  - tool_spec:
      type: code_execution
      name: code_execution
  - tool_spec:
      type: generic
      name: td_cdp_lookup
      description: "Read-only lookups against the Treasure AI CDP API: list parent segments, segments of a parent segment, and journeys of a parent segment. Returns JSON."
      input_schema:
        type: object
        properties:
          action:
            type: string
            description: "One of: list_parent_segments, list_segments, list_journeys"
          parent_segment_id:
            type: string
            description: "Numeric parent segment id from list_parent_segments. Required for list_segments and list_journeys; leave empty for list_parent_segments."
        required:
          - action
tool_resources:
  code_execution: {}
  td_cdp_lookup:
    type: procedure
    identifier: TREASURE_AI.COWORK.TD_CDP_LOOKUP
    execution_environment:
      type: warehouse
      warehouse: <YOUR_WAREHOUSE>
$$;

GRANT USAGE ON AGENT TREASURE_AI.COWORK.TREASURE_CDP_AGENT TO ROLE COWORK_USERS;
```

On the agent's **Configuration → Tools** tab in Agent Studio, confirm that **Code Execution tool** is enabled and `TD_CDP_LOOKUP` appears under **Custom tools**:

### Step 4: Test the Agent in Preview or SQL

Test the agent either from your SQL worksheet or on the agent's **Preview** tab in Agent Studio:

```sql
SELECT SNOWFLAKE.CORTEX.DATA_AGENT_RUN(
  'TREASURE_AI.COWORK.TREASURE_CDP_AGENT',
  $${"messages":[{"role":"user","content":[{"type":"text","text":"List my parent segments"}]}]}$$
) AS r;
```

On the **Preview** tab, ask *"List a couple of parent segments"* and verify that the table displays parent segment IDs and formatted populations:

## Phase 3: Publish the Agent to Snowflake CoWork

### Step 1: Publish the Agent Version

Edits to an agent are saved as a draft. When the agent header shows **Draft** or **Unpublished changes**:

1. Select **Publish** in the upper-right corner of the agent page.
2. Keep **Use this version** checked so live traffic routes to the new version, optionally add a version note, and select **Publish**.


### Step 2: Register the Agent with Snowflake Intelligence (CoWork)

As `ACCOUNTADMIN`, add `TREASURE_CDP_AGENT` to your account's Snowflake Intelligence object and grant `USAGE` to `COWORK_USERS` (run `SHOW SNOWFLAKE INTELLIGENCES;` first if you already have a Snowflake Intelligence object):

```sql
USE ROLE ACCOUNTADMIN;

CREATE SNOWFLAKE INTELLIGENCE IF NOT EXISTS SNOWFLAKE_INTELLIGENCE_OBJECT_DEFAULT;

ALTER SNOWFLAKE INTELLIGENCE SNOWFLAKE_INTELLIGENCE_OBJECT_DEFAULT
  ADD AGENT TREASURE_AI.COWORK.TREASURE_CDP_AGENT;

GRANT USAGE ON SNOWFLAKE INTELLIGENCE SNOWFLAKE_INTELLIGENCE_OBJECT_DEFAULT TO ROLE COWORK_USERS;
```

Once registered, the agent header displays **Added to Snowflake CoWork**:

### Step 3: Query Treasure AI from Snowflake CoWork

Business users can now open [ai.snowflake.com](https://ai.snowflake.com) (or select **AI → Snowflake CoWork** in Snowsight), confirm their active role is `COWORK_USERS`, select **Treasure AI CDP** in the chat input, and ask questions about audiences, segments, and journeys:

## Security, Costs, and Limits

- **Billing and credit usage**: CoWork questions consume **Snowflake AI Credits**, and the `TD_CDP_LOOKUP` procedure runs for a few seconds on `<YOUR_WAREHOUSE>`. Treasure AI serves standard REST API calls, so no Treasure AI Credits are used. You can audit CoWork usage in Snowflake with:

```sql
SELECT * FROM SNOWFLAKE.ACCOUNT_USAGE.SNOWFLAKE_COWORK_USAGE_HISTORY ORDER BY START_TIME DESC;
```
- **Credential isolation**: The Treasure AI Master API key is stored inside `TREASURE_AI.COWORK.TD_API_KEY`. Because the procedure runs as `EXECUTE AS OWNER`, business users with the `COWORK_USERS` role cannot read the secret. To rotate the key, run:

```sql
ALTER SECRET TREASURE_AI.COWORK.TD_API_KEY SET SECRET_STRING = '<NEW_KEY>';
```
- **Read-only metadata scope**: The stored procedure only sends `GET` requests to fixed CDP endpoints (`/audiences`, `/audiences/{id}/segments`, `/audiences/{id}/folders`, and `/entities/journeys`). It retrieves entity metadata (ID, name, population, state, and timestamps) and cannot read individual customer PII, modify segment rules, or launch journeys.
- **200-item safeguard**: `MAX_ITEMS = 200` caps each returned list to keep Cortex Agent context-window usage and latency predictable. When more than 200 items exist, the procedure sets `truncated: true` and the agent notifies the user that only the first 200 items are shown.


## Troubleshooting

| Symptom  | Likely Cause  | Fix  |
|  --- | --- | --- |
| `HTTP 401: API key rejected` | Invalid API key, mismatched regional host, or a write-only API key was used | Issue a **Master Key** in the [Treasure AI Console](/tools/td-console/log-in-to-td-console) (see [Getting Your API Keys](/products/my-settings/getting-your-api-keys)) and verify that `TD_CDP_API_RULE` and `CDP_HOST` both use your region's CDP API host |
| `HTTP 403: the Treasure user behind the API key lacks permission` | The dedicated Treasure AI user does not have permission to view the parent segments | Grant the dedicated Treasure AI user view access to the required parent segments |
| Connection or network error when calling `TD_CDP_LOOKUP` | The hostname in `CDP_HOST` is not listed in `TD_CDP_API_RULE` | Ensure the host in `TD_CDP_API_RULE` and `CDP_HOST` match, then re-run Phase 1 Steps 2–4 |
| Error about Anaconda or Python package terms | Snowflake Python third-party packages are not enabled on the account | Have an `ORGADMIN` accept the Anaconda terms under **Admin → Billing & Terms** in Snowsight |
| Users see `insufficient privileges` or cannot find the agent in CoWork | Missing role grants or the agent has not been added to the Snowflake Intelligence object | Verify the user's active role is `COWORK_USERS` and re-run the `GRANT USAGE` statements in Phase 2 Step 1, Phase 2 Step 3, and Phase 3 Step 2 |
| Recent instruction or tool changes do not appear in CoWork | The agent has unpublished draft changes | Open the agent in Agent Studio and select **Publish** (Phase 3 Step 1) |


## Next Steps

- [Snowflake Cortex Code](/getting-started/ai/snowflake-cortex-code) — connect the Snowflake Cortex Code CLI to `tdx mcp` or a Treasure AI Studio MCP Endpoint
- [Databricks Genie Code](/getting-started/ai/databricks-genie-code) — connect the Databricks Genie Code CLI to Treasure AI
- [Composable Audience Studio on Snowflake](/products/customer-data-platform/composable-cdp/audience-studio/snowflake) — build segments directly on Snowflake tables with Zero-Copy
- [Open Catalog Access for Snowflake](/products/customer-data-platform/open-catalog-access/snowflake-integration) — query CDP tables in Apache Iceberg format from Snowflake