Loom

Type to search across the guides, specification and decision records.

↑↓ navigate ↵ open esc close Pagefind

Start here

Cheatsheet — working with Loom

Card II of two. The command line, the scaffolds, the tests, and the operator passes — plus the handful of things that reliably bite. Every writing command is a dry run until --apply. Card I is what the kernel gives you.

20 plugin seams · 21 conformance kits (loom plugin seams shows which seams have one) · every log command takes --config or --db (knowledge, verify, deploy check are --config only)

Day one

# unpublished: build a loom checkout once (pnpm install && pnpm build), then
node <loom>/tools/create-loom-app/dist/index.js my-app --loom-path <loom>
cd my-app && pnpm install && pnpm test && pnpm start

loom new flow checkout      # scaffold a definition (+ its flow.test.ts), then run gen
loom new flow refund --template approval   # or echo | timer | saga: non-agentic starters, each with a passing test
loom new tool charge --no-gen   # scaffold only; skip the gen that normally follows
loom gen --watch            # keep loom.gen.ts current as modules are added/removed/renamed
loom dev --config ./loom.config.ts   # gen --watch + run the app (restart on change) + the inspector
                            # [--dir app] [--run <command>] [--port N] [--verbose]
loom doctor                 # what can I check right now?
loom logs --help            # per-command help (also: loom help logs, loom version)
loom doctor --json          # machine output: stdout is one JSON document, exit code unchanged
loom doctor --strict        # a check that could not run is a failure (--require verify,timers)
loom completion zsh         # also bash | fish: tab completion from the command table

Outside the checkout --loom-path is required (create-loom-app refuses without it); inside it (<loom>/apps/my-app) no flag is needed. loom new names are kebab-case; a kind that imports a package your package.json lacks (sentinel) prints the line to add. loom new takes flow · agent · tool · skill · schedule · memory · projection · sentinel. The folder convention is build-time codegen — a static loom.gen.ts, never runtime globbing; loom gen skips a file whose name is unsafe to put in generated code.

Writes stay inside the project. gen, new, dev, appgen, plugin new, create-loom-app and migrate-config --apply (and the appgen lockfile) never write through a symlink below the cwd, judged by filesystem identity (dev+ino), not path spelling. An explicit --dir outside the cwd is its own root, but its final component may never be a link — pass the real path.

<store-flags> — where a command reads

Store is a flag (--db / --config), never a positional: a bare path is not a store: alone it is refused, and next to --db it is read as the execution id (loom logs --db x.db events.db). Both halves work for every log command; knowledge, verify and deploy check need --config.

--config ./dist/app.jsthe stores your app wired — SQLite, Postgres, or any conforming backend. Importing it starts nothing.
--db events.db [--snap s.db] [--branches b.db] [--leases l.db]SQLite shorthand, one file per store. A flag you omit is a store the command cannot consult — and it says so rather than reporting zero.

Leases and placement exist because you run more than one node — and a multi-node deployment is not a file you can pass to --db. Use --config for anything real.

Before you ship

loom doctor [--config m]runs every pre-flight check it can, and names the ones it could not — timers, lease scope (process/host/cluster), plaintext snapshots under encrypting events, archive safety
loom checkstatic flow-graph checks from the compiler manifest
loom verifyruntime verification against a built config
loom lint claim-checkflags claim-check reach-through inside flows
loom evalreplay golden logs against expectations
loom deploy checksimulate a deploy against new flow definitions
loom compatvalidate ABI / event-schema versions
loom migrate-config [<file>...] [--config f] [--apply] [--json]rewrites flat defineConfig options into their groups (dry-run diff first; refuses a spread, computed key or non-literal config; never runs the config; default ./loom.config.{js,ts})
loom kernel-manifestkernel version + maturity manifest

Reading doctor’s four marks

markmeaningfails the run?
✓ran, and passedno
✗ran, and found a defectyes (exit 1)
–skipped — the input it needs was not thereno
!advisory — it ran, and whether the finding matters depends on how you deployno

all checks passed and passed, but 2 could not run are deliberately different sentences.

Inspecting a run — read-only

loom ls <store-flags>list executions; what each suspended one waits on (--status, --waiting-on, --since 1h; newest activity first)
loom logs <store-flags> <id>the event timeline (--redact a,b; -v detail; --type/--from/--to; --follow tails it)
loom debug <store-flags> <id> --at Nfolded state at sequence N — time travel
loom inspect <store-flags>web timeline with a slider (loom dev is the dev loop, not an alias)
loom profile <store-flags> <id>flame graph + latency breakdown
loom introspect <store-flags> <id>execution / task / decision / resource graphs
loom optimize <store-flags> <id>advisory recommendations
loom snapshot show <store-flags> <id> [--at SEQ]a stored snapshot, decoded
loom learn report|evaltrajectories, coverage, shadow-evaluate a policy
loom census <store-flags> [--buckets 5m,1h,6h,24h] [--sample N]what is stuck, and is anything watching it (takes no predicate)
loom compliance reportdata-subject inventory
loom knowledge list|searchwhat a corpus holds; ask it

Operating it — writes, dry by default

loom resume <id> --config <mod> --as <who> --event <type> --payload-file f.json [--apply]answer a suspended gate through your app’s runtime: its resume authenticator, execution authorizer and resumeSchemas all run (ADR 0077/0117). --db is refused; --as is required; the continuation runs in this process. The dry run shows the parsed payload (stripped keys named) and the park’s sequence. A kill switch that does not declare scope: "shared" refuses --apply unless --allow-local-controls; unknown lease liveness unless --force; --skip-effect-check skips the registered-effect check.
loom cancel <id> --config <mod> --as <who> [--apply]cancel through the runtime (fenced; runs nothing). A live or unknown lease holder refuses unless --force.
loom start <flow> --config <mod> --as <who> --input '{…}' [--id i | --idempotency-key k] [--apply]start a flow through the runtime; input shown as given (schema predicted only when the app enforces it); the flow runs in this process until it completes or parks. A kill switch that does not declare scope: "shared" refuses --apply unless --allow-local-controls.
loom recover <store-flags> (--stale 15m | --config <mod> with timers.leases) [--apply]re-drive executions whose supervisor died. Needs timers.leases or an explicit --stale — loom will not guess. --apply requires --config, because re-driving runs your flow. --max-recoveries N skips and lists executions whose last N re-drives made no progress (ADR 0105).
loom repair blank-stream <store-flags> (--snap s.db | --config <mod>) [--assign id:from-through] [--apply]re-home events the pre-0106 compaction bug wrote to execution "" — proven segments only; the rest is listed with candidates, never guessed (ADR 0107)
loom retention run <store-flags> (--ttl <dur|ms> | --policy <module>) [--apply]reclaim aged terminal executions (--ttl or an app-owned --policy is required: there is no default; ADR 0145). Without a branchRegistry the live-descendant guard is off.
loom repair terminal-event <store-flags> (--snap s.db | --config <mod>) [id] [--apply]give an execution a pre-0116 compaction left as [0]+checkpoint its terminal event back, so it can take a late outcome (ADR 0116)
loom branches retire <store-flags> (--branches b.db | --config <mod>) --parent <id> --stale <d>cancel branches nobody is coming back to — they keep the parent uncollectable
loom branches prune <store-flags> (--branches b.db | --config <mod>) [--protect main]delete refs that name nothing; main is protected
loom sweep content --content <db>pinned-blob sweep over externalized payloads. --orphans also erases blobs nothing scanned cites — it needs the snapshot store and every archive; dry-run first
loom knowledge sync --applyreconcile a directory into a corpus (also removes)

Heartbeat leases (ADR 0130 D11) are the default evidence that a driver is gone: with a co-located lease store the sweep re-drives an execution whose lease expired, and refuses one whose lease is live. Details: operations.

The preview is the pass. Each dry run is the same code path as --apply, so it cannot promise an outcome the real run won’t deliver — and it claims no lease while looking.

Testing — @loom/testing

createTestApp(config?)in-memory app; storage defaults to memory
createTestApp({ clock: true })time travel: the runtime’s now is a test clock and a TimerService waits for you (autostart stays off — it never polls on its own; fireDueTimers throws if it does)
advanceTime(app, ms) / fireDueTimers(app)move the clock, then fire what became due through the real TimerService (ctx.sleep, suspend({ timeout }), cron). fireDueTimers drains: it polls until nothing is due, so resumes are finished on return; a timer re-arming at an already-due instant throws instead of looping
driveToSuspend(handle)start → first suspend
resumeWith(runtime, id, ev?)resume sugar
waitFor / waitForPhasepoll a predicate or a surface phase
getSurfaceStateread a surface
captureGolden(store, id)freeze a log as a fixture
recordingProvider / replayingProviderVCR for LLMs: record once, replay offline
crashingEventStoreinject a store failure — prove recovery, don’t assume it
@loom/dstdeterministic simulation: simWorld, faultInjectingStore, virtualClock, seeded fault schedules checked against INVARIANTS

Repo evals (not a user command): pnpm eval scores retrieval, agentLoop$, appgen and planner over committed cassettes; pnpm eval:update-baseline [--allow-partial] after an intended change. pnpm eval:record --live re-records against a real model, on demand only (see evals/README.md).

Rules the repo enforces on tests: wait on a condition, never a fixed sleep; no performance.now() or timing ratios in *.test.ts (put them in *.perf.test.ts, run with pnpm test:perf); bind test servers to 127.0.0.1, never a bare listen(0).

Authoring a plugin

loom plugin seams              # 20 seams, and how each is wired
loom plugin new lease my-redis
loom plugin check --publish    # npm-readiness too; needs a real @loom/conformance import AND a real run*Conformance( call
loom plugin try --module ./dist/index.js [--preset <name>] [--options <json>] [--verbose]
loom plugin search <term>

Seams: store · dispatcher · telemetry · middleware · timer · lease · content · durable-state · read-model · keystore · memory · projection · worker · llm · authorizer · execution-authorizer · resume-authenticator · timer-dispatcher · policy · sandbox.

Run the conformance kit. Every seam with one ships run<Spi>Conformance(factory) — it covers the concurrent cases (two owners racing, re-apply, renewal after expiry) that hand-written tests miss.

Generating an app from intent

loom appgen "<intent>" [--dir .] [--dry-run] [--check] [--yes] [--offline]   # --yes skips the confirm prompt; a plan that deletes a flow/step (either planner) always asks, and without a TTY needs --yes
loom appgen --from <file> [...]                       # read the intent from a file
loom appgen "<intent>" --model gemini:<m>   # or anthropic:<m> | openai:<m> | a .gguf path; [--model-big] [--model-tier]
loom appgen --from-spec .loom/appgen/<ts>.spec.json   # replay, no model

Deterministic and offline in the output: no model call at generate time, and none in the generated app. With no --model it plans with the deterministic keyword parser and says so on stderr (silent with --offline / --from-spec); --dry-run ends with a re-run hint. The plan validator refuses duplicate step ids, ids whose generated names collide, non-kebab new ids and control / bidi / format characters; every codegen sink escapes model text.

Things that bite

  • A package’s tests import its siblings’ dist. Edit a dependency’s src and nothing changes until you rebuild it — sourcemaps will point at pre-edit line numbers.
  • pnpm typecheck does not see tests. Package tsconfigs exclude them, so a type-level proof in a test proves nothing until pnpm typecheck:tests (build first) runs it.
  • Inner loop: pnpm build:fast (one tsc process) and pnpm test:src (siblings resolved to source). Before a PR: pnpm build && npx turbo test --force --concurrency=2 && pnpm typecheck && pnpm typecheck:tests && pnpm lint. CI runs only on ready PRs — open drafts.
  • tsc --build trusts .tsbuildinfo. Delete dist and it may rebuild nothing; use --force.
  • Three rules see past declared steps only with a compiler manifest. Without one, loom verify says so rather than reporting clean.
  • needsApproval: true parks the tool call; the deprecated requireApproval: true meant proceed. Same log, opposite polarity — rename, don’t just alias.
  • A strict resume schema changes what is persisted. z.object drops unnamed keys from the log for good; agentLoop$ approvals need viewDigest to pass through.
  • Grouped config options throw when spelled flat — there is no deprecation window.
  • Global install ≠ pnpm link. pnpm’s global store is tied to its major version, and this repo pins one via packageManager.

Source: docs/cheatsheet-dx.md in the loom repository.