# The Observable Runtime computes variables on demand, and that is the whole point of it

> A small ISC licensed JavaScript library that implements the reactive dataflow behind Observable Notebooks. Its defining behaviour is that a variable without an observer is not evaluated at all unless something downstream needs it, which is what makes a notebook feel instant.

**observablehq/runtime** — The reactive dataflow runtime that powers Observable Notebooks

- Repository: https://github.com/observablehq/runtime
- Website: https://observablehq.com
- Stars: 1,099 · Forks: 87
- Language: JavaScript
- License: ISC
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/observablehq-runtime

## Three objects, and the graph is implicit

The Observable Runtime implements reactivity in Observable Notebooks, and its whole API is three kinds of object.

A `Runtime` is the container. It is created with optional builtins and an optional global function, and it owns the modules attached to it. A `Module` is a namespace for variables, and within a module variables should typically have unique names. A `Variable` is a piece of state in a reactive program, and the documentation describes it as akin to a cell.

The dataflow is implicit in the names, not in the wiring. When you define a variable, you declare the names it depends on, and the runtime resolves the graph.

```js
const module0 = runtime.module();
module0.variable().define("a", 1);
module0.variable().define("b", 2);
module0.variable().define("c", ["a", "b"], (a, b) => a + b);
```

There is no subscribe call, no dispatch, no action type. `c` depends on `a` and `b` because its inputs array says so, and the runtime recomputes it when either changes.

The runtime is also where builtins live. If `builtins` is specified, each property on the builtins object defines a builtin variable for the runtime, and those are available as named inputs to any defined variables on any module associated with the runtime.

```js
const runtime = new Runtime({color: "red"});
```

There is a `global` escape hatch too. If a global function is specified, it will be invoked with the name of any unresolved reference, and must return the corresponding value or undefined to trigger a ReferenceError. If `global` is not specified, unresolved values are resolved from the global window.

One constraint on builtins is worth knowing before you design around them. Unlike variables, builtins cannot depend on the value of other variables or builtins, because they are defined with no inputs. If a builtin is defined as a function it is invoked lazily to determine the value, so to have a builtin's value be a function you have to define it either as a promise that resolves to a function or as a function that returns a function, and builtins may also be defined as generators for dynamic values.

## Nothing is computed until something needs it

This is the behaviour that makes the runtime feel the way it does, and it is easy to miss in the API description.

A variable without an associated observer is only computed if any transitive output of the variable has an observer. Variables are computed on an as-needed basis for display.

So the runtime is not an eager graph that evaluates everything on every change. It walks the dependency graph backwards from the variables that are actually being watched, and evaluates only what is on the path to them. The documentation says this is particularly useful when the runtime has multiple modules, as with imports, because only the needed variables from imported modules are computed.

For a notebook where a cell might reference a data file that takes a second to load, this is the difference between a page that responds immediately and one that blocks on the slowest branch. It also means the cost of a cell is proportional to what it feeds, not to what it references.

The observer is where you attach to the graph. The `module.variable(observer)` method takes an optional observer, and the observer is notified when the variable changes state through its `pending`, `fulfilled` and `rejected` methods. The documentation points at the standard inspector as a convenient default observer implementation, which is a separate repository, observablehq/inspector.

That three-method contract, pending, fulfilled and rejected, is the whole surface for handling an asynchronous value. A variable's definition can return a promise, and the runtime reports the pending state, then the resolved or rejected one. Anything more elaborate, like a cancelled in-flight computation, is not in the documented API.

The runtime also has a `dispose()` method, which disposes the runtime, invalidating all active variables and disabling future computation. That is the shutdown path, and for a long-lived page that swaps views it is the method you need to call, because an observer that is never detached keeps its whole dependency chain alive.

## Derive is how you override a value without mutating its origin

The `module.derive` method is the least obvious part of the API and the most useful.

It returns a derived copy of the module, where each variable in `specifiers` is replaced by an import from the specified source module. Each specifier is an object with a `name`, which is the name of the variable to import from source, and an `alias`, which is the name of the variable to redefine in this module, and if the alias is not specified it defaults to the name. A specifier may also be given as a string, in which case the string is treated as both the name and the alias.

The example makes the semantics clear.

```js
const module1 = runtime.module();
const module1_0 = module0.derive(["b"], module1);
module1.variable().define("b", 3);
module1.variable().import("c", module1_0);
```

The value of `c` in the derived module is now 1 + 3 = 4, whereas the value of `c` in the original module remains 1 + 2 = 3.

So `derive` produces a copy of a module with some of its inputs replaced, and that copy can then be used as a source for further imports. This is how a notebook overrides one input of a chart or a table without redefining the module that produced it, and it is the mechanism behind the import with an override pattern in Observable's own code.

The rest of the module API is a set of conveniences over the variable API. `module.define` is equivalent to `module.variable().define`, `module.import` is equivalent to `module.variable().import`, and `module.builtin` defines a built-in constant visible to all variables in the module, with the caution that any built-ins must be defined before variables are defined and must not be redefined afterwards. `module.redefine` replaces a named variable and throws a runtime error if no such variable exists or if more than one has that name, and `module.value` returns a promise to the next value of a named variable with the same error behaviour.

The error on ambiguity is worth noting, because it is the runtime protecting you from a class of bug that is otherwise very hard to see: a module where two variables share a name, and the one that gets recomputed is whichever the runtime found first.

## A small package with no runtime dependencies

The package metadata is short and says something about the design.

The package is `@observablehq/runtime`, the version is 6.0.1, the licence is ISC, and the author is Observable, Inc. The package is marked as an ES module, with `main` and `module` both pointing at `src/index.js` and the export map exposing a single default entry point. The published `files` array is just `src/**/*.js`.

There are no runtime dependencies at all. The only devDependencies are eslint, mocha and module-alias, and the test script is `mocha 'test/**/*-test.js' && eslint src test`, so the linting runs as part of the test command rather than as a separate step.

The module-alias dependency is there for a specific reason, and the manifest shows it. There is a `_moduleAliases` entry mapping `@observablehq/runtime` to `./src/index.js`, which is how the test suite imports the package by its published name while still running against the source tree.

The repository layout is minimal in the same way. There is a `src/` directory, a `test/` directory, an `.eslintrc.json`, a `pnpm-lock.yaml` and a CONTRIBUTING.md, with the pnpm lock file indicating pnpm as the package manager for development.

The release history shows a major version bump and then a long gap. v6.0.0 was published on 2024-11-06, and before that v5.9.9 from 2024-08-06, with v5.9.9 having been the tail of a long 5.x line. v6.0.1 came on 2026-09-16, and the last push to the repository was on 2026-09-16 as well, with the repository not archived.

So the library is a small, dependency free core that has been stable across a major version, with a recent patch release. For something that other people's notebooks depend on, that is the profile you want. The risk is not the code, it is the coupling to the notebook platform's own version, which is what the major version number is really tracking.

## Where this fits, and where it does not

It is worth being clear about the boundary, because the runtime is often mistaken for a state management library.

What it is: a reactive dataflow engine where dependencies are declared by name, evaluation is pull-based from observed variables, and asynchronous values are first class through the pending, fulfilled and rejected observer methods. That model is a good fit for a notebook, where the user is writing a dataflow program and the interface is a set of cells whose dependencies are implicit in the text.

What it is not: a store with actions and reducers, a component model, or a virtual DOM. There is no rendering in this package at all, the interface is entirely the observer you supply. That is why the standard inspector lives in a separate repository, observablehq/inspector, and why the runtime can be used to drive a canvas, a terminal, or a server-side computation just as well as a DOM.

The practical difference from a conventional store shows up in two places. First, evaluation order is not something you control, the runtime decides what to recompute based on the graph and what is observed. Second, a variable with no observer and no observed downstream is never evaluated at all, which is a behaviour that a store does not have and that you have to design for if you are porting a reducer to it.

The builtins mechanism is the third consideration. They are the injection point for platform services, which is exactly what a notebook needs for things like file attachments, and the documentation gives the `FileAttachment` example. If your application has a similar need, a handful of builtins is the clean way to give every cell access to it, and the constraint that builtins cannot depend on other variables is a small price for that.

For anyone building an Observable-like tool, this package is the part you would use rather than reimplement. The parts you would write yourself are the cell interface, the inspector, and whatever the notebook editor does with the dependency graph.

## Conclusion

Adopt the Observable Runtime if you are building a reactive JavaScript interface where a dataflow graph is the right model, for example a notebook-style tool or a dashboard with derived state. Do not adopt it as a general state manager for a conventional application, because its model is pull-based evaluation from observed variables, which is more machinery than a typical store needs. Verify first that the version you use matches the notebooks you want to support, since the major version moved from 5 to 6, and that you read the disposal semantics, because a runtime that is never disposed keeps its observers alive.

## FAQ

### What is the Observable Runtime?

It is the reactive dataflow runtime that powers Observable Notebooks, published as the @observablehq/runtime npm package under the ISC licence. It provides the Runtime, Module and Variable objects that make up a reactive program.

### When does the Observable Runtime actually compute a variable?

A variable without an associated observer is only computed if any transitive output of the variable has an observer. Variables are computed on an as-needed basis for display, which is especially useful with imports across multiple modules.

### What does module.derive do in the Observable Runtime?

It returns a derived copy of a module where each specifier is replaced by an import from a source module, with an optional alias. The copy can then be used as the source for further imports, which is how one input is overridden without mutating the original module.

### What are builtins in the Observable Runtime?

Builtins are named inputs available to any defined variables on any module of a runtime, and they are set when the Runtime is constructed. Unlike variables they cannot depend on other variables, and a builtin defined as a function is invoked lazily, so a function value must be defined as a promise resolving to a function or a function returning a function.

### What are the dependencies of the Observable Runtime package?

None at runtime. The package ships only src JavaScript files and has no runtime dependencies, with eslint, mocha and module-alias as the only dev dependencies. The current version is 6.0.1, released on 2026-09-16.

## Sources

- [License: ISC](https://github.com/observablehq/runtime/blob/main/LICENSE)
- [observablehq/runtime on GitHub](https://github.com/observablehq/runtime)
- [Project website](https://observablehq.com)
- [README](https://github.com/observablehq/runtime/blob/main/README.md)
- [Releases](https://github.com/observablehq/runtime/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/observablehq-runtime
