Sign inCreate account

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 typeEngineWhat that means
SQLDuckDB (WebAssembly)A real analytical database in the tab. Loads with the page; queries run instantly on the attached datasets.
PythonPyodide 314 โ€” Python 3.14 in WebAssemblyReal 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.
๐Ÿ”’ Privacy consequence: your queries and your work never leave the browser. The server stores your blocks and their saved outputs โ€” the execution itself is entirely yours.

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.

KeysWhat they do
Ctrl/โŒ˜ + EnterRun this block.
Ctrl + SpaceShow suggestions now.
โ†‘ โ†“ ยท Enter / TabMove through the suggestions ยท take the highlighted one.
Tab ยท Shift + TabIndent ยท outdent (when no suggestion is open).
EscClose the suggestions. Esc then Tab moves on to the next control, for keyboard-only use.
Ctrl/โŒ˜ + FFind (and replace) in this block.
Ctrl/โŒ˜ + ZUndo (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:

  1. 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.
  2. 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.
  3. 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.
sql
-- The aliases in the left rail ARE your tables. First look:
SELECT * FROM budget LIMIT 5;
๐Ÿ“„ Each dataset is one table. If a dataset in the library holds several files, only its first file is loaded for now โ€” so briefs that join tables attach each table as its own dataset.

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:

sql
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 wantWrite
A datasetIts alias: FROM budget. Case does not matter โ€” Budget works too.
A short name inside one queryFROM budget b (or budget AS b), then b.category.
A column from one particular tablebudget.category or b.category โ€” needed in a join when both tables have a column with that name.
A column whose name has spaces or capitalsDouble quotes: "Order Date". Column names come from the file's header row as they are.
An alias that is also an SQL wordDouble quotes: FROM "order".
A view you createdIts name, like any table: FROM clean_budget.
python
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
๐Ÿ” An alias belongs to the brief: the same library dataset can have a different alias on another brief. When you reuse code from another project, check the names in the left rail first.

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

sql
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

sql
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;
โš ๏ธ WHERE cannot see aggregates โ€” it runs before the groups exist. Filtering on an AVG or SUM belongs in HAVING. Getting this wrong produces a Binder Error, and that error is correct.

Dates

sql
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

sql
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

sql
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

sql
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

sql
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
๐Ÿ” Views live in the browser session. After a page refresh the database is rebuilt from the datasets, so run the blocks that create your views first โ€” or simply press โ–ถโ–ถ Run all, which replays everything in order.

Coming from MySQL? The differences that bite

HabitHere
`backtick` quotingUse "double quotes" for names with spaces or capitals, or that are SQL words: "Order Date", "order".
LIMIT 10, 5LIMIT 5 OFFSET 10
DATE_SUB / DATE_ADDdate_diff(โ€ฆ), d + INTERVAL 7 DAY
IFNULL(x, y)COALESCE(x, y) (works everywhere)
Integer division 3/2 = 13/2 = 1.5 โ€” use // for integer division

๐Ÿ Python guide

Real Python in the tab, with a tiny bridge to your data called lp.

CallReturns
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.
python
# 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.

python
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.

  1. Claim โ€” pick a brief; the Workbench opens with its datasets loaded and its tasks in the ๐Ÿ“‹ drawer.
  2. Work โ€” add note, SQL, and Python blocks; reorder freely; toggle any block out of the final piece. Included blocks become the README.
  3. Verify โ€” โ–ถโ–ถ Run all until green, run the ๐ŸŽฏ checks, cross-check a key number two ways.
  4. Submit โ€” your work enters the review queue. A human reviews it; changes may be requested with notes.
  5. 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.

Why is my first Python run slow?
The Python runtime (about 22 MB, plus about 5 MB the first time you draw a chart) downloads on your first โ–ถ Run of a Python block, then it is cached by the browser. SQL never has this wait โ€” DuckDB loads with the page. Later runs are instant.
Do my views and results survive a refresh?
Saved block outputs survive โ€” they are stored with your work. The in-browser database is rebuilt on refresh, so views (CREATE OR REPLACE VIEW) exist again only after the block that creates them runs. Habit worth forming: after a refresh, press โ–ถโ–ถ Run all once.
My query worked in MySQL but errors here. Why?
The Workbench speaks DuckDB. The usual culprits: backtick quoting, the LIMIT x,y form, and MySQL date functions. The conversion table in the SQL guide above covers all of them โ€” the fix is typically one line.
Where does my code actually run? Who can see my data?
In your browser tab, entirely. Datasets are fetched into the tab and queried there; code is never executed on our servers. What we store is your notebook โ€” the blocks and the outputs you saved with them.
Can I upload my own dataset?
Not yet โ€” projects run on curated datasets so that review and auto-checks stay meaningful. You can propose one: mail it via the link in the dataset library sidebar, and credited curated additions do happen.
Can I use requests / install a package with pip?
No. The Python sandbox has no network and no installer, by design โ€” it keeps every published notebook runnable by every reader forever, with nothing to break or leak. pandas, numpy, and matplotlib cover the analysis work the platform teaches.
What does the โ›“ Reproducible badge mean exactly?
That the last โ–ถโ–ถ Run all executed every included code block, top to bottom, with zero errors and nothing skipped. It is stamped automatically โ€” there is no way to hand-award it โ€” which is why reviewers and readers trust it.
How are the ๐ŸŽฏ checks graded?
A check runs a query (or, for Python tasks, an expression) against your current session and compares the first number it returns to the range the brief author set โ€” a minimum, a maximum, or both (an exact answer is a range with the same minimum and maximum). You see pass or fail plus your actual value, and the reviewer sees the same summary. No AI grading is involved.
Why do I only see some rows of my result?
Result tables show the first 50 rows and tell you the true total. The query itself always runs on all the data โ€” add ORDER BY and LIMIT to pull exactly the slice you want to look at, or browse a whole table in the Data Explorer.
Something says the SQL engine failed to load. What do I do?
The left rail names the problem when an engine cannot start โ€” usually a network block between your browser and the script CDN, or a brief whose dataset has no file attached. Try a refresh first; if it persists, the message on the page says exactly what to report to support.
What name do I use for a dataset in my query?
Its alias on this brief โ€” shown in the left rail and on the brief page โ€” not its title in the library. SHOW TABLES lists them all. In Python, lp.table('alias') or pd.read_csv('/data/alias.csv'). See "Referring to a dataset by name" above.
Can I join two datasets?
Yes โ€” every dataset attached to the brief is a table, and JOIN, LEFT JOIN, several joins in one query, USING and WITH all work, on the tables and on your own views. Only attached datasets are loaded, and a dataset with several files loads only its first one (see Getting data in).
My view is not in the suggestions. Why?
Suggestions come from what exists in the in-browser database right now. A view appears once the block that creates it has run โ€” and after a refresh, once โ–ถโ–ถ Run all (or that block) has run again.
Who can see my draft before I publish?
Only you โ€” and reviewers, after you submit. Published repos are public by design; drafts never are.
Is there AI grading or an AI assistant?
Review is human and checks are deterministic. An AI assistant panel exists in the product design but is switched off until it works properly โ€” pages never pretend otherwise. When it ships, this page will document it.