Getting started
This page assumes you know some TypeScript or JavaScript, but nothing about Jupyter or Deno. It covers what a notebook is, how to install cdp-kernel and run a first notebook, and a few kinds of work it suits.
Notebooks and kernels
A Jupyter notebook is a file (.ipynb) made of cells. A code cell holds a few lines of code; a Markdown cell holds prose. You run cells one at a time, and each cell's output (a value, a table, a chart, printed text) appears under it and is saved in the file. Variables, functions and imports from one cell stay available to the cells you run after it, so you build a program up a step at a time and look at the results as you go.
The program you open the notebook in, such as JupyterLab or VS Code, is the frontend. It doesn't run your code itself. It sends each cell to a kernel, a separate process for one language, and shows what comes back. Jupyter started with Python, but kernels exist for many languages.
Deno, a JavaScript runtime, ships a kernel for TypeScript (deno jupyter). cdp-kernel is that kernel taken out of Deno so that notebook code can run on Node, Bun and other runtimes as well as Deno. You don't need Deno installed, or any knowledge of it, to use cdp-kernel with Node. Where Deno's notebook docs or blog posts show Deno.jupyter, cdp-kernel offers the same API as a global named jupyter.
Install
You need two things: a Jupyter frontend and the kernel.
A frontend. Either of these works:
- JupyterLab, the browser-based editor. It's a Python program; install it with
pipx install jupyterlaboruv tool install jupyterlab(orpip install jupyterlabin a virtual environment). You don't write any Python. - VS Code with the Jupyter extension. VS Code still needs Jupyter's kernel registry, which the
cdp-kernel installstep below writes to.
The kernel. With Node 24 or later:
npm install -g @tlockney/cdp-kernel
cdp-kernel install --runtime nodeinstall registers a kernel named cdp-node, shown in menus as "TypeScript (cdp-kernel, node)". Check that Jupyter sees it:
jupyter kernelspec listTo run notebooks on Deno, Bun, workerd or celld instead, install a kernel for each (--runtime deno and so on). Install covers those, installing from JSR with Deno, and what each runtime needs. Linux and macOS are supported; Windows isn't yet.
Your first notebook
Start JupyterLab in the directory you want to work in:
mkdir scratch && cd scratch
jupyter labIn the launcher, pick TypeScript (cdp-kernel, node) under Notebook. In VS Code, create a file ending in .ipynb, click Select Kernel, and pick the same name under Jupyter Kernels.
Type into the first cell and press Shift-Enter to run it:
const greeting: string = "Hello";
greeting + ", notebook"'Hello, notebook'The cell's last expression is shown as its result: 'Hello, notebook'. Then, in the next cell:
console.log(greeting.length);
await new Promise((resolve) => setTimeout(resolve, 500));
jupyter.md`**${greeting}** from a _Markdown_ result`5Hello from a Markdown result
greeting is still defined, console.log prints under the cell, await works at the top level, and jupyter.md renders Markdown instead of a string. A few things worth knowing early:
- Order matters, not position. State comes from the cells you ran, in the order you ran them. The number beside a cell, like
[3], is its run order. If a notebook gets confusing, use Kernel › Restart Kernel and Run All Cells to rerun it from the top. - Imports work as in a module. Import from
node:builtins, from anode_modulesnext to the notebook, from local files (./util.ts), or straight from npm without an install step:import { z } from "npm:zod@3". The package downloads on first use. - Interrupt (the ■ button) stops a long cell and keeps your variables.
- Shift-Tab on a name shows what it is: its type, a function's signature, a preview of its value.
- Rich output.
jupyter.md,jupyter.html,jupyter.svgandjupyter.imagerender their content, andjupyter.display(...)shows something without ending the cell. See Writing notebooks for the full API.
Save the notebook and its outputs are saved with it, so someone opening it later (or on GitHub) sees the results without running anything.
What to use it for
A notebook is a good fit when you want to see intermediate results, keep a record of what you did, or mix explanation with code. Some examples:
Analysing data
Load a file once, then ask it questions cell by cell without reloading. With orders.csv next to the notebook (sample data):
import { readFile } from "node:fs/promises";
const [header, ...rows] = (await readFile("orders.csv", "utf8")).trim().split("\n");
const orders = rows.map((line) => {
const [date, region, amount] = line.split(",");
return { date, region, amount: Number(amount) };
});
orders.length10const byRegion = Object.groupBy(orders, (o) => o.region);
const totals = Object.entries(byRegion).map(([region, list]) => ({
region,
orders: list!.length,
total: list!.reduce((sum, o) => sum + o.amount, 0),
}));
console.table(totals);┌─────────┬─────────┬────────┬────────┐ │ (index) │ region │ orders │ total │ ├─────────┼─────────┼────────┼────────┤ │ 0 │ 'north' │ 3 │ 367.6 │ │ 1 │ 'south' │ 3 │ 182.15 │ │ 2 │ 'east' │ 2 │ 406.4 │ │ 3 │ 'west' │ 2 │ 196.8 │ └─────────┴─────────┴────────┴────────┘
Each new question is a new cell that reuses orders. For larger data, import a dataframe library such as npm:nodejs-polars or npm:arquero, and draw charts with npm:@observablehq/plot. The MoMA collection notebook is a worked example with both.
Runbooks
A runbook is a procedure written down so anyone on a team can follow it: check this, then do that. As a notebook, the explanation sits in Markdown cells and each step is a cell you run, with its output kept as a record of what happened.
const services = {
api: "https://api.example.com/health",
web: "https://www.example.com/health",
};
for (const [name, url] of Object.entries(services)) {
const res = await fetch(url).catch((e) => e as Error);
console.log(name, res instanceof Error ? `unreachable: ${res.message}` : res.status);
}prompt() and confirm() ask the person running the notebook for input, which suits steps that shouldn't run without a decision:
import { execFileSync } from "node:child_process";
if (confirm("Restart the worker queue?")) {
console.log(execFileSync("systemctl", ["restart", "worker-queue"], { encoding: "utf8" }));
}Save the notebook after a run and it doubles as the incident log.
Exploring an API
When you're learning an HTTP API or a library, a notebook lets you make one call, look at the response, and write the next call against what you saw.
const repo = await (await fetch("https://api.github.com/repos/tlockney/cdp-kernel")).json();
Object.keys(repo)[ 'id', 'node_id', 'name', 'full_name', 'private', 'owner', 'html_url', 'description', 'fork', 'url', 'forks_url', 'keys_url', 'collaborators_url', 'teams_url', 'hooks_url', 'issue_events_url', 'events_url', 'assignees_url', 'branches_url', 'tags_url', 'blobs_url', 'git_tags_url', 'git_refs_url', 'trees_url', 'statuses_url', 'languages_url', 'stargazers_url', 'contributors_url', 'subscribers_url', 'subscription_url', 'commits_url', 'git_commits_url', 'comments_url', 'issue_comment_url', 'contents_url', 'compare_url', 'merges_url', 'archive_url', 'downloads_url', 'issues_url', 'pulls_url', 'milestones_url', 'notifications_url', 'labels_url', 'releases_url', 'deployments_url', 'created_at', 'updated_at', 'pushed_at', 'git_url', 'ssh_url', 'clone_url', 'svn_url', 'homepage', 'size', 'stargazers_count', 'watchers_count', 'language', 'has_issues', 'has_projects', 'has_downloads', 'has_wiki', 'has_pages', 'has_discussions', 'forks_count', 'mirror_url', 'archived', 'disabled', 'open_issues_count', 'license', 'allow_forking', 'is_template', 'web_commit_signoff_required', 'has_pull_requests', 'pull_request_creation_policy', 'topics', 'visibility', 'forks', 'open_issues', 'watchers', 'default_branch', 'permissions', 'allow_squash_merge', 'allow_merge_commit', 'allow_rebase_merge', 'allow_auto_merge', 'delete_branch_on_merge', 'allow_update_branch', 'use_squash_pr_title_as_default', 'squash_merge_commit_message', 'squash_merge_commit_title', 'merge_commit_message', 'merge_commit_title', 'network_count', 'subscribers_count' ]
const { full_name, stargazers_count, open_issues_count, license } = repo;
({ full_name, stargazers_count, open_issues_count, license: license?.name }){
full_name: 'tlockney/cdp-kernel',
stargazers_count: 0,
open_issues_count: 8,
license: 'Other'
}The response stays in repo, so you don't call the API again for each question. Press Shift-Tab on repo to see its properties, or on a library function to see its signature before calling it.
Experiments
Notebooks keep the code, the numbers and your notes together, which makes them a good place for a quick measurement:
const data = Array.from({ length: 1_000_000 }, (_, i) => i);
function time(label: string, fn: () => unknown) {
const start = performance.now();
fn();
console.log(label, (performance.now() - start).toFixed(1), "ms");
}
time("for loop", () => { let s = 0; for (let i = 0; i < data.length; i++) s += data[i]; return s; });
time("reduce", () => data.reduce((s, x) => s + x, 0));for loop 5.3 ms reduce 13.5 ms
cdp-kernel runs the same notebook on different runtimes. Install the deno and bun kernels too, then switch with Kernel › Change Kernel and rerun to compare. Runtimes and Known gaps list where the runtimes behave differently.
Trying out syntax or a library
A notebook is a quicker place than a scratch project to check how a language feature behaves or what a package does. Cells accept TypeScript, with no tsconfig.json or build step:
type Point = { x: number; y: number };
const origin = { x: 0, y: 0 } satisfies Point;
enum Direction { Up = "UP", Down = "DOWN" }
[origin, Direction.Up, [3, 1, 2].toSorted()][ { x: 0, y: 0 }, 'UP', [ 1, 2, 3 ] ]import { z } from "npm:zod@3";
const User = z.object({ name: z.string(), age: z.number().int() });
User.safeParse({ name: "Ada", age: 36.5 }).error?.issues[
{
code: 'invalid_type',
expected: 'integer',
received: 'float',
message: 'Expected integer, received float',
path: [ 'age' ]
}
]Cells aren't type-checked, as in Deno: types are removed and the code runs. For type errors in your editor, see Editor types.
Next steps
- Writing notebooks: the display API, imports, pinning package versions with a lockfile, and Deno projects.
- Runtimes: when to use Bun, workerd or celld, and how they differ.
- Debugger: breakpoints in JupyterLab on Node and Deno.
- Widgets and comms: interactive output.
- Known gaps: what doesn't work yet.