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
- Declare the action in Relay: Settings → Products → your product → Actions. 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. - Announce on your page which declared actions the current page can serve, with
relay.capabilities([...]). - Handle the DOM event
relay:capabilities:<name>and callrespond(...)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
respondonce. 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 })—resultis 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
| Piece | Value |
|---|---|
| Announce | relay.capabilities(string[]) or $relay.push(["capabilities", string[]]) |
| Event per action | relay:capabilities:<name> on document |
| Catch-all event | relay:capabilities |
| Event detail | { name: string, args: object, respond: (r: { ok: boolean, result?: unknown, error?: string }) => void } |
| Action name format | snake_case, max 60 chars, unique per product; built-in tool names are reserved |
| Timeout | Per action, 1–60 s (default 15 s) |
| Audit | Every execution is recorded and visible to your team |