run_code API reference

run_code({ slug, code }) evaluates an async arrow function in a sandbox attached to the space's Workspace, and JSON-serializes whatever you return:

run_code({ slug, code: `async () => {
  await state.writeFile("/index.html", "<h1>hi</h1>");
  return await state.glob("**/*");
}` })

Six namespaces are in scope: state.* (files), git.* (repo), host.* (build/deploy/previews), config.* (asset config + variables/secrets/egress), media.* (durable assets), introspect.* (read deployed SQLite). Arguments are positional, exactly as shown below.

Escaping: code is a JS expression, so a literal ${…} in source you author is interpolated at parse time. Escape it as \${…}, use String.raw, or — simpler — drop verbatim source with the write_files tool instead.


state.* — Workspace files

Paths are absolute (/src/index.ts). readFile/stat/lstat throw ENOENT if missing; exists never throws; glob returns sorted absolute paths.

Read & inspect

await state.readFile("/src/index.ts")              // → string
await state.readFileBytes("/logo.png")             // → Uint8Array
await state.exists("/package.json")                // → boolean
await state.stat("/src")                           // → { type, size, mtime, … } (follows symlinks)
await state.lstat("/link")                         // → stat without following the final symlink
await state.readdir("/src")                        // → string[] (names)
await state.readdirWithFileTypes("/src")           // → [{ name, isDirectory, … }]
await state.glob("src/**/*.{ts,tsx}")              // → string[] absolute paths
await state.walkTree("/", { maxDepth: 3 })         // → nested tree node
await state.summarizeTree("/")                     // → { files, dirs, bytes, … } aggregate
await state.find("/", { type: "file" })            // → [{ path, … }] flat listing
await state.diff("/a.txt", "/b.txt")               // → unified diff string
await state.hashFile("/dist/app.js")               // → content hash string

Write & mutate

await state.writeFile("/index.html", "<h1>hi</h1>")
// Advisory: writeFileBytes is for text you happen to hold as bytes — NEVER media.
// Images/fonts/video/audio go to media (media.write / get_urls dest=media),
// not the git workspace.
await state.writeFileBytes("/data/seed.csv", bytes) // bytes: Uint8Array
await state.appendFile("/log.txt", "line\n")
await state.mkdir("/assets", { recursive: true })
await state.rm("/tmp", { recursive: true })
await state.cp("/a.txt", "/b.txt")
await state.mv("/old.txt", "/new.txt")

Search & bulk edit

await state.searchText("/", "TODO")                          // → [{ path, line, text }]
await state.searchFiles("src/**/*.ts", "useState")           // → matches grouped by file
await state.replaceInFile("/src/app.ts", "v1", "v2")         // single file
await state.replaceInFiles("src/**/*.ts", "v1", "v2")        // glob-wide
await state.planEdits([{ /* instruction */ }])               // → an edit plan (preview)
await state.applyEdits([{ /* edit */ }])                     // apply edits atomically

JSON helpers

await state.readJson("/data.json")                           // → parsed value
await state.writeJson("/data.json", { a: 1 })
await state.queryJson("/data.json", "$.items[0].name")       // JSONPath query
await state.updateJson("/data.json", [{ /* op */ }])         // structured update

Archives

await state.createArchive("/out.zip", ["/src", "/public"])
await state.extractArchive("/in.zip", "/unpacked")

git.* — the Workspace as a repo

Options objects; dir defaults to the repo root / (omit it). git.add stages by filepath (singular).

await git.status({ dir: "/" })                               // → working-tree status
await git.add({ filepath: ".", dir: "/" })                   // stage everything
await git.commit({ message: "feat: x",
  author: { name: "Agent", email: "agent@unfolder.space" }, dir: "/" })
await git.log({ dir: "/" })                                  // → commit history
await git.branch({ list: true })                             // → { branches, current }  ← deploy target
await git.branch({ name: "feat-x" })                         // create (does NOT switch)
await git.checkout({ ref: "feat-x" })                        // switch HEAD
await git.deleteBranch({ ref: "feat-x" })                    // delete a branch (never main / the current branch)
await git.merge({ theirs: "feat-x",                          // LOCAL merge (ff + clean 3-way)
  author: { name: "Agent", email: "agent@unfolder.space" } })
await git.rm({ filepath: "old.ts", dir: "/" })
await git.diff({ dir: "/" })

init/clone/fetch/pull/push/remote exist but are for real remotes (e.g. GitHub) — a space has no remote by default, so use git.merge (not fetch/pull/push) to move commits between local branches. Full promote flow: workflow.


host.* — build, deploy, previews

await host.build()           // bundle HEAD → { commitSha, mainModule, moduleCount, assetCount, warnings }; refuses dirty / no-commits
await host.deploy()          // build + deploy the CHECKED-OUT branch → { target, branch, facetName, url, … }; main → production, else a stable preview
await host.listPreviews()    // → [{ branch, previewId, facetName, commitSha, url }]
await host.stopPreview({ branch: "feat-x" })  // tear down a preview facet (the git branch is left intact)

deploy targets whichever branch is checked out (git.branch({ list: true })'s current). Commit and ship in one call:

async () => {
  await git.add({ filepath: "." });
  await git.commit({ message: "feat: x", author: { name: "Agent", email: "agent@unfolder.space" } });
  return await host.deploy();
}

Custom domains are separate top-level tools (set_custom_domain / get_custom_domain / remove_custom_domain), not part of host.*.


config.* — asset config, variables, secrets, egress

await config.getAssetConfig()                                // → AssetConfig | null
await config.setAssetConfig({ /* headers, redirects */ })    // see bundling
await config.setVariable({ name: "API_BASE_URL", value: "https://api.example.com" })
await config.declareSecret({ name: "STRIPE_KEY" })           // owner sets the value in settings
await config.allowEgress({ host: "api.github.com" })         // owner approves in settings
await config.list()                                          // → { variables, egress } — never secret VALUES
await config.remove({ name: "API_BASE_URL" })                // by variable/secret NAME, or rule by HOST

Secret values and host approvals are owner-only, done on the settings page (get_urls returns the settings URL). Secrets and egress are independent — a secret is injected into env (owner sets the value); outbound hosts are a separate allow/deny list. Full model: secrets and bundling.


media.* — durable assets served by URL

await media.list()                                           // → [{ path, size, uploaded, contentType }]
await media.list("photos/")                                  // filter by prefix
await media.read("notes.txt")                                // → text, or null
await media.write("data/site.json", "{}", "application/json")
await media.remove("old.png")
await media.url("photos/hero.jpg")                           // public → https://<your-space-slug>.<domain>/photos/hero.jpg
await media.url("_private/deck.pdf", { expiresIn: 600 })     // private (`_*`) → signed, time-limited URL

For binary uploads, don't pass bytes here — call get_urls for a sessionUpload URL and PUT out-of-band. Precedence and private (_*) media: media.


introspect.* — probe the deployed facet

Read deployed SQLite

await introspect.query({ sql: "SELECT id, body FROM notes ORDER BY id" })
await introspect.query({ sql: "SELECT * FROM notes WHERE id = ?", params: [1] })
await introspect.query({ sql: "PRAGMA table_list", facet: "production" })

Read-only (a single SELECT/PRAGMA/EXPLAIN) against the deployed facet's runtime DB — this is the only way to read it from run_code, since state.* is the source Workspace, not the live worker. Writes go through your worker's HTTP API. Requires the deployed App to keep the scaffold's __query export. Background: dynamic-worker.

Self-test custom admin views

await introspect.adminFetch("/forms")                                   // GET your __adminFetch view
await introspect.adminFetch("/settings", { method: "POST",              // POST a form (new Request init)
  headers: { "content-type": "application/x-www-form-urlencoded" }, body: "name=hi" })
await introspect.adminFetch("/forms", undefined, "preview:ab12")        // probe a preview facet

Probes your facet's custom admin view hook (__adminFetch) the same way the platform admin does — but from the sandbox, with no interactive owner login — so you can verify a view renders before handing it off. The arguments are the standard new Request(input, init) pair plus an optional third facet (defaults to production); a bare path like /forms is fine — the host is ignored, only the path + query reach your hook. To probe a preview, pass the facetName that host.deploy() / host.listPreviews() hand back (e.g. preview:ab12) — no need to build the preview: name yourself. The probe runs as the space owner.

Returns the response serialized to JSON — { ok, facet, status, statusText, headers, body, truncated } (body is text, capped) — not a Response. A facet with no __adminFetch hook → { ok: false, code: "UNSUPPORTED" }; an unknown/not-live facet → NOT_DEPLOYED. Add the hook per admin-prep.