The brief, line by line
Every requirement in the Capstone 2 brief (Agentic AI Workshop 2026, "Business Performance Analyst Agent"),
and where to see that it's met. "Recording" means a recorded live AI run in app/recordings/. You can watch
each one in the app's Replay mode or online.
Checked on 1 October 2026.
Result: every requirement is met. The one optional item not used is the backup dataset.
Your mission
| Brief | Status | Evidence |
|---|---|---|
| Answer plain-English business questions by writing and running analysis over sales, operations and finance data | Met | Tracks screen; agent loop |
| Produce a chart and a clear explanation | Met | Every answer with numbers by group or over time draws one chart (rulebook rule 12) |
| Flag anything unusual | Met | detect_anomalies; track "Anything unusual last quarter?" |
| At the end of the week, write a one-page briefing for the leadership team | Met | Briefing screen; downloadable as one HTML page |
What your agent must do
| # | Brief | Status | Evidence |
|---|---|---|---|
| 1 | Map each question to the right tables and metrics (revenue, margin, volume, returns, cost) | Met | get_schema gives the tables and a plain-English data dictionary. Metrics: revenue, margin, cost, volume (units), orders, margin %, discount %. Returns aren't in the data, so the agent says so (scorecard X2) |
| 2 | Write and run SQL or pandas code, and fix its own errors | Met | run_sql, run_python. Errors come back with a hint, and rule 7 says to fix and retry (up to 3 times). Example: the briefing recording, where 3 queries failed and the agent rewrote and re-ran all 3. A sample error is shown below the table |
| 3 | Choose and draw a chart that suits the question | Met | make_chart draws bar, line or breakdown charts. Recordings: bar for sales by territory, line for trends over time, a breakdown chart for "why did margin drop" |
| 4 | Explain the result and what drove it ("Margin fell because…") | Met | Track "Why Northwest margin dropped, Q2 2023": margin per unit, the Reseller channel and Mountain Bikes, with numbers. explain_change splits a change into volume, price and mix |
| 5 | Flag anomalies, such as a region or product well off its trend | Met | detect_anomalies compares each month or week with its own recent trend, by territory, category, channel and more |
| 6 | Show its working: the query it ran, so the answer can be checked | Met | "How I got this" under every answer (Developer view) lists the exact queries, taken from the tool log, never from the AI's own words (queries_used) |
What a self-correction looks like. The AI guesses a column name that doesn't exist, and run_sql sends back
the error with a hint (real output from tools.py):
{
"error": "SQL error: ... no such column: yr",
"query": "SELECT territory, SUM(revenue) FROM sales_lines WHERE yr = 2024 GROUP BY 1",
"hint": "Check names with get_schema(). This is SQLite: strftime(), LIMIT, ||. Fix the query and try again."
}
The AI then rewrites the query with the right column (year = 2024) and runs it again.
Tools to build
| Tool | Purpose (brief) | Status | Where |
|---|---|---|---|
get_schema() |
Tables, columns, data dictionary | Met | tools.py |
run_sql(query) |
Read-only query execution | Met | Refuses anything but one SELECT; the database file is opened read-only (mode=ro) |
run_python(code) |
pandas analysis in a sandbox | Met | pandas and numpy only, on earlier query results; no imports, files or system access; 10-second limit. Example call: result = df.assign(growth_pct=df['rev_2024'] / df['rev_2023'] * 100 - 100) |
make_chart(data, type) |
Bar, line or breakdown chart | Met | chart_type: bar, line, breakdown |
detect_anomalies(metric) |
Flags values off trend | Met | Also takes by, grain and period |
(extra) explain_change |
Volume / price / mix (stretch goal) | Met | The three parts always add up to the total change |
A typical run_sql call and the start of its result. The result id (q1) is what make_chart and
run_python use to pick up the rows:
run_sql("SELECT territory, ROUND(SUM(revenue), 0) AS revenue
FROM sales_lines WHERE year = 2024 GROUP BY 1 ORDER BY 2 DESC")
10 row(s) [q1]
Southwest 9,121,932
Canada 6,231,210
Northwest 6,018,433
...
How it works
The brief's flow is: question → plan the analysis → write query or code → run and self-correct → chart and explanation → weekly briefing. Met: every stage shows up as a step on the Tracks screen, and Developer view adds the arguments, SQL, timings and AI rounds for each one.
flowchart LR accTitle: The brief's flow, from question to briefing Q(["Question"]) --> P["Plan the<br/>analysis"] --> W["Write query<br/>or code"] --> R["Run it"] R -- "error: fix and retry,<br/>up to 3 times" --> W R --> C["Chart and<br/>explanation"] --> B(["Weekly<br/>briefing"]) classDef red stroke:#c62828,stroke-width:2px,color:#b71c1c class R red
Your data
| Brief | Status | Evidence |
|---|---|---|
| Microsoft AdventureWorks (MIT licence) | Met | Downloaded from Microsoft's sql-server-samples repo by ./demo.sh setup and built into a read-only SQLite file: 31,465 orders, May 2022 to June 2025, ten territories |
| UCI Online Retail II (backup) | Optional, not used | AdventureWorks has cost data, so margin can be calculated; the backup has none |
| Margin = LineTotal − (OrderQty × StandardCost) | Met | The sales_lines view: d.LineTotal - d.OrderQty * p.StandardCost AS margin (data.py) |
Guardrails (non-negotiable)
| Guardrail | How it's enforced | Tested by |
|---|---|---|
| Read-only access to the database | SQLite opened with mode=ro; run_sql refuses anything but one SELECT, and any word that changes data |
Track "Delete all the 2022 orders…" (refused); Tools screen "Try: delete the orders"; automated checks |
| Always show the query behind an answer | Queries listed from the tool log under every answer and in the briefing | Every recording |
| Say when the data can't answer | Rule 3, with the list of what's not in the data; no stand-ins from other columns | Scorecard X1 (competitor prices) and X2 (return rate): both pass |
| Never present a forecast or guess as a fact | Rule 4: show the past trend; any projection is labelled "ESTIMATE, not a fact" | Scorecard X3 and track "What will our revenue be next quarter?": pass |
Read-only is enforced three times over, so a mistake in one check can't change the data:
flowchart LR
accTitle: Three locks between the AI and the data
Q["SQL the AI wrote"] --> L1{"Starts with<br/>SELECT or WITH?"}
L1 -- yes --> L2{"One statement, no<br/>data-changing words?"}
L2 -- yes --> L3[("Database file<br/>opened read-only")]
L3 --> R["Rows, and the query<br/>kept for the answer"]
L1 -- no --> X["Refused,<br/>with a hint"]
L2 -- no --> X
classDef red stroke:#c62828,stroke-width:2px,color:#b71c1c
class X red
A delete sent straight to run_sql (as on the Tools screen) is refused before it reaches the database:
{
"error": "Only SELECT (or WITH ... SELECT) queries are allowed. The database is read-only.",
"query": "DELETE FROM SalesOrderHeader WHERE OrderDate < '2023-01-01'"
}
In the recorded track the AI doesn't even try: it answers "I can’t help delete or alter data; the database is
read-only, and I’m only able to run SELECT queries." What the agent is told is missing from the data
(tools.py, AW_DICTIONARY):
"not_in_data": ["returns or refunds", "competitors or their prices", "customer satisfaction",
"marketing spend", "website traffic", "budgets or targets", "weather", "staff morale",
"any future figures (no forecasts)"],
Try these first
| Question | Status | Where |
|---|---|---|
| "What were total sales by territory last year?" | Met | Track 01, scorecard T1 |
| "Why did margin drop in the Northwest in Q2?" | Met | Track 03 (names the year: Q2 2023), scorecard T2 |
| "Which product category is growing fastest?" | Met | Track 04, scorecard T3 |
| "Is anything unusual in last quarter's numbers?" | Met | Track 05, scorecard T4 |
Scorecard on the recordings: 7 of 7: the 4 questions above plus the 3 guardrail traps, each marked against an answer worked out from the database at marking time.
Stretch goals and live demo
| Brief | Status | Evidence |
|---|---|---|
| Follow-up drill-downs ("…and by product?") | Met | Track 02 "…and by product category?" remembers the previous answer |
| Break a change into its causes (volume, price, mix) | Met | explain_change, used in the Northwest margin answer |
| Generate the one-page weekly leadership briefing automatically | Met | Briefing screen: the KPI table comes straight from SQL, the AI writes the commentary |
| Demo moment: the leadership team asks a question that leads to a planted anomaly the agent must explain | Met | Hidden track on side B, using a separate copy of the database with a planted 25% bike discount in Northwest, May 2024. Recorded: the agent found the reseller discount jump (to about 22%) and the margin loss ($96,403) |
| Builds on Lab 2 (data analyst agent) | Met | Sections 1 to 5 of the notebook (the Lab 2 engine) kept unchanged; Section 6 added |
Workshop schedule and judging
| Brief | How we prepared |
|---|---|
| 6-minute demo | The Tracks screen is the plan: side A plus side B add up to exactly 6:00, with a counter per track |
| 2-minute Q&A | Prepared answers in the build guide; the Story screen answers "how did you build it?" |
| Business value | Cover screen (managers wait days for an analyst), weekly briefing |
| Agent design | "What changed" screen: tools, rulebook before and after; Developer view |
| Working demo | Live AI, with labelled Replay as a backup if the AI or wifi fails |
| Trust & safety | Read-only database, guardrail tracks, scorecard |
| Pitch | 60-second pitch video, spoken "Explain this page" on every screen |
A learning project, not a product. AdventureWorks is Microsoft's public sample data about a made-up bicycle company. Made by Victor Saly, Akashdeep Nijjar, Manuel Verduzco Valenzuela, Mazen Ahmed, Buddhika Gamage, Nathan Fryatt, Michael Kampouridis and Malak Sheat (the team). Source of this page: docs/requirements.md.