Over the past year I migrated my team's dashboards from GUI-based BI tools to a fully code-driven workflow with Python + Streamlit, using AI coding assistants like Claude Code and Cursor to speed up iteration.

Right now I'm migrating dashboards off Looker (Google Data Studio) and aggregating information from various tools into Streamlit dashboards. The reason is simple: it saves time to build a POC with AI, and then I can continue the updates myself. No waiting on a BI license, no drag-and-drop ceiling — just Python.

Sample prompt

The example below gives OK results for a POC — enough to see something on screen, not enough to ship:

create me Streamlit dashboard about

compare multiple sites:
* section onsite analysis
* serp analysis
    * ranking
    * search results - google, bing, duckduckgo
* keyword comparison
    - home page: title, headings etc
* pricing
* features

files:
* dashboard.py - UI
* data_processor.py - data preprocessing
* visualization.py - data plots and figures
* scraper.py - data collector
* config.yaml - configuration

add more if needed to make it better

But to get better results, smaller steps are needed. Instead of one big prompt, I iterate file by file with concrete instructions, then move to the next one. For example:

dashboard.py

  • concise layout
  • no tabs
  • sections
    • onsite — basic info about titles, pages
    • serp — compare serp data

So far the results are mixed. Still experimenting.

What actually drives good results

In my experience, the output depends mostly on:

  • The initial idea — I prefer to outline the idea in my head first, then on paper. The clearer the shape before prompting, the better.
  • How the idea is shared with the model — smaller steps beat one giant prompt. Deterministic tests and validation keep the agent honest.
  • Inspecting the code yourself — always verify accuracy rather than trusting the first output.

Dashboard design principles

Good code is only half the battle. A dashboard that actually works needs to respect a few principles:

  • Show only important information — minimize or avoid scrolling so key metrics land in the first glance.
  • Organize info in grouped levels — use containers, columns, and tabs to create hierarchy rather than dumping everything on one page.
  • Use color deliberately — make changes and indicators visible with color, but reserve it for what actually needs attention.
  • Adopt a narrative approach — tell a story with your data, not just raw numbers and graphs.
  • Make it self-explanatory — design so viewers need minimal additional questions to understand what they're seeing.
  • Structure as a master dashboard plus details — personally, I prefer one master dashboard with top-level info, supported by a few others for deeper detail.

The full cheatsheet is based on cheat-sheet.streamlit.app. If you're already fluent in Python, this should get you from idea to working dashboard in minutes rather than hours.

Checking Examples and Asking the Model to Update

Sometimes I browse the examples from streamlit.io/gallery?category=favorites to get inspiration, then ask the model to update my dashboard based on what I see. Below are some Streamlit dashboard examples from the web that I've collected — useful as reference points when prompting an AI agent.

Link Description
Cheat Sheet A cheat sheet for Streamlit by @daniellewisDL
Goodreads App Analyzing your Goodreads reading habits by @tylerjrichards
Arup Social Data Code and data for eviction and housing analysis in the US by @arup-group
Sales Dashboard Interactive dashboard visualizing sales data from Excel with dynamic filters and key KPIs
Health Analytics Modern health analytics dashboard visualizing recovery trends, sleep patterns, movement, and calories burned
AI Data Analysis Agent AI-powered data analysis dashboard for CSV exploration, visualization, and insights
COVID-19 Report Dashboard Streamlit dashboard for COVID-19 reporting with real-time updates and visualizations

So far the results from this approach are mixed — some prompts produce a solid layout out of the gate, others need several rounds of iteration. Still experimenting.

Note: that often dashboards can disappear. That's why I'm creating a local library of good dashboard examples. For this I'm using Obsidian as a master organizer for:

  • dashboards
  • Jupyter notebooks
  • Python scripts
  • datasets

Cheatsheet

Setup

Install and import Streamlit
pip install streamlit
Install streamlit
import streamlit as st
Import convention
a = st.sidebar.radio('Choose:',[1,2])
Add widgets to sidebar (just add it after st.sidebar)
'_This_ is some __Markdown__' a=3 'dataframe:', data
Magic commands implicitly st.write()
streamlit --help streamlit run your_script.py streamlit hello streamlit config show streamlit cache clear streamlit docs streamlit --version
Command line
pip uninstall streamlit pip install streamlit-nightly --upgrade
Pre-release features

Display text

Display text
st.text('Fixed width text')
Fixed width text
st.markdown('_Markdown_') # optional kwarg unsafe_allow_html = True
Markdown
st.caption('Balloons. Hundreds of them...')
Caption
st.latex(r''' e^{i\pi} + 1 = 0 ''')
LaTeX
st.write('Most objects') # df, err, func, keras!
Write most objects
st.write(['st', 'is <', 3])
Write a list
st.title('My title')
Title
st.header('My header')
Header
st.subheader('My sub')
Subheader
st.code('for i in range(8): foo()')
Code block

Display data

Display data
st.dataframe(my_dataframe)
DataFrame as an interactive table
st.table(data.iloc[0:10])
Static table
st.json({'foo':'bar','fu':'ba'})
JSON object
st.metric(label='Temp', value='273 K', delta='1.2 K')
Metric with delta

Display media

Display media
st.image('./header.png')
Image
st.audio(data)
Audio
st.video(data)
Video

Columns

Columns
col1, col2 = st.columns(2) col1.write('Column 1') col2.write('Column 2')
Create two columns and write into them
col1, col2, col3 = st.columns([3,1,1])
Three columns with different widths (col1 is wider)
with col1: st.write('This is column 1')
Using 'with' notation

Tabs

Tabs
tab1, tab2 = st.tabs(['Tab 1', 'Tab2']) tab1.write('this is tab 1') tab2.write('this is tab 2')
Insert containers separated into tabs
with tab1: st.radio('Select one:', [1, 2])
Tabs using 'with' notation

Control flow

Control flow
st.stop()
Stop execution immediately
st.experimental_rerun()
Rerun script immediately
with st.form(key='my_form'): username = st.text_input('Username') password = st.text_input('Password') st.form_submit_button('Login')
Group multiple widgets in a form

Personalize apps for users

Personalize apps for users
if st.user.email == '[email protected]': display_jane_content() elif st.user.email == '[email protected]': display_adam_content() else: st.write('Please contact us to get access!')
Show different content based on the user's email address

Display interactive widgets

Display interactive widgets
st.button('Hit me')
Button
st.data_editor('Edit data', data)
Data editor
st.checkbox('Check me out')
Checkbox
st.radio('Pick one:', ['nose','ear'])
Radio
st.selectbox('Select', [1,2,3])
Selectbox
st.multiselect('Multiselect', [1,2,3])
Multiselect
st.slider('Slide me', min_value=0, max_value=10)
Slider
st.select_slider('Slide to select', options=[1,'2'])
Select slider
st.text_input('Enter some text')
Text input
st.number_input('Enter a number')
Number input
st.text_area('Area for textual entry')
Text area
st.date_input('Date input')
Date input
st.time_input('Time entry')
Time input
st.file_uploader('File uploader')
File uploader
st.download_button('On the dl', data)
Download button
st.camera_input('一二三,茄子!')
Camera input
st.color_picker('Pick a color')
Color picker
for i in range(int(st.number_input('Num:'))): foo()
Use widgets' returned values in variables
st.slider('Pick a number', 0, 100, disabled=True)
Disable widgets to remove interactivity

Build chat-based apps

Build chat-based apps
with st.chat_message('user'): st.write('Hello 👋') st.line_chart(np.random.randn(30, 3))
Insert a chat message container
st.chat_input('Say something')
Display a chat input widget

Mutate data

Mutate data
element = st.dataframe(df1) element.add_rows(df2)
Add rows to a dataframe after showing it
element = st.line_chart(df1) element.add_rows(df2)
Add rows to a chart after showing it

Display code

Display code
with st.echo(): st.write('Code will be executed and printed')
Echo code

Placeholders, help, and options

Placeholders, help, and options
element = st.empty() element.line_chart(...) element.text_input(...) # Replaces previous.
Replace any single element
elements = st.container() elements.line_chart(...) st.write('Hello') elements.text_input(...) # Appears above 'Hello'.
Insert out of order
st.help(pandas.DataFrame)
Help
st.get_option(key) st.set_option(key, value)
Get and set options
st.set_page_config(layout='wide')
Page config
st.experimental_show(objects)
Show objects
st.experimental_get_query_params() st.experimental_set_query_params(**params)
Query params

Connect to data sources

Connect to data sources
st.experimental_connection('pets_db', type='sql')
Connection with type
conn = st.experimental_connection('sql')
SQL connection
conn = st.experimental_connection('snowpark')
Snowpark connection
class MyConnection(ExperimentalBaseConnection[myconn.MyConnection]): def _connect(self, **kwargs) -> MyConnection: return myconn.connect(**self._secrets, **kwargs) def query(self, query): return self._instance.query(query)
Create your own connection class

Optimize performance

Optimize performance
@st.cache_data def foo(bar): # Do something expensive and return data return data d1 = foo(ref1) d2 = foo(ref1) # cached, d1 == d2 d3 = foo(ref2) # different arg, executes foo.clear() st.cache_data.clear()
Cache data objects (cached by value)
@st.cache_resource def foo(bar): # Create and return a non-data object return session s1 = foo(ref1) s2 = foo(ref1) # cached, s1 == s2 s3 = foo(ref2) # different arg, executes foo.clear() st.cache_resource.clear()
Cache global resources (cached by reference)
@st.cache def foo(bar): # Do something expensive in here... return data d1 = foo(ref1) d2 = foo(ref1) # cached, d1 == d2 d3 = foo(ref2)
Deprecated caching (@st.cache)

Display progress and status

Display progress and status
with st.spinner(text='In progress'): time.sleep(3) st.success('Done')
Show a spinner during a process
bar = st.progress(50) time.sleep(3) bar.progress(100)
Show and update progress bar
st.balloons()
Balloons
st.snow()
Snow
st.toast('Mr Stay-Puft')
Toast
st.error('Error message')
Error message
st.warning('Warning message')
Warning message
st.info('Info message')
Info message
st.success('Success message')
Success message
st.exception(e)
Exception