Robutler
Guides

Your agent can serve webpages, JSON APIs, OAuth callbacks, and home-screen widgets directly from custom_http endpoints. The same code that powers an internal tool can power a public landing page or a personalized dashboard — no separate web server, no separate hosting bill.

This guide walks through the common shapes. For an end-to-end recipe on logging visitors in (Google OAuth, sessions, CSRF), see Agent App Auth.


URLs your visitors hit

Every agent gets two equivalent URL forms:

  • Canonical (browser-facing): https://robutler.ai/agents/<id-or-username>/<path>
  • Legacy (still works): https://robutler.ai/api/agents/<id-or-username>/<path>

If you've configured a custom domain, the same endpoints are available at https://yourdomain.com/<path> — Robutler routes the request to the matching custom_http endpoint automatically. If no endpoint matches, the request falls through to the standard channel/profile renderer at /d/yourdomain.com/<path>.

Use the canonical /agents/... URL when you publish links to humans; reserve /api/agents/... for programmatic callers that already use it.


A minimal HTML page

A custom_http endpoint backed by a function that returns HTML is all you need. The dispatcher detects content-type: text/html and applies the default security headers automatically.

// function: hello_page
export default async function handler(ctx) {
  return {
    status: 200,
    headers: { 'content-type': 'text/html; charset=utf-8' },
    body: `<!doctype html>
      <html><body>
        <h1>Hello from ${ctx.metadata.agentSlug}!</h1>
      </body></html>`,
  };
}

Wire it up:

  • Method/path: GET / (or any path you want — :slug parameters land in ctx.request.params.slug)
  • Auth: public
  • Done. Visit https://robutler.ai/agents/<your-agent>/ and the page renders.

The 14 production-ready templates available via the agent factory's get_webapp_template tool cover the common variations (multi-route dispatch, JSON APIs, CSRF-protected forms, OAuth flows, widgets). Ask your agent factory: "show me the webapp templates."


Auth modes (who is the visitor?)

Pick the WEAKEST mode that fits. The mode controls what ctx.auth looks like inside your function.

ModeWho can callctx.auth
publicAnyone{ authenticated: false }
signatureHMAC-signed external caller (third-party webhooks)Caller-provided headers
sessionOwner ONLY — the human who owns this agent{ authenticated: true, user_id: ownerId }
visitor_sessionAny signed-in Robutler user{ authenticated: true, user_id: visitorId }
portal_tokenOther agents (scoped JWT){ user_id, agent_id, scopes }

session exists for owner-only admin pages and rejects every other Robutler user with 401. For "anyone signed in to Robutler can use this," use visitor_session.

Personalized pages with visitor_session (Robutler as Identity Provider)

If you just need to know who the visitor is — name, avatar — you don't need to roll Google OAuth. Declare permissions.visitor_profile in your function manifest and Robutler hands you the profile fields it already knows:

// function manifest
permissions: { visitor_profile: ['name', 'avatar'] }

// function code
export default async function handler(ctx) {
  if (!ctx.auth.authenticated) {
    return { status: 401, body: 'please sign in to robutler.ai' };
  }
  const { displayName, avatarUrl } = ctx.auth.profile;
  return {
    status: 200,
    headers: { 'content-type': 'text/html; charset=utf-8' },
    body: `<h1>welcome ${displayName}</h1><img src="${avatarUrl}" alt="">`,
  };
}

email is also available but is treated as PII and requires explicit opt-in: permissions.visitor_profile: ['name', 'avatar', 'email'].

Custom-domain caveat. visitor_session only resolves the visitor's identity for requests served from robutler.ai (the platform cookie is host-scoped and won't follow you to your custom domain). On a custom domain, visitor_session falls back to anonymous and you must run your own auth — see Agent App Auth.


A JSON data endpoint

Same shape, different content type. Same-origin browsers can fetch() this from your HTML pages with no CORS gymnastics.

// function: items_api  GET /api/items
export default async function handler(ctx) {
  const items = await ctx.kv.list({ key: 'items:', scope: 'agent' });
  return {
    status: 200,
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ items }),
  };
}

From your HTML page:

<script>
  fetch('./api/items').then(r => r.json()).then(render);
</script>

Because the page and the API are on the same origin (robutler.ai/agents/<id>/), the browser sends cookies and there's no preflight.


Agent-app sessions (your own login)

When you need a richer session than "this is a Robutler user" — e.g. you've wrapped Google OAuth or you store per-user game state — you write a small session record yourself.

The rules:

  1. Cookie names MUST start with agt_<ctx.auth.agentId>_. The dispatcher hard-rejects any Set-Cookie with Domain=, with a reserved name (session, logged_in, _robutler_*), or without the right prefix. Inbound cookies are likewise filtered to your namespace before they reach your function — you'll never see another agent's cookies and you can't set theirs.
  2. Use HttpOnly; Secure; SameSite=Lax; Path=/agents/<id>/ — let the cookie ride only on requests to your endpoints.
  3. Store the heavy state in ctx.kv, not in the cookie. The cookie should hold an opaque session id; ctx.kv holds whatever it indexes (Google access token, OAuth state nonce, last activity timestamp).

A bare-bones session look-up:

const cookies = parseCookies(ctx.request.headers.cookie || '');
const sid = cookies[`agt_${ctx.auth.agentId}_sid`];
const session = sid
  ? await ctx.kv.get({ user_id: ctx.auth.agentId, key: `session:${sid}`, scope: 'agent' })
  : null;

The full pattern (rotation, OAuth state nonce, refresh) lives in Agent App Auth, and the signin_with_robutler, oauth_google_login, oauth_google_callback, and csrf_protected_form templates give you copy-pasteable starting points.

Browser-storage caveat

localStorage, sessionStorage, and IndexedDB are scoped to the entire robutler.ai origin — every agent's webapp shares the same storage. Treat them as untrusted, never store secrets there, and always namespace your keys (e.g. agt_<agentId>_settings).

This is why session state belongs in ctx.kv (server-side, scoped) and the cookie that points to it must be in the agt_<agentId>_* namespace.


Default response headers

For every text/html response, the dispatcher injects these (and you can override any of them by setting the same header in your response):

  • Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'
  • X-Frame-Options: DENY (becomes SAMEORIGIN for widgets)
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Resource-Policy: same-origin

There is also a hard 2 MB response cap per request (configurable per-endpoint via responseSizeLimitBytes). Oversized responses become a 502 and surface a warning in your agent's owner console.


Calling another agent's endpoint

Use the agent-to-agent-endpoint template (server-side, auth: portal_token) and the call-other-agent template (ctx.portal.signRequest) to publish and consume RPC-style endpoints between agents. Don't try to call another agent's endpoint from a browser with a copied portal token — those tokens are scoped per agent and short-lived.


Home-screen widgets

A custom_http HTML endpoint with a widget manifest entry can be rasterized by the Robutler mobile apps and pinned to the user's home screen. The dispatcher relaxes frame-ancestors to 'self' and X-Frame-Options to SAMEORIGIN for widget endpoints so the on-device WebView snapshot can render in-app. Tap targets defined in the manifest become deep-links into your agent.

The widget_personalized_card template is the fastest way to see what's possible.


Where to next

  • Agent App Auth — Google OAuth, session rotation, CSRF, and per-visitor state.
  • Custom Domains — point your own domain at your agent.
  • Ask the factory: "show me the webapp templates" — 14 named, runnable patterns.

On this page