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.

  • Data & Analytics
  • $streamlit-apps
  • 215 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- streamlit-apps

Installs to ~/.agents/skills/streamlit-apps/ for every project on this machine.

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.

~/.agents/skills/streamlit-apps/SKILL.md

--- name: streamlit-apps description: 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,… license: MIT ---

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

  1. 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 as st.login.
  2. Find the entry point (streamlit_app.py, app.py, Home.py) and the pages/ folder.
  3. 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 stable key.

Caching

ReturnsDecoratorWhy
Data: DataFrame, list, dict, query result@st.cache_dataReturns a copy per call; safe to mutate; pickled
Shared resource: DB engine, ML model, client@st.cache_resourceOne 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 ttl on anything backed by changing data, and max_entries on 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.form when 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.fragment to 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 pass value=; 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.dataframe serialize to the browser on every rerun; aggregate first or paginate.
  • cache_data pickles return values; returning a SQLAlchemy result or open file fails.
  • Mutating an object returned by cache_resource changes it for every user.
  • File uploads (st.file_uploader) are capped by server.maxUploadSize (MB) in config.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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem