Robutler

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. name becomes the app's display name and seeds the slug for its dedicated agent.
  • _meta.ui.resourceUri: the entry file, ./index.html by 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 to tools/<name>.js.
  • runtime: node by default.
  • expose: where the function shows up. http adds an HTTP endpoint at path; tool adds an agent-callable tool. The default when omitted is both (["http", "tool"]).
  • path: the HTTP route, default /<name>.
  • httpAuth: public for 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

On this page