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
| Form | Targets |
|---|---|
'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:
| Mode | Who can call |
|---|---|
public | Anyone, including signed-out /w/<id> visitors. Use for read-only demo data. |
session | The authenticated viewer (the default for canvas apps). |
visitor_session | Per-instance visitor scope (owner-only analytics roll-ups). |
portal_token | Another agent over inter-agent RPC. |
signature | An 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 (checkhost.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).
Related
- host.discover: find an agent, then call one of its endpoints
- host.agents: drive a granted agent's conversation
- host.content: store the bytes a function returns
- widget.json manifest: declaring
tools[]