Workflow for editing space ""

The loop

  1. Read. state.glob("**/*"), state.readFile, state.searchText.
  2. Edit. state.writeFile, state.replaceInFile. Multi-file edits: state.planEdits + state.applyEdits.
  3. Build (validate). host.build() (inside run_code) bundles the working tree as a dry-run and returns { snapshotHash, fromCache, assetCount, warnings }. Fix any warnings. Optional — host.publish() always builds first, so a standalone build() is only for checking.
  4. Publish (go live). host.publish() builds + swaps the single live facet, returning { ok, status, live, snapshotHash, instanceId, url?, visibility? }url + visibility: "public" appear once live is true; a failure carries error instead.
  5. Showcase. place.add("/the-page", "Label") — register the page as a place so the owner sees it as a live frame in the dashboard the moment it ships (a /__-prefixed place stays owner-only; the edge 404s /__* to the public). See run-code.

The working tree is the source, and one space has one live facet. The snapshotHash is the content identity of a build (a pure function of source files + asset config).

Elaborate example: stateful counter

This builds a tiny stateful Worker that persists a counter across requests using the App DO's KV storage (see dynamic-worker for capabilities). The whole flow happens through one run_code call.

async () => {
  // Write the worker source. Named `App` export — required by the bundler
  // (see bundling: unfolder://docs/bundling).
  await state.writeFile(
    "/src/index.ts",
    `import { DurableObject } from "cloudflare:workers";

export class App extends DurableObject {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === "/reset") {
      this.ctx.storage.kv.put("counter", 0);
      return new Response("reset");
    }
    let counter = this.ctx.storage.kv.get("counter") ?? 0;
    counter++;
    this.ctx.storage.kv.put("counter", counter);
    return new Response("count=" + counter + " path=" + url.pathname);
  }
}
`,
  );

  // Go live in the same call.
  return await host.publish();   // → { ok: true, status: "complete", live: true, snapshotHash, instanceId, url, visibility: "public" }
};

To validate without going live, call host.build() first:

async () => host.build()      // → { snapshotHash, fromCache, assetCount, warnings }
async () => host.publish()    // → { ok, status, live, snapshotHash, instanceId, url: ".../", visibility } — url once live

A request to https://<your-space-slug>.unfolder.space/hello returns count=1 path=/hello, and the second request returns count=2. The live facet preserves the App DO's SQLite across publishes, so the counter survives a republish.

Reading and exploring

state.* (exposed inside run_code) is the full file API. Drop into run_code and call the workspace primitives directly. To preserve template literals (e.g. ${name}) verbatim in a string you're writing, use write_files instead — those bytes are not JS-evaluated.

// Walk the workspace (nested tree, max depth 3)
async () => state.walkTree("/", { maxDepth: 3 })

// Find files by pattern
async () => state.glob("src/**/*.{ts,tsx}")

// One-level listing
async () => state.readdirWithFileTypes("/src")

// Read a text file
async () => state.readFile("/src/index.ts")

// Read a binary file as base64 (MCP transport is JSON; pick an encoding)
async () => {
  const bytes = await state.readFileBytes("/photo.png");
  return { encoding: "base64", contents: btoa(String.fromCharCode(...bytes)) };
}

// Search content across files (searchText is single-file — use searchFiles for a glob)
async () => state.searchFiles("**/*", "TODO")

// Aggregate counters for a subtree → { files, directories, symlinks, totalBytes, maxDepth }.
// (The `unfolder://spaces/<your-space-slug>` resource carries a separate whole-workspace
// aggregate as `workspaceInfo` — { fileCount, directoryCount, totalBytes, r2FileCount }.)
async () => state.summarizeTree("/")

Iterating safely

Edits live in the working tree until you host.publish(), so iterate freely with state.* and host.build() (a dry-run, no go-live) until the build is clean, then publish once. The live facet's SQLite is preserved across publishes, so persisted state survives a republish.