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

pip install streamlitimport streamlit as sta = st.sidebar.radio('Choose:',[1,2])'_This_ is some __Markdown__'
a=3
'dataframe:', datastreamlit --help
streamlit run your_script.py
streamlit hello
streamlit config show
streamlit cache clear
streamlit docs
streamlit --versionpip uninstall streamlit
pip install streamlit-nightly --upgradeDisplay text

st.text('Fixed width text')st.markdown('_Markdown_') # optional kwarg unsafe_allow_html = Truest.caption('Balloons. Hundreds of them...')st.latex(r''' e^{i\pi} + 1 = 0 ''')st.write('Most objects') # df, err, func, keras!st.write(['st', 'is <', 3])st.title('My title')st.header('My header')st.subheader('My sub')st.code('for i in range(8): foo()')Display data

st.dataframe(my_dataframe)st.table(data.iloc[0:10])st.json({'foo':'bar','fu':'ba'})st.metric(label='Temp', value='273 K', delta='1.2 K')Display media

st.image('./header.png')st.audio(data)st.video(data)Columns

col1, col2 = st.columns(2)
col1.write('Column 1')
col2.write('Column 2')col1, col2, col3 = st.columns([3,1,1])with col1:
st.write('This is column 1')Tabs

tab1, tab2 = st.tabs(['Tab 1', 'Tab2'])
tab1.write('this is tab 1')
tab2.write('this is tab 2')with tab1:
st.radio('Select one:', [1, 2])Control flow

st.stop()st.experimental_rerun()with st.form(key='my_form'):
username = st.text_input('Username')
password = st.text_input('Password')
st.form_submit_button('Login')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!')Display interactive widgets

st.button('Hit me')st.data_editor('Edit data', data)st.checkbox('Check me out')st.radio('Pick one:', ['nose','ear'])st.selectbox('Select', [1,2,3])st.multiselect('Multiselect', [1,2,3])st.slider('Slide me', min_value=0, max_value=10)st.select_slider('Slide to select', options=[1,'2'])st.text_input('Enter some text')st.number_input('Enter a number')st.text_area('Area for textual entry')st.date_input('Date input')st.time_input('Time entry')st.file_uploader('File uploader')st.download_button('On the dl', data)st.camera_input('一二三,茄子!')st.color_picker('Pick a color')for i in range(int(st.number_input('Num:'))): foo()st.slider('Pick a number', 0, 100, disabled=True)Build chat-based apps

with st.chat_message('user'):
st.write('Hello 👋')
st.line_chart(np.random.randn(30, 3))st.chat_input('Say something')Mutate data

element = st.dataframe(df1)
element.add_rows(df2)element = st.line_chart(df1)
element.add_rows(df2)Display code

with st.echo():
st.write('Code will be executed and printed')Placeholders, help, and options

element = st.empty()
element.line_chart(...)
element.text_input(...) # Replaces previous.elements = st.container()
elements.line_chart(...)
st.write('Hello')
elements.text_input(...) # Appears above 'Hello'.st.help(pandas.DataFrame)st.get_option(key)
st.set_option(key, value)st.set_page_config(layout='wide')st.experimental_show(objects)st.experimental_get_query_params()
st.experimental_set_query_params(**params)Connect to data sources

st.experimental_connection('pets_db', type='sql')conn = st.experimental_connection('sql')conn = st.experimental_connection('snowpark')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)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()@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()@st.cache
def foo(bar):
# Do something expensive in here...
return data
d1 = foo(ref1)
d2 = foo(ref1) # cached, d1 == d2
d3 = foo(ref2)Display progress and status

with st.spinner(text='In progress'):
time.sleep(3)
st.success('Done')bar = st.progress(50)
time.sleep(3)
bar.progress(100)st.balloons()st.snow()st.toast('Mr Stay-Puft')st.error('Error message')st.warning('Warning message')st.info('Info message')st.success('Success message')st.exception(e)