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.
-
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.p8For a key without a passphrase, run this instead:
openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out rsa_key.p8 -nocrypt -
Write out the public key:
openssl rsa -in rsa_key.p8 -pubout -out rsa_key.pub -
Copy the contents of
rsa_key.pubbetween theBEGIN PUBLIC KEYandEND PUBLIC KEYlines and set them on the user:ALTER USER qbash_service SET RSA_PUBLIC_KEY = 'MIIBIjANBgkqh...';DESCRIBE USER qbash_servicethen shows the key's fingerprint inRSA_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.
- In Snowsight, open Users & roles, select
QBASH_SERVICE, and click Generate new token. - Name the token, choose
QBASH_READERas its role, and set how many days it lasts. A token for a service user must name a role. - 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
- Open Connectors, then Snowflake, and click Connect.
- Enter a Credential Name and the Account identifier.
- Under Authentication, choose the sign-in method:
- For Key pair, enter the User name,
qbash_service, and paste the wholersa_key.p8file into Private key, including itsBEGINandENDlines. Fill in Private key passphrase if the key is encrypted. - For Programmatic access token, paste the token secret.
- For Key pair, enter the User name,
- 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_tableandinsert_rowsread the database from the call or the credential, and all butlist_schemasread the schema the same way. Names follow Snowflake's rules, soreporting_whmatches the warehouseREPORTING_WHand a name in double quotes keeps its case. - 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.
- 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_queryruns one statement per call and refuses a second statement after a semicolon. The SQL API does not runPUT,GET,USE,ALTER SESSION,BEGIN,COMMITorROLLBACK. Passwarehouse,database,schemaandroleas arguments in place ofUSE.NUMBERvalues with a scale, such as aNUMBER(18, 2)column or aSUMover one, come back as strings like"37500000.00"to keep every digit. Convert them withDecimal()before doing arithmetic.NUMBERwith no scale, which includesCOUNT(*), comes back as an int, andFLOATas a float.DATE,TIMEandTIMESTAMPvalues come back as ISO 8601 strings.TIMESTAMP_TZkeeps its offset andTIMESTAMP_LTZis converted to UTC.TIMESTAMP_NTZhas no offset. Fractions finer than a microsecond are dropped.CURRENT_DATE()andCURRENT_TIMESTAMP()follow the session time zone, which isAmerica/Los_Angelesunless the account or user setsTIMEZONE. For UTC trade dates, runALTER USER qbash_service SET TIMEZONE = 'UTC';.VARIANT,OBJECTandARRAYvalues come back as parsed JSON, as dicts, lists or scalars.BINARYcomes back as a hex string.- Results stop at
max_rows, which is 10,000 by default and up to 100,000.row_countis the number of rows returned andtotal_rowsis the number Snowflake produced.truncatedisTruewhen rows were cut, though Snowflake still runs the whole statement. - A statement still running after
query_timeoutis cancelled in Snowflake and the call fails. The timeout is 60,000 ms by default and up to 3,600,000. execute_queryandinsert_rowsrun 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_warehousesanddescribe_tableneed no warehouse and do not resume one.- Every statement carries the query tag
qbash, so an admin can find qbash's queries inSNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORYwithWHERE query_tag = 'qbash'. Thequery_idin each result finds that statement in Snowsight's query history. rows_affectedis Snowflake's count of changed rows forINSERT,UPDATE,DELETEandMERGE, and isNonefor other statements.read_only=Truechecks the statement before sending it. It must start withSELECT,WITH,SHOW,DESCRIBEorEXPLAIN. ASELECTorWITHcontainingINSERT,UPDATE,DELETE,MERGE,CALL,PROCEDUREor 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_querywithread_only=Trueand does not sendinsert_rows. Test run (allow writes) sends both as written. insert_rowstakes 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 aVARIANTcolumn is stored as a JSON string. To store it as an object, runINSERT ... SELECT PARSE_JSON(?)withexecute_query.describe_tablereports primary and unique keys, which Snowflake records but does not enforce on standard tables.