Streamlit Apps skill for Codex
Fast reruns, deliberate state, tested pages. Codex builds Streamlit apps around the rerun model: expensive loads go into the correct cache with TTLs, user state lives in session_state, forms and fragments stop needless reruns, and logic sits in plain tested modules. Secrets come from st.connection and secrets.toml, AppTest covers the pages offline, and deployment waits for your go-ahead on target and auth.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- streamlit-apps Installs to ~/.agents/skills/streamlit-apps/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install streamlit-apps The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project streamlit-apps Installs to .agents/skills/streamlit-apps/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/streamlit-apps.zipunzip -o streamlit-apps.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) streamlit-apps PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $streamlit-apps, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and ships Streamlit data apps that stay fast and correct - the rerun execution model, st.cache_data vs st.cache_resource, st.session_state, forms and fragments to limit reruns, multipage apps with st.navigation, st.connection and secrets.toml, AppTest unit tests, and deployment to Community Cloud, Docker or a container platform. Use when building an internal data tool or dashboard in Python, a Streamlit app is slow or loses state on every click, widgets reset unexpectedly, or an app needs tests, auth or deployment.
Use it when
- Building an internal data tool or explorer in Python
- A Streamlit app is slow or reloads data on every click
- Widget values or state reset unexpectedly
- The app needs tests, secrets handling, auth or deployment
Use something else when
- Stakeholders need a standard KPI dashboard in a BI tool: use bi-dashboards
- You need a full web application with custom frontend: use fastapi-services
How to check the result
AppTest and module tests pass offline, the health endpoint responds in the container, and a filter change reruns in well under a second.
The mistake it prevents
Putting a database query outside any cache or using cache_resource for per-user data that then leaks between sessions.
A good Streamlit app reruns top to bottom in well under a second on each interaction
because expensive work is cached, keeps user state deliberately in st.session_state,
reads secrets from st.secrets rather than code, has an AppTest suite, and starts in a
container with one command.
Inspect first
- Read
AGENTS.md, the dependency file and.streamlit/config.toml. Note the Streamlit version (streamlit version); APIs below assume 1.40 or newer. Check before using newer features such asst.login. - Find the entry point (
streamlit_app.py,app.py,Home.py) and thepages/folder. - Time a rerun: add
st.write(time.perf_counter() - t0)at the bottom temporarily, or use the browser dev tools. Anything above about 300 ms per click needs caching.
The execution model
Every widget interaction reruns the whole script from the top. Consequences:
- Anything not cached is recomputed on every click: database queries, file reads, model loading.
- Plain Python variables reset on every rerun. Persist across reruns only through
st.session_state, caches or widget values. - Widgets are identified by their parameters or
key. Changing a label or options creates a new widget and resets its value; give important widgets a stablekey.
Caching
| Returns | Decorator | Why |
|---|---|---|
| Data: DataFrame, list, dict, query result | @st.cache_data | Returns a copy per call; safe to mutate; pickled |
| Shared resource: DB engine, ML model, client | @st.cache_resource | One global object for all sessions; must be thread-safe; never mutate per user |
import pandas as pd
import streamlit as st
@st.cache_resource
def get_model():
import joblib
return joblib.load("models/churn.joblib")
@st.cache_data(ttl="10m", max_entries=50, show_spinner="Loading orders...")
def load_orders(start: str, end: str, region: str) -> pd.DataFrame:
conn = st.connection("warehouse", type="sql")
return conn.query(
"select * from marts.fct_orders where order_date between :s and :e and region = :r",
params={"s": start, "e": end, "r": region}, ttl="10m",
)
- Arguments form the cache key and must be hashable; prefix a parameter with
_to exclude it (def f(_conn, day)). - Set
ttlon anything backed by changing data, andmax_entrieson caches keyed by user input, or memory grows without bound. - Never cache per-user secrets or permission-filtered data in
cache_resource; it is shared across all users.
State, forms and fragments
if "selected" not in st.session_state:
st.session_state.selected = []
with st.form("filters"):
region = st.selectbox("Region", ["EU", "US", "APAC"], key="region")
dates = st.date_input("Dates", key="dates")
submitted = st.form_submit_button("Apply")
- Use
st.formwhen several inputs should trigger one rerun on submit, not one each. - Use callbacks (
on_change=,on_click=) to update state before the rerun, which avoids the “clicked twice to take effect” bug. - Use
@st.fragmentto rerun only part of the page when its widgets change, and@st.fragment(run_every="30s")for auto-refreshing panels.
@st.fragment
def drilldown(df: pd.DataFrame) -> None:
col = st.selectbox("Breakdown", ["channel", "country"], key="breakdown")
st.bar_chart(df.groupby(col)["revenue"].sum())
- Do not set a widget’s value through
st.session_state[key]and also passvalue=; Streamlit warns and the result is confusing. Pick one. st.rerun()forces an immediate rerun; use it sparingly, and never unconditionally (infinite loop).
Structure
app/
├── streamlit_app.py # st.navigation + shared sidebar
├── pages/
│ ├── overview.py
│ └── customers.py
├── lib/ # data access and logic: plain Python, unit-tested
│ ├── data.py
│ └── metrics.py
├── .streamlit/
│ ├── config.toml # theme, server options (commit)
│ └── secrets.toml # credentials (never commit; add to .gitignore)
└── tests/
└── test_app.py
# streamlit_app.py
import streamlit as st
st.set_page_config(page_title="Commerce", layout="wide")
pg = st.navigation([
st.Page("pages/overview.py", title="Overview", icon=":material/insights:"),
st.Page("pages/customers.py", title="Customers", icon=":material/group:"),
])
pg.run()
Keep business logic in lib/ as plain functions so it can be tested without Streamlit.
Page files should mostly call lib and render.
Secrets and connections
# .streamlit/secrets.toml (local only; on Community Cloud paste into the app's Secrets)
[connections.warehouse]
url = "postgresql+psycopg://reader:[email protected]:5432/analytics"
st.connection("warehouse", type="sql") reads that section. Use a read-only database
user. Never print st.secrets or put credentials in config.toml.
Auth
Streamlit apps are public to anyone who can reach the URL unless protected. Options:
Community Cloud viewer restrictions, a reverse proxy or platform auth in front (Cloud
Run IAP, an OAuth proxy), or built-in OIDC via st.login() / st.user in recent
versions with an [auth] section in secrets. Confirm with the user which applies before
exposing internal data.
Testing
# tests/test_app.py
from streamlit.testing.v1 import AppTest
def test_overview_renders_with_filters():
at = AppTest.from_file("pages/overview.py", default_timeout=10).run()
assert not at.exception
at.selectbox(key="breakdown").set_value("country").run()
assert not at.exception
assert len(at.metric) == 4 # headline KPIs still rendered
Mock data access in tests (monkeypatch lib.data.load_orders) so tests run offline
and quickly. AppTest does not render charts; assert on data and text elements.
Deployment
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
HEALTHCHECK CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8501/_stcore/health')"
CMD ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.headless=true"]
- Behind a proxy or load balancer, WebSockets must pass through and sessions need sticky routing when running more than one replica.
- Community Cloud deploys from a GitHub repo and reads
requirements.txt. - Deploying makes the app reachable by others; confirm target, audience and auth with the user before any deploy command or push to a deploy branch.
Gotchas
- Large DataFrames sent to
st.dataframeserialize to the browser on every rerun; aggregate first or paginate. cache_datapickles return values; returning a SQLAlchemy result or open file fails.- Mutating an object returned by
cache_resourcechanges it for every user. - File uploads (
st.file_uploader) are capped byserver.maxUploadSize(MB) inconfig.toml. - Module-level code in imported files runs once per process, not per rerun; do not put per-user state there.
Verification
pytest -q tests/
streamlit run streamlit_app.py --server.headless=true & # then curl the health check
curl -fsS http://localhost:8501/_stcore/health
Click through each page with realistic filters; check rerun time, that state survives navigation where intended, and that no secret appears in the UI or logs.
Delivery template
**App:** Commerce explorer (streamlit_app.py, 2 pages)
**Data:** warehouse via st.connection (read-only user), cached 10 min
**Performance:** filter change 180 ms (was 4.2 s) after cache_data on load_orders
**Tests:** 6 AppTest + 11 lib tests, offline
**Run:** docker build -t commerce-app . && docker run -p 8501:8501 commerce-app
**Not done / needs approval:** deployment to Cloud Run with IAP What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Streamlit Apps"
short_description: "Fast reruns, deliberate state, tested pages"
default_prompt: "$streamlit-apps Build a Streamlit app to explore our orders table with filters and charts" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.