Relay docs
Implementation

Widget actions

Let the AI agent do things on your page — declare actions in the dashboard, announce and handle them on your site.

Widget actions let Relay's AI agent operate your own page on the customer's behalf: open an order, print a receipt, start a walkthrough — anything your frontend can do. The agent calls the action as a tool; Relay delivers it to the live widget session; your page handles it and responds. This works for any site that embeds the Relay widget — a SaaS dashboard, a Shopify app's admin page, or a storefront.

How it fits together

  1. Declare the action in Relay: Settings → Products → your productActions. You author the name (snake_case), a label, the description the AI reads, an optional JSON Schema for arguments, whether the customer must confirm first, and a timeout.
  2. Announce on your page which declared actions the current page can serve, with relay.capabilities([...]).
  3. Handle the DOM event relay:capabilities:<name> and call respond(...) exactly once.

The security model is deliberate: everything the model reads (names, descriptions, schemas) comes from the dashboard. The page can only narrow what the agent may do — announcing an undeclared name does nothing, and pages that announce nothing expose no actions at all. Conversations on channels without a live page (WhatsApp, email) never see these tools.

Announce capabilities

Announce after the widget script loads, and re-announce when your SPA navigates to a page with different abilities. Announcements reset the list (they don't accumulate).

<script>
  window.$relay = window.$relay || []
  // Works before or after widget.js loads:
  window.$relay.push(["capabilities", ["print_receipt", "open_order"]])
</script>

Or, once the widget is ready:

relay.capabilities(["print_receipt", "open_order"])

Handle an action

Relay dispatches a CustomEvent on document named relay:capabilities:<name>. The event's detail carries the validated args and a single-use respond callback. A catch-all relay:capabilities event fires for every action (useful for logging).

document.addEventListener("relay:capabilities:print_receipt", (event) => {
  const { args, respond } = event.detail
  try {
    window.print()
    respond({ ok: true, result: { note: "Print dialog opened" } })
  } catch (error) {
    respond({ ok: false, error: String(error) })
  }
})

document.addEventListener("relay:capabilities:open_order", (event) => {
  const { args, respond } = event.detail
  router.push(`/orders/${args.orderId}`)
  respond({ ok: true })
})

Rules:

  • Call respond once. Extra calls are ignored.
  • Respond quickly — the agent waits up to the action's timeout (default 15 s) and then tells the customer it couldn't complete the action.
  • respond({ ok: true, result })result is passed back to the AI as data (kept small; large payloads are truncated). respond({ ok: false, error }) gives the AI a reason it can relay honestly.

Confirmation-gated actions

Mark an action Confirm first in the dashboard for anything with side effects. The gate is enforced by Relay's server, not by prompting: the action refuses to run unless the customer's previous message was an explicit yes. The agent is instructed to ask ("Want me to open the order for you?") and run only after the confirmation.

Example: Shopify storefront

The same mechanism, on a storefront that embeds the widget:

window.$relay = window.$relay || []
window.$relay.push(["capabilities", ["add_discount_to_cart"]])

document.addEventListener("relay:capabilities:add_discount_to_cart", async (event) => {
  const { args, respond } = event.detail
  const response = await fetch(`/discount/${encodeURIComponent(args.code)}`, { redirect: "manual" })
  respond({ ok: response.ok || response.type === "opaqueredirect" })
})

Declare add_discount_to_cart in the dashboard with a schema like:

{
  "type": "object",
  "properties": { "code": { "type": "string", "description": "Discount code to apply" } },
  "required": ["code"],
  "additionalProperties": false
}

Reference

PieceValue
Announcerelay.capabilities(string[]) or $relay.push(["capabilities", string[]])
Event per actionrelay:capabilities:<name> on document
Catch-all eventrelay:capabilities
Event detail{ name: string, args: object, respond: (r: { ok: boolean, result?: unknown, error?: string }) => void }
Action name formatsnake_case, max 60 chars, unique per product; built-in tool names are reserved
TimeoutPer action, 1–60 s (default 15 s)
AuditEvery execution is recorded and visible to your team

On this page