Skip to main content

Connect Snowflake

Tasks query Snowflake through qbash.integrations.snowflake with a credential you save on the Connectors page. The connector sends each statement to the Snowflake SQL API over HTTPS. That API accepts no password, and Snowflake is phasing out password sign-in for non-human users through 2026, so the credential stores an RSA key pair or a programmatic access token.

After you connect Snowflake, you can add its tools to a qbash MCP server for ChatGPT, Claude and other MCP clients, or call it from a Python task. The Snowflake MCP server page lists what the connector can do.

Every statement runs with the privileges of the credential's role. A role granted only SELECT cannot change data, whatever SQL a task sends.

Find your account identifier​

The account identifier has the form orgname-account_name. To print yours, run this in a Snowsight SQL worksheet:

SELECT CURRENT_ORGANIZATION_NAME() || '-' || CURRENT_ACCOUNT_NAME();

Snowsight also shows it when you open the account selector and select View account details. There it is written ORGNAME.ACCOUNT_NAME, which qbash accepts. qbash also takes the account URL or a legacy locator with its region, such as xy12345.us-east-2.aws.

qbash connects over the public internet, so a PrivateLink address cannot be used. Accounts on snowflakecomputing.cn are not supported.

Create a role and a service user​

Run these statements in a worksheet with the ACCOUNTADMIN role. Replace reporting_wh with your warehouse and analytics.fx with the database and schema your tasks read.

CREATE ROLE qbash_reader;
GRANT USAGE ON WAREHOUSE reporting_wh TO ROLE qbash_reader;
GRANT USAGE ON DATABASE analytics TO ROLE qbash_reader;
GRANT USAGE ON SCHEMA analytics.fx TO ROLE qbash_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics.fx TO ROLE qbash_reader;

CREATE USER qbash_service
TYPE = SERVICE
DEFAULT_ROLE = qbash_reader
DEFAULT_WAREHOUSE = reporting_wh;
GRANT ROLE qbash_reader TO USER qbash_service;

TYPE = SERVICE marks the user as a program, so Snowflake refuses password sign-in for it.

ALL TABLES covers the tables in the schema today. For tables created later, also run GRANT SELECT ON FUTURE TABLES IN SCHEMA analytics.fx TO ROLE qbash_reader;. Grant SELECT ON ALL VIEWS the same way if a task reads views. If a task calls insert_rows, grant INSERT on the table it writes, as in GRANT INSERT ON TABLE analytics.fx.daily_rates TO ROLE qbash_reader;.

Admit qbash with a network policy​

By default, Snowflake accepts a programmatic access token only from a user that a network policy covers. Key pair sign-in needs a policy only when the account or user already has one, and then that policy must list qbash's addresses.

A network rule belongs to a schema, so give it a full name in a schema you administer. This example uses analytics.public.

CREATE NETWORK RULE analytics.public.qbash_egress
MODE = INGRESS
TYPE = IPV4
VALUE_LIST = ('69.55.55.112', '157.230.11.12', '3.131.208.66');

CREATE NETWORK POLICY qbash_policy
ALLOWED_NETWORK_RULE_LIST = ('analytics.public.qbash_egress');

ALTER USER qbash_service SET NETWORK_POLICY = qbash_policy;

A policy set on a user replaces the account's policy for that user, so the service user can sign in only from these three addresses. Select Requires static egress IP on the qbash credential so its calls come from them.

An admin can lift the network policy requirement for tokens with an authentication policy that sets PAT_POLICY = (NETWORK_POLICY_EVALUATION = ENFORCED_NOT_REQUIRED). With the network policy in place, a leaked token cannot be used from any other address.

Sign in with a key pair​

Follow this section or the token section after it. A key pair is the recommended sign-in for a service user.

qbash signs each request with the private key, and Snowflake checks the signature against the public key set on the user. The key has no expiry date.

  1. Generate a private key. This command encrypts it and asks for a passphrase:

    openssl genrsa 2048 | openssl pkcs8 -topk8 -v2 aes-256-cbc -inform PEM -out rsa_key.p8

    For a key without a passphrase, run this instead:

    openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out rsa_key.p8 -nocrypt
  2. Write out the public key:

    openssl rsa -in rsa_key.p8 -pubout -out rsa_key.pub
  3. Copy the contents of rsa_key.pub between the BEGIN PUBLIC KEY and END PUBLIC KEY lines and set them on the user:

    ALTER USER qbash_service SET RSA_PUBLIC_KEY = 'MIIBIjANBgkqh...';

    DESCRIBE USER qbash_service then shows the key's fingerprint in RSA_PUBLIC_KEY_FP.

Keep rsa_key.p8 private. qbash needs the whole file, plus its passphrase if you encrypted it.

Sign in with a programmatic access token​

A token is the alternative to a key pair. It is created entirely in Snowsight, and it expires after a set number of days.

  1. In Snowsight, open Users & roles, select QBASH_SERVICE, and click Generate new token.
  2. Name the token, choose QBASH_READER as its role, and set how many days it lasts. A token for a service user must name a role.
  3. Generate the token and copy its secret. Snowflake shows the secret once.

To create the token in a worksheet instead:

ALTER USER qbash_service ADD PROGRAMMATIC ACCESS TOKEN qbash_token
ROLE_RESTRICTION = 'QBASH_READER'
DAYS_TO_EXPIRY = 90;

The secret is in the token_secret column of the result.

A token lasts 15 days by default and up to 365 with DAYS_TO_EXPIRY, and an account policy can change both limits. Once it expires, calls fail with a 401 error until you paste a new token into the credential. The token runs as the role it names, so leave the role on the credential blank or set it to that role.

Add the credential in qbash​

  1. Open Connectors, then Snowflake, and click Connect.
  2. Enter a Credential Name and the Account identifier.
  3. Under Authentication, choose the sign-in method:
    • For Key pair, enter the User name, qbash_service, and paste the whole rsa_key.p8 file into Private key, including its BEGIN and END lines. Fill in Private key passphrase if the key is encrypted.
    • For Programmatic access token, paste the token secret.
  4. Under Query defaults, optionally enter a warehouse, database, schema and role. A call that leaves one out uses the credential's value, then the user's default. list_schemas, list_tables, describe_table and insert_rows read the database from the call or the credential, and all but list_schemas read the schema the same way. Names follow Snowflake's rules, so reporting_wh matches the warehouse REPORTING_WH and a name in double quotes keeps its case.
  5. Select Requires static egress IP if a network policy covers the user, and click Add Credential. qbash checks the credential with Snowflake before saving it, using a query that needs no warehouse.
  6. In the task editor, open Resources → Integrations, and select the credential. See Connect resources.

To edit the credential later, leave a secret blank to keep the saved one.

Query Snowflake from a task​

report = qbash.integrations.snowflake.execute_query(
query="""SELECT currency_pair, COUNT(*) AS trades, SUM(notional_usd) AS notional_usd
FROM fx_trades
WHERE trade_date = ? AND desk = ?
GROUP BY currency_pair
ORDER BY notional_usd DESC""",
query_params=["2026-10-14", "G10 spot"],
database="analytics",
schema="fx",
)

for row in report["result"]:
qbash.log(f'{row["CURRENCY_PAIR"]} {row["TRADES"]} {row["NOTIONAL_USD"]}')

Bind values with ? placeholders and query_params rather than formatting them into the SQL string. Snowflake returns unquoted column names and aliases in upper case, which is why the loop reads row["CURRENCY_PAIR"]. Write an alias in double quotes, such as AS "trades", to keep its case.

output_format also accepts single_value, which returns the first column of the first row, and columns_and_rows, which returns column names and rows as lists.

To write rows, pass dicts keyed by column name to insert_rows. The role needs INSERT on the table.

qbash.integrations.snowflake.insert_rows(
table="daily_rates",
rows=[
{"rate_date": "2026-10-14", "currency_pair": "EUR/USD", "close_rate": "1.0874"},
{"rate_date": "2026-10-14", "currency_pair": "USD/JPY", "close_rate": "149.62"},
],
database="analytics",
schema="fx",
)

To find names, call list_databases(), list_schemas(database=...), list_tables(database=..., schema=...) and describe_table(table=..., database=..., schema=...). list_warehouses() returns each warehouse's size and whether it is running. The database and schema default to the credential's. The SDK Reference lists every method and parameter.

Add test data​

A Snowflake trial runs for 30 days and needs no card. Trial accounts come with a warehouse named COMPUTE_WH, so use it in place of reporting_wh above.

On a new account, create the test table before you run Create a role and a service user, so its grants cover the table:

USE ROLE SYSADMIN;
CREATE DATABASE IF NOT EXISTS analytics;
CREATE SCHEMA IF NOT EXISTS analytics.fx;

CREATE TABLE analytics.fx.fx_trades (
trade_id NUMBER(38, 0),
trade_date DATE,
desk VARCHAR,
currency_pair VARCHAR,
notional_usd NUMBER(18, 2)
);

INSERT INTO analytics.fx.fx_trades VALUES
(1, '2026-10-14', 'G10 spot', 'EUR/USD', 25000000.00),
(2, '2026-10-14', 'G10 spot', 'EUR/USD', 12500000.00),
(3, '2026-10-14', 'G10 spot', 'USD/JPY', 18000000.00),
(4, '2026-10-14', 'G10 spot', 'GBP/USD', 7250000.50),
(5, '2026-10-13', 'G10 spot', 'EUR/USD', 31000000.00);

Newer accounts also include the shared database SNOWFLAKE_SAMPLE_DATA. After you create the role, let it read the share:

GRANT IMPORTED PRIVILEGES ON DATABASE snowflake_sample_data TO ROLE qbash_reader;

snowflake_sample_data.tpch_sf1.orders has 1.5 million rows. Selecting all of it returns the first 10,000 with truncated set to True.

Details that affect results​

  • execute_query runs one statement per call and refuses a second statement after a semicolon. The SQL API does not run PUT, GET, USE, ALTER SESSION, BEGIN, COMMIT or ROLLBACK. Pass warehouse, database, schema and role as arguments in place of USE.
  • NUMBER values with a scale, such as a NUMBER(18, 2) column or a SUM over one, come back as strings like "37500000.00" to keep every digit. Convert them with Decimal() before doing arithmetic. NUMBER with no scale, which includes COUNT(*), comes back as an int, and FLOAT as a float.
  • DATE, TIME and TIMESTAMP values come back as ISO 8601 strings. TIMESTAMP_TZ keeps its offset and TIMESTAMP_LTZ is converted to UTC. TIMESTAMP_NTZ has no offset. Fractions finer than a microsecond are dropped.
  • CURRENT_DATE() and CURRENT_TIMESTAMP() follow the session time zone, which is America/Los_Angeles unless the account or user sets TIMEZONE. For UTC trade dates, run ALTER USER qbash_service SET TIMEZONE = 'UTC';.
  • VARIANT, OBJECT and ARRAY values come back as parsed JSON, as dicts, lists or scalars. BINARY comes back as a hex string.
  • Results stop at max_rows, which is 10,000 by default and up to 100,000. row_count is the number of rows returned and total_rows is the number Snowflake produced. truncated is True when rows were cut, though Snowflake still runs the whole statement.
  • A statement still running after query_timeout is cancelled in Snowflake and the call fails. The timeout is 60,000 ms by default and up to 3,600,000.
  • execute_query and insert_rows run on the call's warehouse, then the credential's, then the user's default. If that warehouse is suspended and has auto-resume on, which is the default, Snowflake resumes it and bills at least 60 seconds of credits each time. list_databases, list_schemas, list_tables, list_warehouses and describe_table need no warehouse and do not resume one.
  • Every statement carries the query tag qbash, so an admin can find qbash's queries in SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY with WHERE query_tag = 'qbash'. The query_id in each result finds that statement in Snowsight's query history.
  • rows_affected is Snowflake's count of changed rows for INSERT, UPDATE, DELETE and MERGE, and is None for other statements.
  • read_only=True checks the statement before sending it. It must start with SELECT, WITH, SHOW, DESCRIBE or EXPLAIN. A SELECT or WITH containing INSERT, UPDATE, DELETE, MERGE, CALL, PROCEDURE or a DDL keyword is refused, so put a column with one of those names in double quotes. SYSTEM$ functions and the ->> pipe operator are refused too. External functions the role can call still run.
  • A Test run sends execute_query with read_only=True and does not send insert_rows. Test run (allow writes) sends both as written.
  • insert_rows takes up to 1,000 rows and 50,000 values per call and inserts them in one statement, so either all of them are inserted or none are. Values are sent as text and Snowflake converts each to its column's type, so ISO 8601 strings work for dates and timestamps. A dict or list sent to a VARIANT column is stored as a JSON string. To store it as an object, run INSERT ... SELECT PARSE_JSON(?) with execute_query.
  • describe_table reports primary and unique keys, which Snowflake records but does not enforce on standard tables.