AGENTS.md
August 5, 2026 · View on GitHub
Copyright 2026 Moddable Tech, Inc.
Revised: August 4, 2026
Note: This document was written by an LLM for LLMs.
Guidance for AI coding agents working in the Moddable SDK repository. It covers what you may no be able to easily infer from the source tree, and links to the documentation for everything else.
Most of this file describes mistakes that are easy to make here and hard to diagnose. The sections most worth reading before you change anything are The JavaScript language here is current, not a subset, Console output and debugging, Choosing an API, Documentation that describes legacy APIs, and Manifests.
Three assumptions carried over from Node.js will waste your time: that the language is a limited subset (it is not), that printed output reaches your terminal (it does not), and that a program exits when it finishes (it never finishes).
What this is
The Moddable SDK builds JavaScript applications that run on microcontrollers. It contains the XS JavaScript engine, a runtime of JavaScript modules (many implemented entirely or partly in C), and the command line tools that compile, link, and deploy applications to devices and to a desktop simulator.
The JavaScript language here is current, not a subset
Do not write ES5-era JavaScript. Embedded JavaScript engines are often severely limited, and that assumption is badly wrong here. XS tracks the current JavaScript language standard closely — of the test262 cases for features it implements, it passes 99.95% of the language tests and 99.85% of the built-ins tests.
Write modern JavaScript. All of the following are present, verified by running them:
- Classes with private fields and static initialization blocks, modules,
async/await, generators and async generators, destructuring, template literals, optional chaining and nullish coalescing,Proxy,Reflect,WeakRef,Map/Set/WeakMap/WeakSet, typed arrays,BigInt,SharedArrayBuffer,Atomics - ES2024/ES2025:
Object.groupBy, iterator helpers (.take,.drop, …),Set.prototype.unionand the other new set methods,RegExp.escape,Promise.try,Float16Array, the RegExpdflag, duplicate named capture groups - ES2026:
Error.isError,Array.fromAsync,Math.sumPrecise,Uint8Array.fromBase64, and explicit resource management —usingdeclarations andSymbol.disposeboth work
If you are unsure whether a recent feature is available, assume it is and check with xst rather than working around it.
What is genuinely absent or conditional, none of it about language level:
- No ECMA-402 — the internationalization API (
Intl) is not implemented. Temporalis not implemented. It is the largest single source of skipped conformance tests.- One realm per virtual machine.
$262.createRealmand multi-realm patterns do not work. - Script evaluation is optional per platform —
evalandnew Functionmay be unavailable on a given target.JSON.parseis always available. - Built-ins can be dead-stripped. The manifest
stripproperty removes built-ins the application does not use; reaching one that was stripped throwsdead strip!at build time. See thestripsection of documentation/tools/manifest.md. - This is not a browser. There is no DOM, and no
window. The web APIs that exist are the ones in modules/web and the globals listed in eslint.config.mjs.
The real constraint on a microcontroller is memory, not language features — see creation. What limits an application is how many features it uses in combination, not which ones are available.
To check a feature yourself, xst runs plain scripts on the host — xst script.js, using print() for output.
Background: documentation/xs/XS Conformance.md for the per-feature results, and documentation/xs/XS Differences.md for how the compile-time/run-time split and object freezing shape the runtime model.
Setup
Two environment variables and a PATH entry are required by every tool:
export MODDABLE="/path/to/this/repo"
export PATH="${MODDABLE}/build/bin/mac/release:$PATH" # or lin, win
Tools are not checked in. Build them once before doing anything else:
cd $MODDABLE/build/makefiles/mac && make # or lin, win, wasm
ESP32 work additionally needs IDF_PATH pointing at an ESP-IDF installation, plus source $IDF_PATH/export.sh in each shell. See documentation/Moddable SDK - Getting Started.md and documentation/devices/esp32.md. Similarly, building for other microcontroller families requires installation and configuration of their SDKs (see Getting Started).
Build and run
Two front ends, depending on how the project is described.
mcconfig builds projects that have a manifest.json:
cd $MODDABLE/examples/piu/balls
mcconfig -dl -m -p sim
-ddebug build,-irelease-instrumented, neither means release-dldebug build using xsdb,-dndebug build with no debugger launched (see below)-mrunmake(without it,mcconfigonly generates the makefile)-p <platform>target platform; defaults to the host-t build|deploy|xsbug|clean|alltarget, defaultall- trailing
key="value"pairs merge into the manifestconfigsection, readable asimport config from "mc/config"
Two -t targets are worth knowing, because using them instead of the default is the difference between a fast loop and a slow one:
-t buildis how you check that code compiles. It runs the real build —xscandxslover every module, TypeScript type-checked where present — and stops before launching anything. This is the syntax and type check for a project; there is no separate lint or check step to reach for, and nothing here corresponds tonode --check.-t xsbugstarts a debug session against the build that already exists, skipping compile and deploy entirely. On a device this avoids re-flashing the binary, which dominates the edit-run cycle. Despite the name it works with-dland xsdb, not just the xsbug GUI.
Splitting the two — -t build to compile, then -t xsbug to run — is usually the right shape for automated or scripted work: only the build is slow, and it needs no timing guesswork, while launch and connect take well under a second.
mcpack builds projects that have a package.json. It generates a manifest and chains to mcconfig (for applications) or mcrun (for mods):
cd $MODDABLE/examples/packages/hello
npm install # first time only
mcpack mcconfig -dl -m -p sim
mcpack has no page under documentation/tools/. Its documentation is examples/packages/readme.md, and each guide topic's index.md has a "Building with mcpack" section.
Platform names are the host by default (mac, win, lin), the simulator (sim, or sim/<device> for a device skin), or a device (esp32/<target>, esp/<target> (for ESP8266), pico, nrf52, zephyr, wasm). The valid targets for a platform are the directory names under build/devices/<platform>/targets/; simulator skins are under build/simulators/.
A -d build launches and waits for the xsbug GUI debugger. For unattended or headless work use -dn to build debug without launching a debugger, or -dl to use xsdb.
The simulator is not a device
-p sim runs your JavaScript on the host computer. It is the fastest way to check application logic, and it is weak evidence for anything that touches hardware. Pins, BLE, Wi-Fi, and displays are absent or stubbed, memory behaves nothing like a microcontroller (see static), and timing is unrepresentative. "It works in the simulator" does not mean it works on the target — build for the real device before concluding a hardware-facing change is correct.
Console output and debugging
Nothing your code prints reaches your terminal. This is not Node.js. trace() and console.log() write to the debug channel, not to stdout, so a program that runs without a debugger attached produces no visible output at all — including when it runs in the simulator. Building with -dn gets you a running program you cannot see.
To see output, run under xsdb. Traces appear at the (xsdb) prompt, prefixed with the thread they came from:
(xsdb) Continuing.
[Thread 1] Hello, world - sample
If you are looking for a log line and finding nothing, the usual cause is that no debugger is attached — not that the code did not run.
The program never exits. Also unlike Node.js: an application here runs forever. There is no "script finished, process exits" — the event loop keeps going, on real hardware and in the simulator alike. Do not run a build-and-run command and wait for it to return; it will block until whatever timeout you are under kills it, and you will read that as a hang or a failure.
Instead, drive the session explicitly: run under xsdb and issue quit when you have what you need, or start the program in the background and terminate it yourself. When scripting xsdb, allow time for the target to connect and reach your code before sending commands — commands sent too early are answered with The program is not stopped.
xsdb
xsdb is a command line JavaScript debugger modeled on gdb, working against both devices and the simulator. Its documentation names three intended audiences, the third being "code generators, such as LLMs" — so it is the tool to reach for here rather than adding trace() calls and rebuilding.
cd $MODDABLE/tools/xsbug-log && npm install # first time only; xsdb is implemented in Node
cd $MODDABLE/examples/piu/balls && mcconfig -dl -m -p sim
helpat the(xsdb)prompt lists every command, grouped into control flow, breakpoints, inspection, navigation, settings, and control. The full command surface is discoverable at runtime, so it is not reproduced here.set output jsonswitches to structured JSON output intended to be parsed rather than scraped.set output textgives gdb-style output. Prefer JSON when consuming output programmatically.- Settings persist per project in a hidden
.xsdb.jsonfile in the project directory — breakpoints, output mode, break-on-exception, break-at-startup. Behavior can differ between projects for this reason. The file is gitignored. mcrun -dlworks for mods, but not yet on all targets.
Reference: the xsdb section of documentation/tools/tools.md. The GUI debugger is documentation/xs/xsbug.md.
Repository layout
- build: Files required for specific microcontroller targets, the simulator, and make files for build tools in the
toolsdirectory. - contributed: Unofficial projects and modules. See the caution below.
- documentation: All the documentation for the Moddable SDK, in markdown.
- examples: Example applications. examples/readme.md is a guide to building them.
- guides: Task-oriented how-to pages, newer than most of
documentation. Each topic directory has anindex.mdgiving the manifest include path and module specifier for the topic. - licenses: License agreements, including the Contributor License Agreements.
- modules: The software modules that make up the runtime — networking, graphics, user interface, hardware access, cryptographic primitives, and device drivers. All have a JavaScript API; many are implemented in part using C.
- tests: Unit tests, in test262 format. See Tests.
- tools: Tools to build applications, including
mcconfig,mcpack,mcrun, and the xsbug debugger. - typings: TypeScript declarations for the SDK, published as
@moddable/typings. - xs: The XS JavaScript engine including its compiler and linker, and the test262 execution shell.
Choosing an API
Several parts of the SDK have been superseded. The older code still works and is still shipped, so it is not obviously stale — but it should not be used as a model for new code, and its documentation is extensive enough to be actively misleading.
The rule: if an ECMA-419 (embedded:*) version of a module exists, it is the preferred one.
There are roughly three tiers, newest first:
-
Web standard APIs — modules/web and related:
fetch,WebSocket, streams,console, timers,localStorage. Prefer these where they cover the task. -
ECMA-419 — modules/io, imported as
embedded:io/*,embedded:network/*,embedded:storage/*, andembedded:provider/*. These are not documented in this repository; the reference is the standard itself, https://419.ecma-international.org, plus the relevant guides topic. -
Legacy — the following, which should not be used for new work:
- modules/pins — superseded by
embedded:io/*(digital,analog,i2c,smbus,spi,pwm,pulsecount,pulsewidth,serial). Treat the whole directory as deprecated.
Exception: audio. AudioOut is current. It already lives at modules/io/audioout asembedded:io/audio/out, with apins/compatibility shim inside it, and documentation/pins/audioout.md remains the reference. - modules/files — the original
file,flash, andpreferenceAPIs. Preferembedded:storage/filesandembedded:storage/flash. - modules/network — the original networking stack. Prefer
embedded:io/socket/*andembedded:network/*, or the web standard tier above.
- modules/pins — superseded by
Existing examples and tests still use the legacy modules, so you will encounter them. The guidance above is about what to write, not a claim that the old modules are broken.
Preferences is replaced by Key-Value storage
This one is missed constantly, because the two APIs share no vocabulary — nothing in the name "Key-Value store" suggests it supersedes "Preference", so there is no way to make the connection by pattern matching on names.
The Preference module is replaced by the ECMA-419 Key-Value store, or by Web Storage / localStorage if you prefer that wrapper. Web Storage is implemented on top of the Key-Value store.
const kvp = device.keyValue.open({path: "local"});
globalThis.localStorage = new WebStorage(kvp);
guides/key-value/index.md explains both APIs and how to choose between them. The Web Storage implementation is examples/io/storage/webstorage with its manifest_webstorage.json; typings are in typings/web/webstorage.d.ts.
Documentation that describes legacy APIs
These documents are thorough, well written, and carry recent revision dates. They describe the superseded APIs above. Read Choosing an API before following them.
- documentation/pins/pins.md — superseded by
embedded:io/*. Note that its sibling documentation/pins/audioout.md is not obsolete. - documentation/files/files.md — the File, Flash, and Preference sections are legacy. For Preference, see Key-Value storage above.
- documentation/network/network.md — the original Socket, HTTP, WebSocket, MQTT, DNS, and Wi-Fi stack.
contributed is not a reference
contributed holds unofficial projects, mostly unmaintained. Do not treat it as a source of current practice. These are the ones worth consulting:
conversationalAI, first-run-moddable-four, moddable_six, somafm, window-display-moddable-one, window-display-moddable-three
Ignore the rest.
Manifests
If you are adding or changing anything in a manifest.json, read documentation/tools/manifest.md first. Manifests look like ordinary configuration files and invite editing by pattern matching on whatever JSON is nearby. They are not: the build's module list, memory configuration, per-platform behavior, and dead-stripping all live here, and mistakes surface later as failures that do not point back at the manifest.
A manifest describes the modules and resources of an application. Its properties are build, include, creation, defines, config, strip, modules, preload, resources, data, platforms, and bundle. Two rules that are not obvious:
- Every module in the build must be listed in
modules. A file that is present on disk but unlisted is simply not in the build. - Same-named properties concatenate across includes rather than replacing. You cannot override a value inherited from an included manifest by redeclaring it.
See also documentation/tools/defines.md for how defines becomes MODDEF_-prefixed C #defines.
creation — configuring RAM
creation sets the allocation parameters of the XS virtual machine: static, chunk, heap, stack, keys, parser, and main (the module specifier of the entry module). The inherited defaults work for most projects, including nearly all of the examples. Change them only with reason — bigger values are not always better on a constrained device.
Know where the value you are looking at comes from. creation is assembled from several manifests, and the memory sizing is usually not in the project's own manifest:
- examples/manifest_base.json supplies
keys,parser, andmain. It does not set the memory sizes. - The memory sizes —
static,chunk,heap,stack— come from the platform or target manifest underbuild/devices/. Compare build/devices/pico/manifest.json (static: 131072) with build/devices/esp32/targets/moddable_six/manifest.json (static: 0with explicitchunkandheappools). Different targets are configured very differently. - The simulator manifests set no
creationat all.
To see what a build actually resolved to, read the generated manifest_flat.json in the project's build/tmp/<platform>/<target>/<build>/<project>/ directory rather than reasoning about the merge by hand.
staticis the total byte budget for the JavaScript runtime on a microcontroller — a hard ceiling covering the stack, objects, byte code, and strings. It exists so that a script cannot exhaust the memory the host OS needs.staticis ignored by the simulator, which allocates on demand. A project can therefore run cleanly under-p simand fail for memory on a device, with nothing in the symptom pointing back at the manifest. If you only ever test in the simulator, you will not see this.- Setting
staticto0disables the static allocator and switches to split slot and chunk heaps, needed when RAM is discontiguous (ESP32) or when pools have been tuned by hand. It also requires settingchunk.initial,heap.initial, and bothincrementalvalues. Deleting thestaticproperty is not sufficient, because an included manifest may still define it. keys.initialshould be small, since most keys are allocated at build time.keys.incremental: 0prevents any runtime key allocation.
Full detail is in the creation section of documentation/tools/manifest.md, which refers to Machine allocation in XS in C.
preload — code that runs at build time
preload is the other manifest property with consequences that are easy to get wrong, and it has no analogue in Node.js or the browser.
Modules named in preload are executed on your build machine, not on the device. The XS linker runs the module body at build time, then writes the resulting classes, functions, and objects into ROM, where they are used in place rather than copied to RAM. This is why applications here boot instantly and start with so little RAM used.
Consequences:
- A preloaded module body cannot call a native function. Native functions expect the target's environment, possibly a different instruction set, so calling one during preload is a build error. Defining a native function during preload is fine — only calling it fails. This is the most common preload failure.
- A preloaded module body cannot touch hardware, for the same reason. Typically exactly one module — the one that starts your application — cannot be preloaded for this reason.
- Preloaded objects are frozen. The linker automatically freezes classes, functions, prototypes, and built-ins like
MathandJSON. Code that mutates them at runtime still works, via an alias table that clones the object into RAM, but it costs memory and time. Writing code that expects to patch a preloaded object is a design smell here. preloadtakes module names, not paths, unlikemodules.
Read documentation/xs/preload.md before adding a module to preload — it has a "What Cannot be Preloaded" section listing the restrictions and which built-ins are safe to construct at build time.
Tests
Tests live in tests — modules/ mirrors modules/, xs/ covers the engine, and contributed/. They are written in test262 format with YAML frontmatter, and all Moddable tests use flags: [module]. Reference: documentation/tools/testing.md.
Two things to know before trying to run them:
- Tests require a debug build. The test runner communicates over the xsbug channel, so release builds cannot run tests.
- The
testssuite runs only through the xsbug GUI test runner. There is no headless runner for it in this repository — test selection, execution, and reporting are all xsbug UI operations. Running it also requires a test262 checkout with$MODDABLE/testscopied into it.
What is available headless:
xstruns test262 cases directly from the command line on the host. See documentation/xs/xst.md.- tools/test-examples builds, installs, and launches every project in a tree, passing if nothing throws. It drives execution through xsdb.
Native code
Much of the runtime is C beneath a JavaScript API — around 250 .c files under modules use the xsmc macros. If you are editing C in modules/ or xs/, read XS in C first.
It is not a conventional C API, and the ways it goes wrong are memory corruption rather than clean errors. In particular the garbage collector may move data, so pointers obtained from the VM must not be held across an allocation, and native data that references JavaScript objects needs the correct mark hooks. Follow the patterns in a neighboring module rather than inventing an approach — modules/io is the most current native code in the tree.
Conventions
- Indent with tabs, including in JSON manifests.
.editorconfigonly covers markdown, so this is not enforced by tooling. - Every source file carries a license header, and it differs by tree: LGPL "Moddable SDK Runtime" for
modules/**andxs/**, GPL "Moddable SDK Tools" fortools/**andtypings/**, Creative Commons BY 4.0 forexamples/**. Copy the header from a neighboring file in the same tree rather than reconstructing it. See licenses/readme.md. - Markdown in
documentation/opens with an H1, thenCopyright <years> Moddable Tech, Inc.<BR>andRevised: <date>. Pages inguides/instead use YAML frontmatter withname,SPDX-FileCopyrightText, andupdated. - eslint.config.mjs declares the SDK's implicit globals (
trace,device,screen, the Piu surface, and others). It marksSystemandHostas deprecated — do not use them in new code. - Never commit build output or Node artifacts:
build/bin/,build/tmp/,node_modules/,report.json. See .gitignore. - A signed Contributor License Agreement is required before a pull request can be accepted. See contributing.md.
Where to look things up
Start with guides. It is the freshest documentation in the repository — the topic pages carry 2026 dates, while much of documentation/ is older and some of it predates the APIs it describes.
How the guides are structured, which is not obvious from any one page:
- Each guide is a short, task-focused page with runnable JavaScript examples.
- A horizontal rule (
---) separates independent sections, and each section is a short explanation followed by the code block immediately below it. The prose and its example are meant to be read as a unit — a code block lifted out of its paragraph loses the context that explains it. Note the double duty: the first two---in every page delimit the YAML frontmatter, not a section. - Most topics document two APIs — a web standard one (
fetch,WebSocket,localStorage) and a lower-level ECMA-419 one (theClientanddevice.*variants). Both are current; they are tiers, not old and new. The topic index explains how to choose. - Start at the topic's
index.md. Beyond listing the guides, it explains how to choose between the available APIs, gives the build setup needed to use them — manifest include paths formcconfig/mcpack, and module specifiers where relevant — and ends with a "Learn More" list linking the documentation, the relevant standard, runnable examples, and reference implementations.
ECMA-419 is not yet documented in this repository. For the embedded:* APIs, use the standard itself: https://419.ecma-international.org — a single HTML page covering the whole specification. Read it rather than guessing at an API shape. The guides deep-link into it with anchors (#http-client-class-pattern, #storage-keyvalue, #byte-buffer, and so on), so the relevant guide topic is usually the fastest way to the right part of the spec.
Note that documentation/io/io.md is not a reference for ECMA-419: it is a 2021 introduction written while the IO Class Pattern was still a proposal, and it targets only ESP8266.
- documentation/readme.md — API reference, organized per module. Incomplete: it omits
documentation/io/entirely, along with the board guides and the driver documentation. List the directory rather than trusting this index alone. - examples/readme.md — building and running examples, screen formats, Wi-Fi configuration.
- documentation/tools/tools.md —
mcconfig,mcrun,mcrez,png2bmp,xsc,xsl, the simulator, xsdb, andtest-examples. - typings — TypeScript declarations. Projects including
examples/manifest_base.jsonget them automatically;mcconfigcompiles TypeScript itself.