Robutler

host.fn

host.fn invokes a custom function that runs on the server, not in the iframe. Use it for anything the browser should not do directly: hitting a third-party API with a secret key, heavy compute, writing to a shared store, or returning data that must be authoritative.

interface FnNamespace {
  invoke<R = unknown>(ref: string, args?: Record<string, unknown>): Promise<R>;
}

The call is server-mediated. It runs as the current viewer (so it sees their auth), but billing and quotas land on the app-instance owner. That makes host.fn the right place for a shared, owner-funded capability every viewer of a shared app can use.

The ref argument

FormTargets
'forecast' (bare name)An endpoint on your own bundle's agent.
'robutler/<endpoint>'The system robutler agent.
'<agent>/<endpoint>'A granted agent only. Arbitrary agents are rejected.
await host.ready();

const data = await host.fn.invoke('forecast', { city: 'Lisbon' });
render(data);

In detached mode (host.detached === true) there is no host bridge, so invoke rejects with unknown_op.

Declaring a function

Functions are declared in the bundle's widget.json tools[], each pointing at a source file in the bundle:

{
  "name": "Weather",
  "tools": [
    {
      "name": "forecast",
      "description": "Return a forecast for a city.",
      "_meta": {
        "robutler": {
          "file": "tools/forecast.js",
          "runtime": "node",
          "expose": ["http"],
          "path": "/forecast",
          "auth": "public"
        }
      }
    }
  ]
}
// tools/forecast.js: runs server-side; secrets stay here, never in the iframe.
export default async function forecast(ctx) {
  const { city } = (ctx.request && ctx.request.body) || {};
  const r = await fetch(`https://api.example.com/forecast?city=${encodeURIComponent(city)}`, {
    headers: { authorization: `Bearer ${process.env.WEATHER_KEY}` },
  });
  return { status: 200, headers: { 'content-type': 'application/json' }, body: await r.text() };
}

widget_publish wires each declared tool onto the bundle's dedicated agent as a custom function.

Auth modes

The auth field controls who may call the function:

ModeWho can call
publicAnyone, including signed-out /w/<id> visitors. Use for read-only demo data.
sessionThe authenticated viewer (the default for canvas apps).
visitor_sessionPer-instance visitor scope (owner-only analytics roll-ups).
portal_tokenAnother agent over inter-agent RPC.
signatureAn HMAC-signed caller.

public functions are what make a shared deck or board render for signed-out visitors; session and visitor_session only resolve for apps mounted on a real canvas.

The 64KB size cap

Custom functions in the js-v1 runtime have a hard size limit. A function source larger than 64KB is rejected at publish time with CODE_TOO_LARGE. The function is then stored as a stub, so calling it later returns 404 FN_NOT_FOUND. If you embed data (a lookup table, a template, a model) inline in the source, minify it or move it out of the function body, or you will silently ship a broken endpoint. Keep function source lean and load large assets at runtime instead.

Errors

  • unknown_op: the function name is not declared, or you are in detached mode (check host.detached).
  • CODE_TOO_LARGE / FN_NOT_FOUND: the 64KB cap pitfall above.
  • Auth and quota failures surface per the error codes.

Keep heavy data out of the response when you can. host.fn round-trips through the host bridge, so it is for authoritative results, not bulk streaming (use host.live for that).

On this page