Workbooks
Preface
Literate ProgrammingWhy WebAssembly
Introduction
What is a workbookWhat is a .work fileWhat is the NexusWhat is the work CLIWhat is Workbooks Cloud
The .work language
Anatomy of a .work fileBlock grammarThe prose lane & refsDeclarationsPlacement & languagesKinds referenceElixir in Markdown
Capabilities & sandboxing
Sandboxing and the DockGrants and capabilitiesThe three walls
Data & persistence
Resources and the Store
Serving & routing
Routes and pagesSite mode
Running & deploying
Running the NexusDeploy: local and cloudDeploy as an index treeSecrets & configurationWorkbooks Cloud
Agents & tooling
Get started with your CLI agentAuthoring an agentToolkits
Architecture
One frontend, many targetsRuntime connect (RCP)
Examples
Learn by example
Blog
Why we author these docs as a workbookOne nexus, many sites

Literate Programming

Most code is written for the machine first and the human second. The machine gets a precise list of instructions; the human gets some comments, a README that went stale months ago, and a wiki nobody opens. Workbooks start from the opposite idea: write for the human first, and let the machine read the same document. That idea is old, it has a name, and it turns out to be exactly what you want in a world where an AI agent is reading and writing your code alongside you.

Knuth

In 1984 Donald Knuth described literate programming: a program should be a piece of literature. You write an explanation a person can follow — in plain prose, in the order that makes sense to a reader — and the runnable code lives right there inside the explanation. The prose and the code are not two artifacts that drift apart. They are one document. The compiler pulls the code out; the reader follows the prose.

Knuth's point was that a program is mostly read, not written, so the writing should be aimed at the reader. A .work file is a literate document in exactly this sense: paragraphs narrate, and do … end blocks run.

The agent turn

Here is what changed. For forty years "the reader" meant another developer. Now the reader is also an agent — and the agent doesn't just read your code, it writes it.

When a machine is editing your codebase, the prose stops being decoration. It becomes the shared context that you and the agent both work from: what this is for, what must stay true, what to leave alone. A literate document co-locates the three things that usually live in three different places —

  • intent (the prose: what you're trying to do and why),
  • execution (the code: what actually runs),
  • review (both at once: you read the reasoning next to the change).

An agent can read the intent, make the change, and you can verify it without hopping between a ticket, a wiki, and a diff. The document is the program is the spec.

What agent harnesses get wrong

Two failure modes show up the moment you point an agent at a real codebase.

Detached docs. The conventional wisdom says code and documentation should live apart — code in the repo, docs in a wiki, design notes in a folder of Markdown files. With a human team you can just barely keep those in sync. With an agent you cannot. The agent reads the code and the scattered docs as separate, often contradictory sources, and every gap between them is a place for the code to go fuzzy. The more you separate the explanation from the thing it explains, the worse the agent does. (We feel this so strongly that the first thing we did when writing these docs was delete a pile of stale Markdown that no longer matched the code — detached docs are a liability, not an asset.)

Lines-of-code buildup. Agents are eager. Ask for a change and you tend to get more code — another file, another layer, another abstraction "to be safe." Do that for a few weeks and you have a thirty-thousand-line codebase where nobody, human or machine, can hold the whole picture, and every edit risks drift somewhere far away. The fix is not a bigger context window. The fix is to force simplification: let the agent write its reasoning next to its code, and a simpler path almost always appears. Prose is where you notice that three functions are really one.

Why Elixir

If you're going to be opinionated about writing less, simpler code, you should be opinionated about the language too. We chose Elixir.

Elixir is easy to read. You can hand a block of it to someone who has never seen it and they'll mostly follow along — it reads close to plain instructions. It's easy to learn and genuinely capable as a server-side language. We picked it for two concrete reasons: its concurrency model (lightweight isolated processes, the BEAM) is a perfect fit for running many workbooks and agents side by side, and it fit the architecture we needed rather than fighting it.

And because a .work block can be written in Elixir, you get the full language — this really is Elixir, written inside a literate document:

def elixir :hello
def greet :hello do
  def run(name), do: "hello, #{name}"
end

We were inspired here by org-mode and MDX — both wonderful ways to mix prose and live content. We wanted to go further: deeper language concepts, and the ability to manage real parts of the codebase directly from these blocks, not just illustrate them.

Radical simplicity

There's a piece we keep coming back to, Radical Simplicity. Its argument: developers are happiest, and software is best, when you stop drowning in glue code and framework churn and get back to solving the actual problem — with one database, one language, as little machinery as possible.

That's the same instinct, pointed at the agent era. Microservices and serverless sprawl don't just cost money at scale; they give an agent a hundred moving parts to keep consistent. Collapse the stack — one engine, one language, the document and its code in one file — and you give both the human and the agent something small enough to actually understand. Literate programming is the writing style. Radical simplicity is the discipline. Together they're how you keep an agent-built codebase from turning into the thing everyone's afraid of.

Why WebAssembly

A literate document that mixes languages needs a common ground to run the foreign-language blocks on — one target they can share. That target is WebAssembly. It's also the thing that lets us run many agents safely at once. Two properties, both load-bearing.

One target for foreign-language blocks

If the promise is "write a block in the language that fits, in one document," you need a common target the foreign-language blocks can share. WebAssembly is exactly that: a small, portable instruction format. Today Rust, C, C++, Zig, and Swift compile down to it. Instead of gluing a dozen runtimes together, those blocks compile to the same thing and run in the same place.

Not everything goes through WASM, by design. Elixir server units run natively on the BEAM — they don't compile to wasm. And a client block's body is emitted into the page as a browser island, which is its own lane. WASM is the target for the foreign-language blocks; the BEAM and the browser-island lane handle the rest.

WASI — the WebAssembly System Interface — extends that with a standard, portable way for those modules to touch the outside world: files, clocks, the network. One interface for the languages that compile to it. That's what makes the .work promise of "the language that fits" actually buildable.

Isolation is the other half

WebAssembly modules run sealed off — from each other and from the host — by default. A module can't reach memory, files, or network it wasn't explicitly handed. That isn't a nice-to-have here; it's what lets the Nexus run many agents and tenants side by side. Each agent's code runs in its own sandbox, granted only the capabilities it needs.

Sandbox per request, not per second

This changes the economics of running agents. The usual approach keeps a Linux container or VM running per agent, billed per second — even while it does the same operation over and over. WebAssembly sandboxes are cheap enough to spin up per request: instantiate one, do the work, throw it away. For agent workloads — often the same operation many times — that's dramatically cheaper and scales to far more concurrent agents than keeping warm Linux boxes around. It's the same scale-by-RAM story, applied to agents.

The honest caveat: speed

WebAssembly's tradeoff is build time. To run a block it must be compiled to WASM first. So you either pre-build your packages, or — for things compiled dynamically at run time — cache the result after the first run and reuse it. In practice you acknowledge what your users and agents actually do, pre-warm or cache those paths, and the first-run cost is paid once. The only real caveat is speed, and it's addressable: caching, a build queue, and languages that adopt WASM well. Zig has been the smoothest of the bunch.

Standing on others' work

We don't run our own engine — workbooks run on wasmtime (via wasmex, its Elixir binding), from the Bytecode Alliance. The portable system interface is WASI, stewarded by the Bytecode Alliance and the W3C. And the wider ecosystem — the people behind WASIX at Wasmer, and the broader WebAssembly community — is what makes "every language, one interface" practical at all. Workbooks is an integration of that work, not a reinvention of it. Credit where it's due.

What is a workbook

A workbook is the woven artifact — the single, self-contained thing you ship and run. You author a folder of .work files; the work CLI weaves that folder into one workbook, and a Nexus runs it (or a browser opens it). The folder is the source; the workbook is the product.

Source vs. workbook

It's worth keeping the two apart, because they're easy to blur:

  • The source — a folder of .work files (plus any assets). This is what you write, read, and version: a tree of literate documents, prose and do … end blocks side by side. It's the authoring surface, not the thing that runs.
  • The workbook — what work weave compiles that source into: one shippable, self-contained artifact. This is the end product — the thing a Nexus serves and a browser can open.

The relationship is the familiar one between source and build: you edit a tree of files; a build step folds them into the thing you actually ship. Here the build step is the weave, and its output is the workbook.

How it's woven

You never assemble a workbook by hand — the CLI does it from the source folder:

work weave <dir> <out>   # fold the .work tree into one workbook
work check <dir>         # resolve refs + audit what each block may do
work dev <dir>           # watch the folder and re-weave as you edit

work weave reads the whole tree, resolves the links between files, and emits a single artifact. work dev does the same continuously, re-weaving on every save and hot-swapping the result into a running Nexus, so the live workbook tracks your edits.

That artifact is HTML — but HTML here is purely a build output, the format the weave emits because it runs everywhere. You don't author or configure it and you never hand-write it. You write .work; the weave produces the workbook.

What you put in the source

Everything you write in the source .work files falls into one of four lanes — you'll meet them properly in Anatomy of a .work file:

  • Prose — the rich text that narrates and carries links between ideas.
  • Declaration — structure with no body: a table, an enum, a record.
  • Code — a runnable block: a function, a server unit, a browser island.
  • Placement — the first word of a block says where it runs: in the browser (client), on the engine (server), or in a capability sandbox (sandbox).

One file can hold all four. That's the point — the thing people see, the code that makes it work, and the data it stands on don't live in three repositories. They sit together in the source, and the weave carries them into one workbook.

Why a single artifact

Because a workbook is one woven thing, it moves the way a finished document moves: you deploy it to a Nexus, open it in a browser, or hand it to someone — and they get the whole piece of software at once, interface and logic and data shape together. The source stays legible for authoring and review; the workbook is the compiled result that actually runs.

Kits and apps

Most workbooks are one of two things. A kit is meant to be imported and composed by other workbooks — a shared piece other things build on. An app is the leaf you launch — the thing with an interface you actually open. A workbook can be both: an app that also exports a kit for the next one to use.

You don't have to decide this up front. Write a .work file, weave the folder, and you have a workbook — head to What is a .work file to see what goes in the source.

What is a .work file

A .work file is a literate document: plain prose with runnable blocks mixed in. The prose explains; the blocks run. It is not a new programming language — it's a way to write literate source in the languages you already use, with the explanation living right next to the code.

Prose narrates, work blocks run

Open any .work file and you'll see two things. Prose — ordinary paragraphs that explain what's going on, aimed at the reader. And work blocks — the runnable pieces. A work block opens with a header and ends with end:

server elixir :hello
server greet :hello do
  def run(name), do: "hello, #{name}"
end

That's the whole format: prose for the reader, work blocks for the machine. Every work block header reads the same way — <kind> [lang] :name … do … end — where the first word, the kind, says what the block is and where it runs. Three kinds carry most of the work, and each does something distinct.

server — runs on the Nexus

A server block runs on the Nexus, on the BEAM. It's your backend: logic, data access, anything that needs to happen on the engine. Here it's Elixir:

server elixir :place
server orders :place do
  def run(order), do: Store.create(Order, order)
end

This is where persistence and trusted work live — the server holds the powers a browser shouldn't.

client — runs in the browser

A client block is a browser island. Its body is the interface, rendered into the page and run in the browser. This is the part people see and touch — here's a real one, running right now:

And that button is the source above — no build step you wrote, no separate file. Here is the .work that produced it:

  client :counter do
    <button class="demo-btn"
      onclick="this._n=(this._n||0)+1;this.querySelector('b').textContent=this._n">
      Clicks <b>0</b>
    </button>
  end

Same file, different home: the server above runs on the engine, this client runs in the browser — and they sit side by side in one document.

sandbox — runs in a capability box

A sandbox block runs untrusted or risky code in a capability-scoped box: compiled to WebAssembly and given only the powers you grant it. Perfect for a scraper or a snippet an agent wrote. You name each power in the header's grant: list:

sandbox :scrape
sandbox :scrape, grant: [fetch] do
  extern char* fetch(const char* url);
  char* run(const char* url){ return fetch(url); }   // the host brokers the call
end

The sandbox can compute and do exactly what it was granted — nothing more. Three kinds, three places code can run, all written the same way.

Any code can be a work block

Those three are the placements you'll reach for most, but the bigger point is that a work block's body is written in a real programming language, not a new one. Elixir runs native on the Nexus (the BEAM). And these compile to WebAssembly today:

rust · c · cpp · zig · swift

A Rust function, a Zig routine, a C++ kernel — drop it in a work block and it compiles. More languages are recognized but not yet compiling (python · go · js · ts · svelte · solid). The literate document is the authoring surface; the code inside is whatever fits the job.

It's a real tree, not text matching

One quiet but important detail: a .work file isn't parsed by guessing with regexes. The do … end block is the delimiter, and each work block parses into a real syntax tree — kind, language, name, and body all read off the structure. That single parse is shared by everything that touches the source: the viewer that highlights it, the tool that maps dependencies, and the check that audits capabilities. One source of truth, read the same way every time. (More on the header in Block grammar.)

index.work — it all compiles together

A workbook is rarely one file. You write several .work files, and one of them — index.work — is the composition root that names the project and lists its pieces in order. The weave reads from the index and folds the whole tree — every page, every work block, across every file — into one compiled workbook: the single artifact a Nexus runs or a browser opens.

So the arc is: write prose and work blocks in .work files, compose them in an index.work, and weave it all together into one workbook. Next, see the four lanes up close in Anatomy of a .work file.

What is the Nexus

The Nexus is the engine you own. It runs your workbooks — on your Mac, on a server, or in the cloud — and connects them to your data, your services, and each other. A workbook is the thing you build; the Nexus is the thing that runs it.

One command, dev is prod

One command starts the Nexus and it serves your workbook. There's no separate "production setup" to graduate to later. The engine you run on your laptop while you're sketching is the same engine that runs the finished product at scale — dev is prod. The prototype becomes the deployment without a rewrite.

You'll find the exact command in Running the Nexus.

Everything runs isolated

Inside the Nexus, every moving part is its own isolated process — each agent, each tenant, each piece of running code. This comes from the BEAM (the runtime behind Elixir, originally built to keep phone networks up), and from running untrusted code inside WebAssembly sandboxes. An agent can build, run, and rebuild inside that boundary without ever reaching your keys, your files, or anyone else's work.

The same engine backs all the lanes of a workbook: the server units that run on it, the agents, the data, sync between clients, and the compile/weave step that turns .work files into running software.

The economics: scale by RAM

Here's the part that matters when you do the math. The usual way to build for scale — microservices and serverless — costs an arm and a leg as you grow: every service is a deployment, every function call is billed, and the glue between them is its own tax.

A workbook on the Nexus scales differently. Because everything runs as lightweight isolated processes inside one engine, you scale by memory — give the Nexus more RAM and it holds more. That is dramatically cheaper than scaling out a fleet of services, and it's a single system to reason about instead of a maze.

And it's multi-tenant capable. By default the Nexus runs single-tenant, which is all most workbooks need; flip tenancy-mode to multi (with a Postgres database and real auth) and the same engine isolates tenants — no rewrite, no separate production stack. The thing you build on day one runs the way it will run at scale.

This is radical simplicity applied to the runtime: one engine instead of a microservices maze.

Yours to run anywhere

The Nexus is portable — a Mac, a Linux box, a VPS, or the cloud, on your own account. You choose where it runs, and it carries the whole journey with it. See Deploy: local and cloud for the two canonical targets, or Workbooks Cloud if you'd rather we run it for you.

What is the work CLI

work is the one Workbooks command line — author, build, run, deploy. It operates on a folder of .work source: it reads the tree, weaves it into a workbook — one shippable artifact — and stands up a Nexus to run it. The same work binary runs natively on your machine and inside the wasm agent sandbox, so an agent has the exact tools you do.

The lifecycle

Working on a workbook follows a simple arc, and each step is one command.

You author — write .work files, then work check to make sure every reference resolves and every block only does what it's allowed to. work structure lists the units in the tree so you can see what you've got, and work why, work near, and work wit answer questions about how blocks depend on each other.

You build — work weave folds the whole folder into one self-contained HTML artifact. While you're iterating, work dev watches the folder and re-weaves on every change (and hot-swaps into a running Nexus), so the page updates as you type.

You deploy — work deploy stands up a runtime, local or cloud, from declarative settings that live with the workbook: scaffold them, validate, apply.

That's the loop: author → build → deploy, all over the same folder.

Command reference

The CLI groups its verbs by what you're doing.

Author — read & verify a tree

VerbWhat it does
work check [dir]resolve references + audit capabilities
work structure [dir]list the units in the tree
work lint [dir]lint the tree for problems
work why :unitwhat a unit depends on (code graph)
work near :unitunits related to one in the graph
work wit :unitthe generated WIT world for a unit

Build — weave & run

VerbWhat it does
work new <template> [dest]scaffold a project you own and edit
work weave <dir> <out>weave the tree into one workbook
work graph <dir> <out>render the code graph as a workbook
work dev <dir>watch & re-weave as you edit

Deploy — stand up a runtime

VerbWhat it does
work deploy initscaffold the deploy config
work deploy validatecheck the deploy config
work deploy applydeploy the config, local or cloud
work deploy <dir> --nexus <name>mount a workbook into a running nexus
work secret set / get / delete <NAME>manage secrets (macOS Keychain)
work secret list / check / schemalist declared secrets, emit a schema

Platform — identity & contexts

VerbWhat it does
work ctx use / set <name>switch or save a target context
work nexus [<url>]list nexuses, or point at an engine
work loginauthenticate to the control plane
work whoamishow your identity
work versionshow the CLI version

Run work help to see the live surface.

For the deploy story end to end, see Deploy: local and cloud.

What is Workbooks Cloud

The Nexus is yours to run anywhere. Workbooks Cloud is us running it for you — a managed Nexus with a portal in front of it, so you can ship a workbook without standing up any infrastructure yourself.

A managed Nexus + a portal

Cloud is the same engine described everywhere else in these docs, hosted and kept running on your behalf. The portal is where you manage it: you work inside a Nexus (your isolated unit, roughly an organization — cloud or local) and organize work into workspaces under it. You deploy a workbook, it runs, and the data, storage, and scaling are handled for you.

When to use Cloud vs running it yourself

Reach for Cloud when you want the workbook live without thinking about servers — the fastest path from work weave to a URL.

Run it yourself when you want the Nexus on your own machine or your own cloud account — see Deploy: local and cloud. Nothing is locked away: the engine is the same in both places, so you can start on Cloud and move to your own infrastructure later, or the reverse.

There's also a middle path — bring your own infrastructure. You can point a managed Nexus at your own storage and database and pay for compute only, instead of handing us the whole stack. Same engine, your backend.

What it costs to reason about

Workbooks Cloud is priced around what you actually use — storage and compute — not per seat, so adding people doesn't change the bill. Because a workbook scales by memory rather than by spinning up a fleet of services, the cost curve stays far flatter than the usual per-service, per-seat stack.

Open the portal

The portal is where you sign in, create a Nexus, and deploy. Start at workbooks.sh.

These docs stay deliberately conceptual about Cloud. The portal's own screens — workspaces, members, billing, storage — are documented in the portal itself, where they're always current.

Anatomy of a .work file

A .work file looks like a document because it is one. As you read down it, the parser sees a simple sequence of pieces: headings, paragraphs, declarations, and runnable blocks. Those pieces sort into four lanes. Once you can spot the four lanes, you can read any workbook.

Here's a small file with all four in it:

Orders

This page tracks orders and shows them in a table.

data elixir :Order
data Order do
  id :int
  total :money
end
server elixir :submit
server place :submit do
  def run(order), do: Store.create(Order, order)
end

The heading and the sentence are prose. data Order do … end is a declaration. server place :submit do … end is code, and the word server is its placement.

Prose

Prose is the rich text that explains. Paragraphs, headings, lists — ordinary writing aimed at the reader. Prose isn't a comment bolted onto code; it's a lane of its own, and it can carry live references to other parts of the workbook (see The prose lane & refs).

Declaration

A declaration is structure with no body to run — you're describing a shape, not an action. A data block declares a typed table, a list of atoms declares an enum, a record declares a struct. Declarations are how a workbook states what its data looks like. They get their own page: Declarations.

Code

A code block is something that runs. It opens with a header and ends with end, and the body is real code in a supported language:

server elixir :sum
server place :sum do
  def run(items), do: Enum.sum(items)
end

A server unit, an agent, a browser island — if it executes, it's in the code lane. The def/defp functions you write live inside one of these units, not as a block of their own. The exact header shape is covered in Block grammar.

Placement

Placement is the quiet fourth lane: the first word of a code block says where it runs. client runs in the browser, server runs on the Nexus, and sandbox runs in a capability-scoped box. Same literate file, but each block knows its home. This is what lets one document describe the browser and the server at once — see Placement & languages.

Why this matters

Four lanes, one file. The thing people see (prose + client), the logic that runs it (server), and the data it stands on (data) aren't scattered across a repo — they sit next to each other, in reading order, in a single document. That's the anatomy that makes a workbook legible to a person and to an agent at the same time.

Block grammar

Every runnable block in a .work file is written the same way:

<kind> [lang] :name … do … end

That one line of grammar covers all of it. Let's take the header apart.

The header, token by token

server elixir :submit
server place :submit do
  def run(order), do: Store.create(Order, order)
end

Reading the header server place :submit:

  • kind is the first word — always. It says what the block is and where it runs: server, client, sandbox, data, agent, resource, and others. The parser takes the first token as the kind, no exceptions.
  • lang is optional. If a token is one of the supported languages, it's read as the language of the body. Leave it out and the block uses its default.
  • :name is the atom that names the block — a token starting with :. It's how other blocks and your prose refer to this one.
  • Anything else on the line is the block's arguments, passed through to the body.

So sandbox rust :scrape do is kind sandbox, language rust, name scrape. And client :widget do is a browser island named widget.

do … end is the delimiter

A block opens with a non-indented line that ends in do, and closes with the first end at column 0. Everything between is the body — real code in the block's language. Plain prose never ends in do, so there's no ambiguity about where a block begins.

The body can have its own do … end pairs — an if, a case, a nested function. Keep those ends indented: the block closes at the first non-indented end, so the only end written hard against the left margin is the one that closes the block. Write the language naturally with normal indentation and it just works.

A name without a colon

Some blocks name themselves the way the host language does — defmodule Workbook do declares something called Workbook. When there's no :atom, the parser takes the bare name. You'll mostly use :name, but both are understood.

Why it's a tree, not a regex

This grammar isn't matched with fragile text patterns. Because do … end is the delimiter and the header tokenizes cleanly, each block parses into a real syntax node — kind, language, name, and body are read straight off the structure. That single parse is shared by everything that touches a workbook: highlighting, the dependency graph, and the capability audit. Write the block once; every tool reads it the same way.

Next, the lane that surrounds the blocks: The prose lane & refs.

The prose lane & refs

Prose is a first-class lane, not a comment. It's where you explain the workbook in ordinary language — and where you wire ideas together with live references the tools understand.

It's just markdown

The prose lane is markdown. Headings, paragraphs, bold, italic, inline code, lists, and links all work the way you'd expect. Write naturally; the document reads like prose because it is prose.

References that mean something

Inside prose you can point at other things directly, and the parser picks these tokens out in order:

  • [[backlinks]] — a link to another block or page in the workbook.
  • work://… — a link across the workbook, addressed by path.
  • :atom — a mention of a named block or value (the same :name a block declares).
  • @type — a mention of a type.
  • #tag — a tag you can group and find things by.

These aren't styling. They're how a workbook knits itself into a graph: when you mention :submit or write [[Order]], that's a real edge the tools can follow — the same edges work why and work near walk when they answer "what depends on this?"

Refs inside code are examples, not links

There's one sensible rule worth knowing: a reference written inside inline code is treated as a syntax example, not a real reference. So a sentence teaching you about [[backlinks]] (in code font, like this) doesn't accidentally create a link. Real references live in the running prose; quoted ones stay quoted. This very page mentions the syntax constantly without tangling itself in links.

Why prose carries the graph

Because references are part of the prose lane, the explanation and the dependency graph are the same artifact. You don't maintain a separate map of how things relate — you write the relationships into the sentences, and the workbook is the map. That's a big part of why a .work file stays legible to an agent: the intent and the connections are right there in the text it's already reading.

Next: the structure lane — Declarations.

Declarations

A declaration describes a shape, not an action. Where a code block runs, a declaration just states what something is — the data your workbook holds, the options a value can take, the settings it runs under. It's the structure lane.

Typed tables

The most common declaration is a typed table. You name it and list its fields as field :type:

resource elixir :Product
resource Product do
  name :text
  price :money
  stock :int
end

That declares a Product with three typed fields. A resource is the persisted, queryable kind — the Nexus stores its rows and serves them. A data table is declared the same way.

Because the table is declared in the document, your prose can show it: a show Product directive renders the rows as a table, columns drawn straight from the fields you declared. Until you show a declaration it renders as a plain preformatted block — the table appears where you ask for it.

Enums and records

Two more shapes round it out:

  • An enum is a fixed set of options — written inline as a field type, a union of atoms like status :draft | :live. It isn't a block of its own; it's the type of a field inside a data, resource, or record.
  • A record is a struct — a fixed set of named fields grouped into one value, the way a defstruct works in Elixir.

Both describe shape: you're naming a structure, not running anything.

Attributes

Single settings are declarations too. An attribute is a flat @name value line — configuration stated in the document rather than hidden in an environment variable. Tunable settings live as attributes (read back through the engine's config), which keeps a workbook's configuration visible and reviewable alongside everything else.

The reserved words

A handful of words introduce declarations and directives, so the parser treats them specially rather than as ordinary prose: data, show, query, type, deps, checks, theme, task, user, grant, route, workbook, and nexus. You'll meet most of them where they're used; the thing to remember is that a declaration is always describing, never doing.

Next: where blocks run, and what languages they're written in — Placement & languages.

Placement & languages

Two things about a code block decide how it runs: its placement (where) and its language (what it's written in). Both are right there in the header.

Placement: where a block runs

The kind word at the start of a block puts it somewhere. Three placements cover the ground:

  • client — runs in the browser. A client block is a browser island: its body is the interface, emitted directly into the rendered page so it runs client-side wherever the page loads. This is the lane that draws what people see.
  • server — runs on the Nexus, the engine, on the BEAM. This is your backend: logic, data access, the work that shouldn't happen in the browser.
  • sandbox — runs in a capability-scoped box. Guest code is compiled to WebAssembly and given only the powers it's explicitly granted, so you can run untrusted or risky work without handing it the keys to everything.

Same file, but client draws in the browser while server runs on the engine — which is how one document describes the whole application.

Languages: what a block is written in

A block's body is real code in a supported language. elixir runs natively on the BEAM; the rest compile to WebAssembly through the work toolchain:

elixir (native) · rust · c · cpp · zig · swift

You name the language in the header (sandbox rust :scrape, client :counter). Leave it off and the block uses its default. The body is ordinary code — there's no special dialect to learn, just the language you already know, written inside a literate document.

Placement and language are independent

The two choices compose. You can write a sandbox in Rust, a client in the browser, a server in Elixir — pick the placement for where the work belongs and the language for what fits the job. That independence is the heart of the format: one workbook, many languages, each block running in the right place.

Next, a compact tour of every kind: Kinds reference.

Kinds reference

The kind is the first word of a block. Here's the whole vocabulary in one place, grouped by what each kind is for. The header shape is always the same — <kind> [lang] :name … do … end.

Runnable kinds

These execute. Their placement decides where.

  • client — a browser island. The body is the interface, emitted into the rendered page so it runs client-side. This is what people see.
  • server — a unit that runs on the Nexus, on the BEAM, natively. Your backend logic and data access; the def/defp functions you write live inside it.
  • sandbox — guest code in a capability-scoped box, compiled to WebAssembly and given only the powers it's granted. For untrusted or risky work.
  • agent — a unit with an agent brain: a server-side block that can reason and act, not just compute.
  • toolkit — a bundle of capabilities a workbook builds and exposes.

Structure kinds

These declare shape; nothing runs.

  • data — a typed table. Declares fields as field :type; render it with a show directive (until shown it renders as a plain preformatted block).
  • resource — a persisted, queryable table. The Nexus stores its rows; a show directive renders them.
  • record — a struct: a fixed set of named fields grouped into one value.

An enum isn't a kind of its own — it's an inline field type, a union of atoms (status :draft | :live) inside a data, resource, or record.

Directives

These tell the page to do something with a declaration.

  • show — render a resource as a table, columns taken from its fields.
  • task — a declared task or job the workbook defines.
  • design — the brand sheet: a block whose CSS is hoisted into the page head, shaping how the rendered workbook looks.
  • check — declared assertions the workbook holds itself to.

Reading a kind you don't recognize

The grammar guarantees you can always parse an unfamiliar block: the first word is the kind, an optional language follows, then :name, then do … end. Even if you haven't met a kind before, you can see what it's called, what language it's in, and where it runs — which is usually enough to follow along.

Elixir in Markdown

When a block runs on the Nexus, it's written in Elixir — and it's the real thing, not a cut-down version. A .work file just lets you write that Elixir inside a literate document. This page is a quick tour so the server blocks you see elsewhere read clearly. You don't need to master Elixir to use Workbooks; you mostly need to be able to read it.

It reads like instructions

Here's a function — the def run(...) body lives inside a server unit. Even if you've never written Elixir, you can follow it:

server elixir :hello
server greet :hello do
  def run(name), do: "hello, #{name}"
end

def run(name) defines a function that takes a name. The #{name} drops the value into the string. That's most of Elixir's day-to-day shape: small named functions that take values and return new ones.

A few ideas worth knowing

  • Atoms are names that stand for themselves, written with a leading colon: :ok, :submit, :hello. You've already seen them — a block's :name is an atom. They're how Elixir labels things.
  • Pattern matching lets a function branch by the shape of its input. You can write two clauses of the same function and the right one runs:
server elixir :check
server status :check do
  def run(:ok), do: "all good"
  def run(:error), do: "something broke"
end
  • The pipe |> passes a value into the next function, so a sequence of steps reads top to bottom instead of inside out: items |> Enum.filter(...) |> Enum.sum().
  • Immutability — values don't change in place; functions return new values. This is a big part of why Elixir code is easy to reason about, for you and for an agent: nothing mutates behind your back.

Why Elixir fits here

Two reasons, the same ones from the opener. Concurrency: the BEAM runs huge numbers of tiny isolated processes, which is exactly what a Nexus full of workbooks and agents needs. And readability: the language stays close to plain instructions, so the prose and the code in a workbook speak the same calm dialect.

If you want the full language, elixir-lang.org is the canonical guide. Everything there works inside a .work block — because it is Elixir.

Sandboxing and the Dock

Some code you'd rather not trust with everything — a scraper, a third-party toolkit, a snippet an agent wrote. A sandbox block is how a workbook runs that code with only the powers you grant it. It compiles to WebAssembly and runs against a single, audited host surface called the Dock.

The membrane

WebAssembly has no ambient authority: a wasm module can compute, but it can't open a socket, read a file, or call an API on its own. The only things it can do are the functions the host imports into it. The Dock is that host surface — the one place the Nexus hands real powers to guest code.

Nothing crosses the membrane that you didn't grant. That's the whole security model: not a list of rules the guest is asked to follow, but a wall it physically cannot reach through.

What the Dock can offer

The Dock exposes a small, fixed set of host functions. Guest code never gets the raw power — it gets a narrow, typed call that the host brokers. These are the live host functions a sandbox can call by name today:

  • fetch — an HTTP GET the host performs on the guest's behalf. The host runs an SSRF gate first, so a sandbox can't reach loopback, private, or link-local addresses even if it asks.
  • complete — a brokered LLM completion. The model key never appears in source; the host resolves it and calls the provider (see Secrets & configuration).
  • store / load — an in-memory key/value store (store(key, val) / load(key)).
  • cache_get / cache_put / cache_delete — a tiered, durable cache. The guest names only a key; the host binds the namespace.
  • now — the host clock; emit — write a line to the host log.

Each host function is WIT-typed: the host declares the exact function signature the guest may import. There's no way to import a power that wasn't wired in.

Behind these live calls sits a broader capability catalog — the named capabilities (net, kv, secrets, fs, exec, and the runtime caps vfs, commands, llm, browse, parallel) and the WIT interface each projects. The catalog is the vocabulary the toolchain reasons about; not every catalogued capability has a live guest entry point yet. net (via fetch) and the brokered LLM (via complete) are live today; secrets, fs, exec, and a tenant-scoped kv are catalogued WIT interfaces without a live guest call.

Why broker instead of allow

Every Dock function is the host doing the work, not the host opening a hole. fetch isn't "raw sockets, please be careful" — it's "ask the host to fetch this URL," and the host applies its own policy (the SSRF gate, allowlists) before it acts. That keeps the dangerous part on the trusted side of the membrane, where it can be audited in exactly one place.

You declare what it needs

A sandbox names the host functions it wants, and the weave checks them: the capability audit runs at weave/build time, matching the host functions a block actually calls against what it grants, so a block that reaches fetch is visible at review time and a block that grants nothing can't surprise you. The grants are part of the source — see Grants & capabilities.

The point: untrusted code is still useful code. The Dock lets you run it, see exactly what it can touch, and trust the wall instead of the author.

Grants and capabilities

A capability is a named power: the ability to make a network call, read a key, run a command. A grant is you handing one of those powers to a block. Together they make the sandbox model concrete — and they live in the source, so they're reviewable like any other code.

The host functions

There is a small, fixed set of host functions the Dock brokers — the powers a block can actually call by name and must grant:

  • fetch — perform an HTTP GET (host-mediated, SSRF-gated).
  • complete — a brokered LLM completion (the model key stays on the host).
  • store / load — read and write an in-memory key/value store.
  • cache_get / cache_put / cache_delete — a tiered, durable cache.
  • now — the host clock; emit — write a line to the host log.

A block that grants fetch can call fetch; it cannot suddenly also reach another host function. Powers don't bleed.

Beyond the live host functions, the Dock maintains a broader capability catalog — named capabilities (net, kv, secrets, fs, exec) and the WIT interface each projects. The catalog is the vocabulary the toolchain reasons about; the host functions above are what a guest can call today.

Granting a power

A block requests only what it needs. The default is nothing — a sandbox with no grants can compute and return a value, and that's all. You add a power by naming it in the block header's grant: list, and the weave wires the matching host import into the compiled module:

sandbox :scrape
sandbox :scrape, grant: [fetch] do
  extern char* fetch(const char* url);
  char* run(const char* url){ return fetch(url); }   // the host brokers the call
end

A block that never grants fetch is flagged by the audit if it calls it — the grant is the contract. The grant token is the host-function name (fetch, emit, …), the exact name the audit matches against the block's body.

The audit is the point

Because grants are written down and the one parser reads them off the tree, the toolchain can audit them. The capability audit runs at weave/build time: it matches every host function a block calls against the functions it grants, and an ungranted call is a hard error, not a silent capability.

A reviewer (or an agent) sees at a glance that :scrape reaches the network and the report-renderer doesn't. There's no hidden System.cmd, no smuggled socket — if a block can touch the outside world, the grant says so, in the file, in the diff.

(work check is a separate, faster gate: it resolves [[refs]] and validates auth/route policy. The capability audit is the weave/build's job.)

Least power, by default

The model rewards asking for less. Grant load when you only need to read a value by key, not the network. Grant nothing when the block is pure. The narrower the grant, the smaller the audit, and the easier the block is to trust — which is exactly the property you want when an agent is writing the code.

Next: the powers that are deliberately not on this list, and why — The three walls.

The three walls

Some things a sandbox cannot do, and won't ever do as described — not because we haven't built them, but because the architecture rules them out. We call these the three walls. Naming them is a feature: a wall you can see is more trustworthy than a gap left silent.

Why a wall is honest

A capability model is only as good as its edges. If a system claims it can run anything safely, it's either lying or it has a hole. Workbooks would rather tell you exactly where the box ends. Each wall is a place where the safe answer is "no, and here's the seam you use instead."

Bedrock — no native execution in the guest

Guest code runs as WebAssembly. It cannot fork a process, JIT new native code, or shell out to the host's machine. There is no "just run this binary" inside the box.

The escape is not a hole in the wall — it's a trusted host-service broker. When a sandbox needs real execution, it asks the host through exec, and the host runs a registered command (a toolkit) under its own policy. The dangerous part stays on the trusted side.

Bridge — no browser host inside the runtime

A client island is browser code; it expects a DOM, a window, the browser's JavaScript host. The Nexus is the BEAM — there is no browser there. So you can't run browser-host code server-side by pretending the server is a browser.

The seam is a real headless build on the BEAM when you genuinely need to render without a browser — not a fake window bolted onto the server. Client code runs where clients run: in the page, as wasm.

Forge — the compiler frontier

A .work block compiles to wasm through a real toolchain, and that toolchain has edges. Some languages compile cleanly today; others are still being brought up. When a language's path to wasm isn't ready, that block doesn't ship — it's a forge wall, not a runtime promise that quietly fails.

The honest move is to tell you which lane is solid and which is in progress, so you pick a language knowing where it stands rather than discovering the gap at deploy.

The shape of all three

Each wall has the same shape: a thing that looks like it should work, a reason the architecture says no, and a real seam you use instead — a host broker, a proper headless build, a supported language. The walls aren't apologies. They're the reason you can trust what the box does let through. For what it does let through, see Grants & capabilities.

Resources and the Store

A workbook isn't just prose and logic — it stands on data. There are three ways to declare a shape, and they differ by one question: does it persist? Get that distinction right and the rest follows.

Three shapes, one question

  • data — a typed table you render with show. You write rows in the file; it's literal data that lives in the document. See Declarations.
  • record — a struct: a fixed set of named fields grouped into one value. It describes a shape you pass around in code. Nothing is stored; it's a type.
  • resource — a persisted, queryable table. The Nexus stores its rows and you query them back. This is your database, declared in the document.

The line is persistence. data is in the file, record is in memory, resource is in the store.

Declaring a resource

A resource names its fields with types, the same way a data block does — but it means a table the Nexus keeps:

resource elixir :Product
resource Product do
  name :text
  price :money
  stock :int
  status :draft | :live | :sold_out
end

That last field shows an inline enum: status is one of three atoms. The declaration is the schema; the Nexus provisions storage for it when the workbook runs.

Reading and writing

A server unit is where rows come and go, because persistence is a server power. A route maps a method-and-path to a handler, and the handler writes through the store with Store.create(Resource, attrs):

server elixir :orders
server :orders do
  route "POST /orders", :create
  def create(req), do: Store.create(Order, req.body)
end

The data plane is part of what a runtime advertises over RCP, so a connected client knows the workbook has queryable data without being told out of band.

Rendering it back

Declaring a resource isn't the same as showing it. The show directive renders a resource as a table, taking its columns from the fields you declared:

unknown resource Product

One line turns the stored rows into a table on the page — no template, no field list to keep in sync, because the columns come straight from the schema. Change a field in the resource and the rendered table follows.

Why it's in the document

A schema in a migrations folder, queries in a model file, and a table in a template are the same fact written three times, drifting apart. Here the resource is the schema, the server unit is the access, and show is the view — all in reading order, in one file, sharing the one parse. The data is as legible as the prose around it.

Routes and pages

A workbook becomes a site when the Nexus serves it. Most of the time you don't write routes at all — a docs-style app declares sections and pages and the runtime renders the navigation for you. When you need an HTTP endpoint of your own, you declare a route.

Pages come for free

An app composition declares its structure, and the runtime turns that into a routed site — sidebar, clean URLs, deep links — with no front-end code:

app elixir :docs
app :docs do
  title "Workbooks"
  section "Introduction" do
    page "introduction/what-is-a-workbook"
  end
end

Each page is a .work file of prose; the runtime server-renders it and wires the router. You link pages with text and reload-safe deep links just work. This is the common case — declare the tree, get a site.

Routes for endpoints

When you need a real HTTP endpoint — an API, a form handler, a webhook — declare a route inside a server unit. A route maps a method-and-path to a handler:

server elixir :orders
server :orders do
  route "POST /orders", :create
  route "GET /orders/:id", :show

  def create(req), do: Store.create(Order, req.body)
  def show(req), do: %{id: req.params["id"]}
end

The path can carry params (:id), and the verb is part of the declaration. A handler takes a request map %{params, query, body, method, path, tenant} — path captures arrive as string keys, so req.params["id"]. The server unit holds both the routes and their handlers, side by side.

Guarding a route

Routes default to whatever the workbook's access posture is; you mark the exceptions. Guards are declared in the workbook's auth policy block — public (no auth) or protect (auth required) — matched against requests by method and path, so the access posture lives in one readable policy rather than buried in middleware. The same guards apply whether a request arrives with a session cookie or a minted token — the token carries scopes and is subject to the same per-route check.

One document, browser to endpoint

A page renders prose, a client island runs in the browser, a server route answers HTTP, and a resource stores the rows behind it — all in the same tree, all read by the one parser. The site isn't a separate app wired to the workbook; the workbook is the site. See how one runtime hosts more than one of them in One nexus, many sites, and how a site renders itself in Site mode.

Site mode

This documentation is a workbook. The sidebar, the routed URLs, the deep links you can reload — none of it is hand-written front-end code. It's site mode: the runtime reading an app composition and rendering the whole tree as a navigable site. The same machinery serves any docs-shaped workbook, including yours.

What the runtime renders

You declare structure — sections and pages — and the platform's site mode does the rest:

  • a sidebar built from your section / page declarations,
  • clean, history-routed URLs (/section/page), reload-safe so a deep link works,
  • server-rendered prose for each page, with internal links via text.

The look is yours — it comes from a brand sheet in your composition root, never from the runtime. The Nexus ships unopinionated plumbing; you supply the title and the theme.

Why render it server-side

Server-rendering the prose means the site is fast, linkable, and indexable without shipping a front-end framework to do routing. The client islands that do need to be interactive hydrate as wasm in the page; everything else is just rendered HTML. You get a real site without writing one.

The same shape, scaled

Whether the runtime serves this one docs workbook or a dozen apps side by side, site mode is the same — one nexus, many sites, each a tree the runtime renders. Declare the structure; the site follows.

Running the Nexus

The fastest way to see a workbook run is the push-to-live loop: point work dev at your folder and keep editing.

work dev .

work dev weaves the workbook once, then watches the tree. Every time you save a .work file it re-weaves the output. You write; the woven artifact stays current. That's the whole loop. (Live hot-swap into a running Nexus — an instant reload without re-applying — is not yet wired.)

What "running" means here

There are two things you might mean by "running a workbook," and they line up with two commands:

  • Iterating — work dev gives you the live edit loop above. This is where you spend most of your time: write prose and blocks, watch them come alive.
  • Standing up a real runtime — when you want an actual Nexus serving the workbook (local container or cloud), that's work deploy. Same engine, but a durable runtime rather than a watch loop.

Dev is prod

The reason this matters: the engine behind work dev is the same engine you deploy. There's no separate "production mode" to learn later — the behavior you see locally is the behavior you get at scale. You're not building a toy that you'll rewrite for production; you're building the production thing, just on your own machine first.

When you're ready to put it somewhere durable, head to Deploy: local and cloud.

Deploy: local and cloud

When you want a durable runtime — not just the dev loop — you use work deploy. It stands up a Nexus for your workbook in one of two places: local or cloud, and both run the same engine.

The deploy loop

work deploy init local

init scaffolds the configuration. Then:

  • work deploy validate — check the config is coherent (the same lockouts catch mistakes before they ship).
  • work deploy apply — stand the runtime up.

To mount a workbook into a running Nexus, point work deploy at the folder:

work deploy . --nexus my-nexus

Config lives with your workbook

A deployment is described by a set of declared settings that live with your workbook — version-controlled and reviewable, part of the source like everything else. It is not a JSON file off to the side, and it is not a scatter of environment variables. Secrets are the one exception: they're never written into the config, they're injected from the environment at deploy time — see Secrets & configuration. work deploy init scaffolds the settings; you edit them in place.

The settings you declare:

  • engine-place — local or cloud.
  • tenancy-mode — single or multi.
  • storage — local-fs or s3.
  • database — sqlite or postgres.
  • auth — trusted, betterauth, clerk, or oidc.

Local

engine-place="local" runs the Nexus in a Linux container on your machine (via krunvm), and it's cloud-identical — the same image you'd run in production. It defaults to local-fs storage and a sqlite database, with a persistent /data volume that survives restarts. This is the closest you can get to production without leaving your laptop.

Cloud

engine-place="cloud" deploys to Fly, under your own account — you set the provider, app, and region. It defaults to s3-compatible storage (such as Cloudflare R2) and a postgres database, so it holds up across scale-to-zero machines.

Same workbook, either target

Because both targets run the same engine, moving between them is a config change, not a rewrite — flip engine-place and re-apply. Start local, go to cloud, or run both. And if you'd rather not manage any of this yourself, that's exactly what Workbooks Cloud is for.

Deploy as an index tree

How a workbook tree declares its own deployment. It's the same composition-as-source idea you already use for the app, extended to how the app ships — no sidecar file, no magic filename, no scatter of environment variables.

The convention

The runtime reads its deploy config from the index.work at the deploy root: at boot it reads $WB_DATA/index.work and takes the deploy declaration it finds there. The convention follows from that: put one deploy declaration in the deploy-root index.work, and that index is the deployable unit — the root of the served tree.

Because the config lives in the source, it's version-controlled and reviewable rather than hidden in a sidecar file or a scatter of environment variables. You make an index and give it a deploy declaration to say "this tree ships as a unit."

The manifest index is not a page

The deploy-root index.work is the composition root and the manifest. It is not served as a / page — surface discovery skips it — unless you deliberately give it a client or server block. It indexes the tree and carries config; the served surfaces are its subfolders.

The simple case

Most workbooks are a single nexus: one root index.work with one deploy declaration. The runtime reads its config straight from that deploy-root index.work at boot — that's the whole model.

Secrets & configuration

There are three kinds of "settings," and keeping them in the right home is the most important thing to get right when you deploy. Here is exactly how it works.

Configuration — not secret

Tunable knobs — concurrency, caching, which search provider to use — live in your workbook's deploy config: the deploy do … end block (see Deploy: local and cloud). It's version-controlled and reviewable, part of the source. Not JSON, not environment variables.

Secrets — never in your source

API keys and tokens follow one absolute rule: a secret never lives in your source. Not in a .work file, not in the deploy config, not committed to git — ever. A workbook ships and is version-controlled, so a key inside it would leak the moment you share or push it.

Instead, secrets are provided to the running Nexus through the environment at deploy time, from a secure source:

  • Cloud — Workbooks Cloud includes an encrypted, per-organization secret manager. Values are encrypted at rest (AES-256-GCM), shown masked, and revealed only on demand. You set a secret once in the portal and it's injected into your runtime; the plaintext is never stored in the open and never logged.
  • Local — the work secret CLI keeps keys in your macOS Keychain, never in a file. You declare which secrets a workbook needs in a secrets do … end block (names + descriptions, no values — so an agent has full context and zero exposure), then:

work secret set OPENROUTER_API_KEY # prompts; stored in the macOS Keychain work secret list # shows declared secrets, masked (set / unset) work secret schema # emits a varlock .env.schema for varlock run

At run time the values are injected (via varlock) into the environment for that one command — never written to disk, never pasted into a .work file.

The runtime then reads every secret through a single, audited path, so there is exactly one place that knows which secrets exist and nothing reads a key ad-hoc.

Machine identity — deploy injection

A few values look like config but aren't yours to write — the data mount path, the tenant slot. The real test is author-time vs deploy-time: you can't author your own mount path or which tenant you're deployed into, because the orchestrator assigns those at boot. So they're not configuration (you don't tune them) and not secrets (they're not sensitive) — they're facts the deployer hands the running Nexus. Read them where needed; never try to set them in a .work file.

The one rule to remember

  • Tunable and safe to read → deploy config (.work).
  • A key or token → a secret, injected from a secure store, never in source.
  • Identifies the machine → deploy injection.

When in doubt, ask: would I be comfortable committing this to a public repo? If the answer is no, it's a secret — and it stays out of every file you write.

Workbooks Cloud

Deploying yourself puts the Nexus on your machine or your own cloud account. Workbooks Cloud is the managed alternative — we run the Nexus for you, so deploying is just pushing a workbook to a runtime that already exists.

If you haven't met it yet, start with the concept: What is Workbooks Cloud.

When to choose it

Reach for Cloud when you want the workbook live without standing up or maintaining any infrastructure — the shortest path from a folder of .work files to a URL. You skip the work deploy apply step entirely; the managed Nexus is already running.

It's the same engine

Nothing about your workbook changes for Cloud. It's the same engine you'd run locally or on Fly, so you can move between managed Cloud and your own infrastructure later without a rewrite. You can even bring your own storage and database and let Cloud handle only the compute.

Get started

Sign in, create a Nexus, and deploy from the portal at workbooks.sh. The portal's own screens stay documented in the portal, where they're always current.

Get started with your CLI agent

Workbooks is built to be driven by a coding agent — Claude Code, Codex, or whatever you use. The whole format exists so an agent can read intent and write code in the same document. This page is the on-ramp for pointing your agent at Workbooks.

You drive the CLI; the agent works inside the sandbox

You run the work CLI to author, weave, and ship — work check, work weave, work dev. An agent you author runs server-side on the Nexus with its own surface: one bash tool over a sandboxed /work filesystem, with a coreutils base, built-in web access, and any toolkits you write. It computes real answers in the box instead of guessing.

Install the skill

The packaged way to teach your agent the Workbooks workflow is the skill:

npx skills add workbooks-sh/workbooks.sh

That gives your agent the conventions for authoring .work files, running the CLI, and shipping a workbook — so you can describe what you want and let it build.

How to work with it

The loop is simple: describe the outcome, let the agent author the .work files, then review the rendered workbook the way you'd review any change — reading the prose next to the code. Because the document carries its own intent, you review reasoning and implementation together, in one place.

That's the whole point of everything in these docs: the workbook is legible to you and to the agent at the same time. Start by reading what a workbook is, then put your agent to work.

Authoring an agent

An agent is a unit with a brain: a server-side block that can reason and act, not just compute. Where a server unit runs a function, an agent reads a goal, decides what to do, and uses tools to do it — all declared in the same .work file as everything else.

The brain is prose

An agent's body is its instructions, written as plain prose. You describe who it is and how it should behave; that is the program:

agent :analyst
agent :analyst do
  You are a terse data analyst. You have one tool, bash, with a coreutils kit
  (sort, uniq, cut, wc) and built-in web access (search, scrape). Always COMPUTE
  with bash against /work — never guess a number. When you have the answer, reply
  with exactly the requested format, nothing else.
end

There's no separate prompt file and no framework glue. The brain lives where you can read it next to the data and logic it works over — the same legibility the whole format is built on.

Tools are capabilities

An agent acts through tools, and tools are just brokered powers — the same capabilities any sandboxed block uses. The key tool is bash: the agent runs commands in a capability-scoped box, against a mounted work area (/work), with toolkits as the available commands. It computes real answers instead of hallucinating them, because the box gives it real tools.

Every agent has a coreutils base and built-in web access (search, scrape, operate a site); the toolkits you write add commands on top. The Dock is the wall here — the agent only ever runs commands inside the sandbox, against /work, never the host. An agent is powerful and contained.

It runs on the Nexus

Agents are server-placed: they run on the Nexus, on the BEAM, with access to the runtime's brokered model completion and the tools you declare. That's why the brain is a server-side kind — reasoning and tool use are host powers, mediated like any other.

The same review loop

Because the agent's instructions are prose and its commands are the toolkits in the workbook, you review an agent the way you review any block: read what it's told to do, see which toolkits it can run. No hidden system prompt, no opaque tool registry — the agent is as auditable as the rest of the workbook. To give it commands of your own, write a toolkit.

Toolkits

A toolkit is a command you wrap once and call everywhere — a small CLI, written in a real systems language, compiled to WebAssembly, and made available as a command that a sandbox or an agent can run through exec. It's how you extend the box with tools of your own.

A toolkit is a wrapped CLI

The body of a toolkit block is an ordinary command-line program — it reads stdin and writes stdout, the classic Unix shape. The platform compiles it to a wasm32-wasi command module and registers it under its name:

toolkit :upper
toolkit :upper do
  // summary: uppercase stdin
  #include <stdio.h>
  #include <ctype.h>
  int main(void) { int c; while ((c = getchar()) != EOF) putchar(toupper(c)); return 0; }
end

Once registered, upper is a command. An agent told it has an upper tool can pipe text through it; a sandbox with exec can invoke it. The toolkit is the wasm command behind that name.

Many languages, one shape

The language is inferred from the body — C and Rust compile to wasm commands today, each through its real toolchain. Whatever the language, the contract is the same: stdin in, stdout out. That uniform shape is why a toolkit composes with any other command and why an agent can use one without knowing how it was written.

The same source always yields the same command — a toolkit block compiles to one deterministic wasm32-wasi module behind its name.

Why wrap a CLI at all

Three reasons it's worth the block:

  • Reuse — write the command once; every agent and sandbox can call it. No copy-pasted shell snippets drifting apart.
  • Containment — the command runs as wasm under the Dock, with only the powers granted. A toolkit is a safe way to give an agent real teeth.
  • Legibility — the tool's source sits in the document next to the agent that uses it, parsed by the one parser like everything else.

Toolkits and agents

This is the seam that makes agents useful. An agent that "has a bash tool with a web kit" is really an agent granted exec over a set of registered toolkit commands. You build the kit, grant it, and the agent computes real answers with real tools — inside the wall, every time.

One frontend, many targets

Workbooks runs on the desktop, in the browser, and on mobile — but there isn't a desktop app, a web app, and a mobile app. There's one frontend that talks to a single capability surface, and a target is just a choice of how that surface is fulfilled. No code fork per platform.

The single seam

The UI never calls the operating system, the network, or the runtime directly. It calls one set of named capabilities — open a file, fetch a URL, run a unit — through a single membrane. Behind that membrane, a router decides who fulfills each call. That's the whole architecture: the app asks for a capability; something provides it.

Providers fulfill capabilities

Each capability is satisfied by a provider, and there are two kinds:

  • local — the capability is served on the device: the OS through a native shell, or the browser's own APIs. Reading a file locally, showing a native dialog.
  • runtime — the capability is served by a shared Nexus over the wire, via RCP. Server units, data, sync, the compile-and-weave lane.

A target — desktop, web, or mobile — is a routing config: which capabilities go local, which go to a runtime, and which runtime endpoint to use. Swap the config and the same frontend runs somewhere new. You don't recompile the UI for a platform; you re-point it.

Where the lanes run

This mirrors the placement model you already know:

  • a client island renders client-side — wasm in the browser, so it runs on every target with no special engine,
  • a server unit, agents, data, and the compiler lane are backed by the runtime provider.

The browser is a render target, not a second runtime contract for the UI to juggle. There's one Host surface; targets just route it differently.

Why one seam matters

The trap every cross-platform tool falls into is a second "contract" — the UI ends up knowing about both an OS API and a server API and managing the difference. Here the UI knows exactly one thing: the capability membrane. Local versus runtime is the router's problem, not the app's. That's how desktop, web, and mobile stay one codebase instead of three that drift. The handshake that lets a client discover and connect to a runtime is Runtime connect (RCP).

Runtime connect (RCP)

When a client — desktop, web, or mobile — wants to use a Nexus as its runtime provider, it has to learn what that runtime is before it can talk to it: which protocol, how to authenticate, what it can do. RCP is that handshake — a small, public description a runtime serves about itself.

A self-describing runtime

Every Nexus answers a well-known discovery endpoint with a compact description of itself. A client fetches it once and knows how to connect — no out-of-band config, no guessing. The response covers:

  • protocol version — which RCP revision the runtime speaks,
  • runtime version — what build is running,
  • tenancy — whether this runtime is single- or multi-tenant,
  • auth — how to authenticate (a trusted rung for a local runtime, or OIDC/JWT with an issuer and key set for a shared one),
  • transports — which wire protocols are available (HTTP, and websockets when offered),
  • capabilities — what planes this runtime exposes.

The capability planes

A runtime advertises what it offers, so a client only attempts what's actually there:

  • data — the workbook data plane: serving workbooks, their resources, and routes. Every runtime has this.
  • platform — the control-plane fleet API, present only when a runtime is running as a control plane. A plain single-workbook runtime won't advertise it, and a client won't try to use it.

Because the runtime declares its planes, the one frontend adapts to what it connected to instead of assuming a fixed server shape.

Names, not URLs

You generally don't paste an RCP endpoint around by hand. You point at a runtime by name — work nexus <url> to target an engine, work login to authenticate against it — and the tooling does the discovery. The name resolves to a runtime your account owns; RCP fills in the rest. (See What is the work CLI.)

Why a handshake at all

A client that hard-codes its server's shape breaks the moment the server changes, and can't tell a local runtime from a cloud one. RCP makes the runtime the source of truth about itself: auth rung, tenancy, and planes are discovered, not assumed. That single property is what lets the same client connect to your laptop's runtime today and a cloud Nexus tomorrow without a code change.

Learn by example

Every template that ships with Workbooks is a small, complete app — and its whole source is a handful of .work files you can read top to bottom. Below, each one is presented by what it teaches, not by its brand. Pick a card, then read the real literate source behind it in the editor. Nothing is hidden: what you see is exactly what the running app is woven from.

Open the live app ↗

    Why we author these docs as a workbook

    A short post on dogfooding — the docs you're reading are built with the thing they document.

    Most documentation sites are a static-site generator, a theme, a pile of Markdown, and a build pipeline that turns the three into HTML. We don't run any of that. These docs are a workbook: a folder of .work files, woven by the same work CLI we ship to you.

    One file is the table of contents

    The whole site is declared in one composition root — index.work — with an app :docs block. It names the site, picks a theme, and lists the sections and pages in order. The sidebar you see is a direct render of that block. Reorder two lines, the navigation reorders. There is no separate config, no front-matter database, no _sidebar.yml.

    Every page is just prose

    Each page is a sibling .work file. The first heading is its title; the rest is Markdown. Because a page is a real workbook, when one needs to be interactive — like the Examples explorer, which reads live source out of running apps — you drop a client island in between two paragraphs. The same document is both prose and program.

    Why it matters

    We have a rule: if we build it, we use it. The site-mode renderer behind these docs (Nexus.Docs) is the same one your work new docs scaffold runs. When it's not good enough for our own documentation, we feel it first — and fix it before you ever see it.

    One nexus, many sites

    How a single runtime hosts this documentation, the example apps, and your projects — side by side.

    A nexus is a host. Not a host for one workbook — a host for as many as you mount on it. The runtime you're reading this on is serving the documentation, the Search Swarm, Brandnana, and a slide deck at the same time, each at its own path, from one process.

    Each workbook gets a mount

    When the nexus boots over a folder of workbooks, every subfolder becomes a mount at /<name>/. The runtime injects a <base href="/<name>/"> so each app's relative links — live/<source>, data/<Resource>, page routes — resolve under its own prefix and never collide with its neighbours.

    Names, not URLs

    A nexus has a memorable codename (an adjective-animal handle) and a friendly name you give it. You deploy into one by name — work deploy . --nexus <name> — not by copying a URL around. The name is a handle, not a credential: it only resolves to a nexus your account actually owns.

    What this unlocks

    It means a documentation site, a marketing page, and three internal tools can live on one runtime you pay for once — the same shape whether that runtime is on your laptop or in the cloud. The Running the nexus page shows how to bring one up locally in a single command.