The Workbench, documented.
Everything the code environment does, in one page: the engines, the syntax, working examples, and the path from raw dataset to published work. Every snippet below runs as-is in a Workbench block.
๐งช The environment
What actually runs when you press Run.
The Workbench is a notebook of blocks โ note, SQL, and Python โ and the whole engine runs inside your browser tab. Nothing is installed on your machine and no code or data is sent to a server to execute:
| Block type | Engine | What that means |
|---|---|---|
| SQL | DuckDB (WebAssembly) | A real analytical database in the tab. Loads with the page; queries run instantly on the attached datasets. |
| Python | Pyodide 314 โ Python 3.14 in WebAssembly | Real Python with pandas 3, NumPy 2 and matplotlib 3.10 โ the same versions as the Academy and the LucidPlexus books. Downloads about 22 MB the first time you run a Python block (charts add about 5 MB), then it is cached. |
| Note | โ | Prose. Notes become the README of your published work. |
Deliberate limits (they are features, not bugs): Python has no network access, no pip install, no input(), and no threads; memory is what the browser tab allows. Everything a project needs ships with the runtime.
โจ๏ธ The editor
A real code editor in every SQL and Python block โ with suggestions from your own data.
Code is coloured as you type, brackets and quotes close themselves, and suggestions appear as you type: SQL keywords and DuckDB functions, your dataset tables, their columns (each labelled with its table), and any view you have created โ as soon as the block that creates it has run. Right after FROM or JOIN it suggests tables; elsewhere, columns. In Python it suggests the lp helpers, dataset names inside lp.table("โฆ"), column names inside df["โฆ"], and common pandas methods after a dot.
| Keys | What they do |
|---|---|
| Ctrl/โ + Enter | Run this block. |
| Ctrl + Space | Show suggestions now. |
| โ โ ยท Enter / Tab | Move through the suggestions ยท take the highlighted one. |
| Tab ยท Shift + Tab | Indent ยท outdent (when no suggestion is open). |
| Esc | Close the suggestions. Esc then Tab moves on to the next control, for keyboard-only use. |
| Ctrl/โ + F | Find (and replace) in this block. |
| Ctrl/โ + Z | Undo (Shift adds redo). |
Everything you type is saved as you go, exactly as before. The same editor is used in the Academy's career projects and practice challenges.
๐๏ธ Getting data in
Upload โ attach โ alias. The alias is the table name.
Data enters the platform once, at the top, and flows down to every member:
- Upload (curators): an admin uploads a CSV (or an Excel file, whose first sheet is converted to CSV) in the dataset manager. Rows and columns are scanned automatically and the dataset appears in the public library.
- Attach (brief authors): a project brief attaches one or more datasets, each under an alias. Aliases are validated because they become SQL table names โ budget works, Budget Data! is rejected.
- Use (you): open the Workbench on a claimed brief and every attached dataset is already loaded as a table under its alias. The left rail lists each one with a โ and its row count.
-- The aliases in the left rail ARE your tables. First look:
SELECT * FROM budget LIMIT 5;
Referring to a dataset by name
Code never uses a dataset's title from the library ("Public Procurement Data"). Each brief gives every dataset it attaches a short alias โ lowercase letters, digits and underscores, starting with a letter, up to 41 characters โ and that alias is its name in SQL and in Python. You will find it in the Workbench's left rail, under "Datasets in scope" on the brief page (alias: โฆ), in the Data Explorer's list, or by asking the engine:
SHOW TABLES; -- every dataset alias, plus the views you have made
DESCRIBE budget; -- its columns and their types
SUMMARIZE budget; -- per column: min, max, average, distinct values, % missing
SELECT * FROM budget LIMIT 5;
| You want | Write |
|---|---|
| A dataset | Its alias: FROM budget. Case does not matter โ Budget works too. |
| A short name inside one query | FROM budget b (or budget AS b), then b.category. |
| A column from one particular table | budget.category or b.category โ needed in a join when both tables have a column with that name. |
| A column whose name has spaces or capitals | Double quotes: "Order Date". Column names come from the file's header row as they are. |
| An alias that is also an SQL word | Double quotes: FROM "order". |
| A view you created | Its name, like any table: FROM clean_budget. |
lp.datasets() # ['budget', 'departments']
df = lp.table('budget') # the dataset's file as a DataFrame
df = pd.read_csv('/data/budget.csv') # the same file, read directly
v = await lp.sql('SELECT * FROM clean_budget') # views: only through lp.sql
Looking at your data without writing SQL
๐ Data Explorer โ switch to it at the top of the left rail, or click a dataset in the rail. It shows the whole table: search across every column, click a column header to sort (click again to reverse), and page through 25, 50 or 100 rows at a time, with the row count.
๐ Profile & Quality โ a tab in the right panel. For each column: its type, how much is missing and how many distinct values it has; for the table: duplicate rows, columns with 5% or more missing, and columns that never change. A good first stop before any cleaning.
Members do not upload their own files in the current version โ every project runs on curated data, which is what makes review and auto-checks possible. Have a dataset worth adding? Mail it to the address in the dataset library sidebar.
๐ค SQL guide
The dialect is DuckDB. If you learned MySQL or Postgres, 95% carries over โ the differences are listed at the end.
Selecting and filtering
SELECT category, budgeted_amount, actual_amount
FROM budget
WHERE actual_amount > budgeted_amount -- conditions can compare columns
ORDER BY actual_amount - budgeted_amount DESC
LIMIT 5;
Aggregating and grouping
SELECT category,
COUNT(*) AS line_items,
SUM(actual_amount) AS spent,
SUM(actual_amount) - SUM(budgeted_amount) AS variance
FROM budget
GROUP BY category
HAVING SUM(actual_amount) > 1000000 -- HAVING filters groups; WHERE filters rows
ORDER BY variance;
Dates
SELECT strftime(due_date, '%Y') AS year,
date_diff('day', due_date, DATE '2027-01-01') AS days_overdue
FROM receivables
WHERE due_date BETWEEN DATE '2022-01-01' AND DATE '2022-12-31';
Write date literals as DATE 'YYYY-MM-DD'. date_diff('day', a, b) counts from a to b; strftime(date, format) formats it.
Banding with CASE
SELECT CASE WHEN receivable_amount >= 30000 THEN 'large'
WHEN receivable_amount >= 10000 THEN 'medium'
ELSE 'small'
END AS size_band,
COUNT(*) AS invoices
FROM receivables
GROUP BY size_band; -- grouping by the alias works
Joins
SELECT b.category, d.dept_head, SUM(b.actual_amount) AS spent
FROM budget b
JOIN departments d ON b.category = d.category
GROUP BY b.category, d.dept_head
ORDER BY spent DESC;
-- LEFT JOIN keeps rows with no match (departments that spent nothing)
You can join any datasets attached to the brief and any views you have created โ as many as the question needs. Datasets that are not attached to the brief are not loaded, so they cannot be joined.
Subqueries and CTEs
WITH dept AS (
SELECT category, SUM(actual_amount) AS spent
FROM budget
GROUP BY category
)
SELECT category, spent,
ROUND(spent * 100.0 / (SELECT SUM(spent) FROM dept), 1) AS pct
FROM dept
ORDER BY spent DESC;
Views โ saving a query under a name
CREATE OR REPLACE VIEW clean_budget AS
SELECT * FROM budget WHERE actual_amount >= 0;
SELECT SUM(actual_amount) FROM clean_budget; -- reads through the view
Coming from MySQL? The differences that bite
| Habit | Here |
|---|---|
`backtick` quoting | Use "double quotes" for names with spaces or capitals, or that are SQL words: "Order Date", "order". |
LIMIT 10, 5 | LIMIT 5 OFFSET 10 |
DATE_SUB / DATE_ADD | date_diff(โฆ), d + INTERVAL 7 DAY |
IFNULL(x, y) | COALESCE(x, y) (works everywhere) |
Integer division 3/2 = 1 | 3/2 = 1.5 โ use // for integer division |
๐ Python guide
Real Python in the tab, with a tiny bridge to your data called lp.
| Call | Returns |
|---|---|
lp.datasets() | The list of attached dataset aliases. |
lp.table('budget') | That dataset's original file as a pandas DataFrame. It does not see your views โ use lp.sql for those. |
await lp.sql("SELECT โฆ") | Any SQL โ including your views โ run on DuckDB, back as a DataFrame. Note the await. |
lp.save(df, "clean_air") | Hands a DataFrame to SQL as a table with that name, so later SQL blocks, lp.sql and the task checks can read it. Saving again replaces it; a dataset alias cannot be used as the name. |
# Cross-check a SQL number independently in pandas
df = await lp.sql("SELECT * FROM invoices")
overdue = df[df.is_overdue]
print("overdue invoices:", len(overdue))
print(f"overdue amount: {overdue.receivable_amount.sum():,}")
Output rules, in order: everything you print appears as stdout; a DataFrame that is the last expression of the block renders as a table, index first, every cell as pandas prints it (first 50 rows, with the true total shown); a Series, a number or any other last value is shown the way Python writes it (its repr, as in Jupyter); matplotlib figures, pandas .plot() included, appear as images โ up to two plots per run. A trailing semicolon hides the last value. Errors show only the lines of your own code.
import matplotlib.pyplot as plt
cash = await lp.sql("""
SELECT cash_type, SUM(inflow) - SUM(outflow) AS net_cash
FROM cash_flow GROUP BY cash_type
""")
plt.bar(cash.cash_type, cash.net_cash)
plt.title("Net cash by activity")
plt.show()
Available out of the box: pandas, numpy, matplotlib, and the Python standard library. Not available, on purpose: network requests, pip installs, file system, input(), threads. If a script needs the internet, it belongs on your own machine โ the Workbench is for the analysis.
โถ๏ธ Running code & the pipeline
One block, the whole notebook, and the badge that proves it.
โถ Run โ executes one block (Ctrl/โ + Enter inside the editor does the same). The result is saved with the block, so your outputs are still there tomorrow.
โถโถ Run all โ executes every included SQL and Python block top to bottom and stops at the first error. If everything passes, the work earns a โ Reproducible stamp, shown on the published page โ proof the notebook runs clean start to finish, not just in the order you happened to click.
๐ฏ Checks โ tasks can carry an auto-check: run it and your current result is compared to the expected answer, pass or fail with the actual value shown. A check is either a SQL query (it sees your views and lp.save tables) or a Python expression on your variables, so run your notebook first. Reviewers see the same check summary.
Errors โ appear under the block, verbatim from the engine. Read them; they usually name the problem (an aggregate in WHERE, a missing table, a typo in a column). An error in a Run all halts the pipeline at that block so you always know where it broke.
๐ From Workbench to published work
The whole loop, end to end.
- Claim โ pick a brief; the Workbench opens with its datasets loaded and its tasks in the ๐ drawer.
- Work โ add note, SQL, and Python blocks; reorder freely; toggle any block out of the final piece. Included blocks become the README.
- Verify โ โถโถ Run all until green, run the ๐ฏ checks, cross-check a key number two ways.
- Submit โ your work enters the review queue. A human reviews it; changes may be requested with notes.
- Published โ the repo goes public on your portfolio, readable logged-out, with block-level comments โ and XP and coins land in your account.
๐ฌ Questions & answers
The ones every new member asks.