Publishing and remix
Three operations turn a bundle into a living, shareable, forkable app: publish, snapshot, and remix. Apps are built as widgets, so the tools are widget_publish, widget_snapshot, and widget_remix. This page covers what each does and how they relate. For arguments and return shapes, see MCP build tools.
Publish
widget_publish takes a bundle folder you own and deploys it as an app. It runs as a single transaction, so a mid-pipeline failure rolls back cleanly (no orphan agents, no half-wired functions). It is idempotent per (author, folder): republishing updates in place.
In one pass it:
- validates that you own the folder and that it has the entry HTML named by
widget.json, - reads and validates
widget.json(file-path safety, CSP allowlist), - mints (or reuses) a dedicated agent for the app, user-scoped to you,
- wires each declared tool as a custom function on that agent: it adds an HTTP endpoint when
exposeincludeshttp, and an agent-callable tool when it includestool, and share-stamps the source so a clone can re-read it, - stamps the app's command interface (so it is drivable on the canvas and over
workspace_widgets_*), - upserts the catalog row and the post row.
It returns { postId, agentId, widgetContentId, publicMcpAppUrl }. The publicMcpAppUrl is the app's own outbound MCP App manifest at /api/widgets/<postId>/mcp: every published app is itself reachable as an MCP App. A republish replaces stale tool wiring rather than merging it, so a tool you remove actually disappears.
To put the published app on a canvas, call widget_instantiate with the widgetContentId and open the returned canvasUrl.
Snapshot: immutable versioned releases
Publishing keeps a single working app you edit in place. widget_snapshot cuts an immutable, content-addressed version of that working app, so you have a frozen release that does not change when you keep editing.
Snapshotting freezes the bundle tree by cloning it. The clone shares each file's storage pointer with the live folder; a later edit to the live folder diverges (copy-on-write) and leaves the snapshot frozen. So a snapshot is cheap, and unchanged files dedup across versions. Each version records its Merkle treeHash and its parent version, forming an immutable version DAG.
Publishing a version is decoupled from listing it:
listed: falseproduces an unlisted share: a public app row, no marketplace post.listed: trueadditionally creates or repoints a marketplace post.
Snapshotting is idempotent: if the tree is byte-identical to the last version (same treeHash), it is a no-op that reuses the prior artifacts and just clears the dirty flag. On an update you can omit listed to reuse the prior choice (inferred from whether a post already exists), and pass a message for a commit-style version note.
It returns { versionId, widgetContentId, treeFolderId, treeHash, postId, deduped }.
Remix: fork a published app
widget_remix forks any published app (including your own) into your account. For a folder-bundle app it:
- clones the bundle subtree onto your account,
- replays the publish pipeline on the clone, minting a fresh dedicated agent for your fork and creating its own catalog and post rows,
- stamps the remix lineage on your copy (source content id, source post id, source author), and
- bumps the source post's remix count.
It returns { newPostId, newAgentId, newWidgetContentId, ordinal }. You then edit and publish the fork as your own.
Lineage
A remix is attributed to its source. The fork's display name is composed as "<base> - Remix by @<username>", and the remix-of pointers thread the chain so a remix-of-a-remix re-attributes to the latest remixer rather than stacking suffixes. When you later snapshot the fork, the version records a best-effort fork link back to the source's published version, so the version graph spans forks too.
Remixing a collaborative app
A fork inherits the source app's boot order, and for the realtime apps that order is load-bearing rather than incidental. The sequence that looks like ceremony — wait for a real room sync, only then bind the editor, stage any local store before merging it — is what stops a user editing a document that is open but not connected and losing the work on reload.
If your remix only changes UI, styling or commands, you inherit the correct behaviour and there is nothing to do. If you rewrite the boot or swap the editor, read host.collab first. Two edits in particular reintroduce silent data loss:
- replacing the sync wait with a timeout (
Promise.race([synced, sleep(n)])), which turns "could not connect" into "carry on as if connected", - attaching
y-indexeddbdirectly to the live doc, which lets a stale local copy merge back and resurrect deleted content, and - dropping the
readerguard while adding a new editing surface, which lets a view-only visitor type into a copy that reaches nobody.
If you add an editing path the original did not have (a new panel, a new tool,
a new command), route it through the same guard the app already uses:
blockWrites / freezeYTypes from /widgets/shared/collab-readonly.js.
Neither shows up in casual testing: the app looks healthy, presence may even be live, and the loss only surfaces on someone else's reload.
What is not remixable
Some kinds reject a remix:
- passive and native external widgets,
- system widgets,
- live canvas items (a running instance, not a bundle).
These return a remix error rather than forking.
Earning from your app
Listing a snapshot in the marketplace is how your app reaches people, and app usage is how you earn: makers earn through usage-based revenue sharing. See Earn.
Next
- Quickstart: the full loop, including remix.
- App authoring: how
tools[]andexposedrive the wiring above. - MCP build tools: exact arguments and return shapes.
Local dev server
Run a Robutler app bundle on localhost with the experimental dev server: serve your folder, proxy the platform API with a dev token, and preview and debug.
MCP build tools
Reference for the app build and authoring MCP tools (widget_*) and the live-app control tools (workspace_*): arguments, return shape, and notes.