App authoring
An app on Robutler is a small bundle of files: a manifest, an entry HTML page that loads the App SDK, and optional custom-function source. Apps are built as widgets, so the manifest is widget.json and the build tools are named widget_*. This page is the reference for what goes in the bundle and why.
If you just want to ship something, start with the quickstart, which drives this whole loop with a coding agent.
Project layout
A minimal bundle:
my-app/
widget.json manifest: name, version, tools, UI descriptor
index.html the entry: loads the SDK, renders your app
tools/
hello.js a custom function (one file per declared tool)You can add JS / CSS modules, images, fonts, and an AGENT.md. Text files are written through widget_put_files; binary assets (images, fonts) go through the dev-token upload path. Nested paths auto-create their parent folders.
widget.json
The manifest declares the app's identity, its tools, and its UI. widget_scaffold produces a working starting point:
{
"name": "my-app",
"version": "1.0.0",
"description": "my-app widget",
"tools": [
{
"name": "hello",
"description": "Example tool",
"_meta": {
"robutler": {
"file": "tools/hello.js",
"runtime": "node",
"expose": ["http"],
"path": "/hello",
"httpAuth": "public"
}
}
}
],
"_meta": {
"ui": { "resourceUri": "./index.html", "mimeType": "text/html", "permissions": [] },
"robutler": { "v": 1 }
}
}Key fields:
name,version,description: app identity.namebecomes the app's display name and seeds the slug for its dedicated agent._meta.ui.resourceUri: the entry file,./index.htmlby default. Publish reads it to find your entry HTML._meta.ui.permissions: browser-feature grants the app needs (camera, microphone, and so on). Keep this minimal; see the security model.tools[]: each declared tool maps to a custom-function file and how it is exposed (below).
The full manifest schema, including the CSP allowlist enforced at publish, is documented in Widget manifest.
index.html and the App SDK
The entry HTML loads the App SDK and waits for the host before doing anything:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="robutler:widget"
content='{"title":"My App","description":"My App","size":{"width":360,"height":320},"kind":"iframe"}'
/>
<script src="/widgets/sdk.v2.js"></script>
</head>
<body>
<main id="app">My App</main>
<script>
(async () => {
await host.ready();
// use host.kv / host.content / host.fn / host.discover here
})();
</script>
</body>
</html>The <meta name="robutler:widget"> tag carries render hints (title, default size, kind). The SDK exposes everything through the host.* global once host.ready() resolves. The surface includes:
host.kv: per-instance key-value storage.host.content,host.documents: name-addressed content and documents.host.collab: realtime multiplayer.host.fn: call your app's custom functions.host.discover: discover agents and intents.host.commands: the agent command surface (below).host.infer,host.python,host.shell,host.live,host.user, and more.
Each of these has its own page; start at the App SDK overview.
A note on the SDK script tag: first-party bundles reference the SDK at the portal-absolute path /widgets/sdk.v2.js. The local dev server serves that path too, so a bundle renders the same locally as on the portal.
Loading entry modules
If your entry HTML boots through an inline <script type="module"> that dynamically imports your app code, resolve the specifier against document.baseURI instead of writing it bare:
const app = await import(new URL('./app.js', document.baseURI).href);Apps render inside a sandboxed srcdoc iframe with an injected <base href>. Chrome applies that base when resolving relative module specifiers, but Safari/WebKit does not, so a bare import('./app.js') loads in Chrome and fails in Safari with Module name, './app.js' does not resolve to a valid URL, and the app never boots. Only the first hop from the inline module script needs this: static imports inside app.js resolve against its own (absolute) URL, and a classic <script src="./app.js"> like the example above is unaffected because the browser applies <base> to src attributes.
Custom functions
Each entry in tools[] points at a source file (default tools/<name>.js) that exports a handler. The scaffold's example:
export default async function hello(ctx) {
return {
status: 200,
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ ok: true, ts: (ctx && ctx.request && ctx.request.body) || null }),
};
}The _meta.robutler block on the tool controls wiring:
file: source path. Defaults totools/<name>.js.runtime:nodeby default.expose: where the function shows up.httpadds an HTTP endpoint atpath;tooladds an agent-callable tool. The default when omitted is both (["http", "tool"]).path: the HTTP route, default/<name>.httpAuth:publicfor an anonymous endpoint, or a session-gated mode. Choose carefully; see Widget content auth.
At publish, each declared tool's source must exist at its file path or the publish fails. Publish wires the functions onto the app's dedicated agent and exposes them per expose.
The 64 KB custom-function cap
A custom-function source larger than 64 KB is rejected at deploy (CODE_TOO_LARGE) and the function will 404 at call time (FN_NOT_FOUND). If you are embedding data inline, minify it or move it out of the function body. See Error codes.
From inside the app, call a function with host.fn:
const res = await host.fn('hello', { name: 'world' });The agent command surface
To make your app drivable by agents (and by your coding agent over workspace_widgets_invoke), declare a command interface. The entry HTML can declare it inline in the robutler:widget meta, as commands (and optional events):
<meta
name="robutler:widget"
content='{"title":"My App","commands":{"addItem":{"description":"Add an item","input":{"text":"string"}}}}'
/>At publish, this interface is captured and stamped on the app so the canvas and the workspace_widgets_* tools can list and call it. First-party core apps declare their commands server-side in the registry instead; custom apps use the inline form above. Wire the handlers with host.commands.
The full model, including how commands are dispatched and how to handle them, is in Agent command interface.
Next
- Quickstart: build and publish end to end.
- Local dev: preview and debug locally.
- App SDK overview: the full
host.*surface. - Widget manifest: the complete
widget.jsonschema. - Publishing and remix: ship, version, and fork.
Quickstart: build your first app
End to end with a coding agent over MCP: connect, scaffold, edit against the dev server, publish, snapshot, and remix your first Robutler app.
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.