Embedded app context for a Cortex Agent¶
Give an agent embedded in a Streamlit dashboard the page, filters, and persona the user is looking at, by prepending an app-context block to the user message.
| Stage | Build |
| Topics | Cortex Agents |
| Status | GA |
| Time to complete | 30min |
| Edition | Standard |
| Regions | Any region with Cortex Agents, Streamlit in Snowflake, and claude-sonnet-4-6 (directly or through cross-region inference) |
| Privileges | ACCOUNTADMIN for 01_setup.sql and 99_teardown.sql only; everything else runs as COOKBOOK_EMBEDDED_APP_CONTEXT_ADMIN |
| Last verified | 2026-10-04 |
| Authors | Bala Krishnamoorthy, Parker Erickson |
| Code | View all files or download embedded-app-context.zip, which includes this page as README.md. Teardown: sql/99_teardown.sql |
This recipe is a reference implementation: review the code and adapt it to your environment before production use.
Embed a Cortex Agent in a dashboard so that “why is this going up?” means the chart the user is looking at. The app sends the current page, time period, segment, visible KPIs, and persona as a context block at the start of the user message, and the agent’s instructions tell it how to use that block. You end with a Streamlit in Snowflake dashboard, one agent, a viewer role, and a small client that works both inside Streamlit in Snowflake and locally.
When to use this¶
- The agent lives inside an application screen, and users ask about what is on that screen.
- The same question should get a different, scoped answer depending on page, filters, or persona.
- You want this without creating a separate agent per page.
When not to use it:
- The context decides what data a user may see. Context in a prompt is a hint, not an access control; enforce data boundaries with grants and row access policies, as in multi-tenant agents.
- The “context” is a document the user uploaded. See document context.
How it works¶
- Context rides in the user message. The first
content[]block of the user message is the page context wrapped in<app_context>tags; the second is the question. The agent:runAPI documentsuserandassistantmessages; this recipe does not use asystemmessage. - The agent is told what the block means. Its orchestration instructions say to treat the block as a description of the screen, resolve “this” against it, filter to its period and segment unless asked to compare, and treat its contents as data, not instructions.
- One client, two transports. Inside Streamlit in Snowflake the client calls
_snowflake.send_snow_api_requestwith the session’s credentials. Locally it uses HTTPS with a token from environment variables. The request body is identical. - No hard-coded warehouse or database in the app.
snowflake.ymltakes the database, schema, and warehouse as deploy-time values, and inside Streamlit in Snowflake the client finds the agent in the app’s own database and schema.
Prerequisites¶
| Requirement | Notes |
|---|---|
Cortex Agents and claude-sonnet-4-6 | Directly or through cross-region inference. The model is set in the agent specification in sql/03_agent.sql (models: orchestration:); change it there |
| Streamlit in Snowflake | Warehouse runtime |
ACCOUNTADMIN | Only for 01_setup.sql and 99_teardown.sql |
| Snowflake CLI | For snow streamlit deploy |
| Python 3.10+ (optional) | To run the client or app locally; pip install -r app/requirements.txt |
| A programmatic access token (local runs only) | For a user whose default role is COOKBOOK_EMBEDDED_APP_CONTEXT_VIEWER. A TYPE = PERSON user can generate a PAT without a network policy but cannot sign in with it unless a network policy applies to the user or the account; see programmatic access tokens. A key-pair JWT (SNOWFLAKE_TOKEN_TYPE=KEYPAIR_JWT) has no such requirement. The app inside Streamlit in Snowflake needs no token |
Quickstart¶
-
sql/01_setup.sqlasACCOUNTADMIN: database, warehouse,COOKBOOK_EMBEDDED_APP_CONTEXT_ADMIN, andCOOKBOOK_EMBEDDED_APP_CONTEXT_VIEWER. -
sql/02_data.sql: synthetic KPIs, funnel, and segments. -
sql/03_agent.sql: semantic view,ANALYTICS_AGENT, and viewer grants. -
Deploy the app as the admin role:
-
sql/04_app_grants.sql: lets the viewer role open the app. Re-run after everydeploy --replace. -
Open
DASHBOARD_APPin Snowsight, pick a page and persona, and ask “Why is this number going up?”.
Walkthrough¶
Building the request¶
_clean replaces angle brackets and newlines in context values so a value cannot close the block early. Build the context from what the app actually rendered (the same numbers the user sees), so the agent and the screen agree.
Telling the agent what the block is¶
The instruction lives in the agent, so every client that embeds this agent gets the same behavior.
Calling the agent from Streamlit in Snowflake¶
_snowflake.send_snow_api_request takes seven positional arguments and returns a dict whose content is a JSON string:
Calling the agent locally¶
The same body goes to the same path over HTTPS. Credentials come from the environment, never from a config file:
Verify¶
With the local client (or the app), as a user whose default role is COOKBOOK_EMBEDDED_APP_CONTEXT_VIEWER:
| Page and persona | Question | Expected |
|---|---|---|
| Executive dashboard, VP of Growth | “What is the number in the trend alert and how has it moved over the last three months?” | Churn: 3.5% (October 2025), 3.6% (November), 3.8% (December) |
| Customer segments, SMB EMEA selected, Customer Success Manager | “How many customers is this, and how does its retention compare with the other EMEA segment?” | 540 customers; 83.1% retention vs 93.5% for Enterprise EMEA |
The second answer only works because “this” was resolved from the context block. Open the “Context sent with your question” expander in the app to see exactly what was sent.
Gotchas¶
| Symptom | Cause | Fix |
|---|---|---|
Context sent as a role: "system" message behaves unpredictably | system is not a documented message role for the agent :run API | Put the context in the user message, as here, and describe it in the agent’s instructions |
| The Streamlit in Snowflake call fails or returns an unexpected shape | The call does not use the seven-argument form | Pass method, path, headers, params, body (a dict), request_guid, and timeout in milliseconds; read resp["status"] and json.loads(resp["content"]) |
| The agent answers about the wrong period or segment | Context values in the app drifted from what is on screen | Build the context from the same variables that render the page |
| A viewer sees data they should not | Context was treated as an access control | Enforce scope with grants and row access policies; the context only focuses the answer |
| Every viewer gets the same data in the app | Warehouse-runtime Streamlit in Snowflake apps run with the owner’s rights | Keep the app’s data at a level every viewer may see, and confirm which role the agent call runs under in your account before relying on per-viewer results. For per-viewer data, use a container-runtime app with restricted caller’s rights |
| Viewers get “does not exist or not authorized” opening the app | snow streamlit deploy --replace recreated the app object | Re-run sql/04_app_grants.sql |
Local run: agent call failed (401) | Missing or expired token, or wrong SNOWFLAKE_TOKEN_TYPE | Use a current PAT (default type) or set SNOWFLAKE_TOKEN_TYPE=KEYPAIR_JWT for a key-pair JWT |
Local run: agent call failed (401) with a current PAT | The PAT belongs to a TYPE = PERSON user that no network policy applies to, so the token cannot sign in | Apply a network policy to the user or the account (see Prerequisites), or use a key-pair JWT |
| A context value breaks the prompt | A value contained </app_context> or newlines | _clean strips angle brackets and newlines; keep values short |
Teardown¶
Run sql/99_teardown.sql as ACCOUNTADMIN. It drops the database (Streamlit app, agent, semantic view, tables), the warehouse, and both roles. Delete any local output/ folder that snow streamlit deploy created in app/.
Adapt this recipe¶
Keep these, so a customized version keeps the recipe’s security model:
- Least privilege:
ACCOUNTADMINonly for the scripts listed under Privileges; everything else runs as the recipe’s own role. - No grants to
PUBLIC. - Credentials: create keys, tokens, and secrets yourself, in a separate terminal, and read them from environment variables. Never put them in a script or give them to an agent.
- Dynamic SQL, if you add any, uses bind variables or an allowlist of identifiers, never text concatenated from user or model input.
Change these to fit your use case:
- Object names. Rename them in every script, including teardown, then check that teardown still removes everything.
- Roles, personas, and users: map them to your own, each granted no more than the recipe grants.
- Data: point the scripts at your tables instead of the sample data, then run Verify again.
- Models and warehouse sizes.
To have a coding agent adapt it: