Skip to main content
Your page tells the AI what can be done right here: add to cart, change plan, export a CSV, book a slot. When a visitor asks for one, the AI picks it, the chat asks for anything it couldn’t fill in, confirms, runs your function in the visitor’s browser with their own login, and shows the result.
  • The AI can only pick from the list your page registered. It never reads the page or fills in forms itself, unless you turn on page control for the site.
  • Your functions never leave the page. Only the name, description, input schema and the extras below are sent to the desk.
  • Each run is recorded in the conversation’s AI actions, so agents see what happened.

The standard: WebMCP

Page actions use WebMCP, the browser API for exposing a page’s tools to AI agents, rather than a Jun-only format. Tools you register for Chrome’s own agent work with Jun with no extra code, and actions you register with Jun are visible to other WebMCP agents too.
WebMCP is a W3C draft in a Chrome origin trial, and it changes every few releases. Between Chrome 153 and 156, for example, it moved from navigator to document, deprecated JSON-string inputs, and dropped window.ontool*. Check Chrome’s imperative API and declarative API docs before relying on a detail. This page describes the loader as of October 2026.
Where the browser has no native document.modelContext, the loader installs a small stand-in so the same code works everywhere. Where the browser has it, the loader uses the native one.
Put the snippet in <head> with defer, before your own scripts. Code that checks for document.modelContext once on load must find it there.

Register an action

There are three ways in. All three end up in the same list the AI picks from.
The WebMCP tool shape plus Jun’s extras (pages, instances, highlighting, context, availability, undo, risk, status line, result card). Returns a function that removes the action.
JunDesk.registerAction exists as soon as the loader has run. Registrations made before the actions code has loaded are queued and replayed.

What execute gets and returns

execute(input, signal) runs in the visitor’s browser.
  • input holds the values the AI filled and the visitor confirmed, already checked against your schema (numbers are numbers, enums match exactly).
  • The second argument is an AbortSignal that also has a signal property pointing at itself, so both fetch(url, { signal: opts }) and opts.signal work.
  • It may be async. A thrown error or rejected promise shows as Didn’t work with the error’s message, and the AI hears about it.
What you return is what the AI reads next (up to 2,000 characters): Return JSON when the AI needs data for its next step (search results, a list of slots). Return a sentence when the customer should see what happened.

Fields

Standard WebMCP fields:
string
required
The action’s name, like add_to_cart. Up to 64 characters. The AI sees it as pa_<name>.
string
required
What it does, in one line (up to 200 characters). The AI chooses by this, and the confirm card shows it.
object
A JSON Schema object. Each property can be string, number, integer or boolean, with description, enum (up to 24 values), minimum, maximum and format (email, date and url get matching fields). Up to 12 properties. Properties listed in required that the AI couldn’t fill are asked for in the chat.
function
required
Runs the action. See above.
object
WebMCP hints. Without risk, consequentialHint: true means confirm and readOnlyHint: true means auto.
Jun extras (only through JunDesk.registerAction or useJunAction):
string[]
Offer the action only on matching paths. An exact path ("/pricing") or a prefix ending in * ("/plans/*" matches /plans/pro and /plans/). Left out, it’s offered everywhere it’s registered.
string | number
One instance per item on screen, like one per product card. name plus key must be unique; registering the same pair again replaces the older one. Keyed instances aren’t mirrored into document.modelContext (WebMCP names must be unique there).
Element | RefObject
Highlighted with an outline and scrolled into view while the action runs. Also tells the loader whether the action is on screen, which helps it rank.
object | function
Small facts for the AI, like { price: 89, inStock: true }, or a function returning them. Up to 8 entries; strings up to 80 characters, numbers and booleans.
() => boolean
Checked before the action is offered and again before it runs. When it returns false (sold out, already on this plan), the action isn’t offered, and a run in flight shows No longer on this page.
(result) => void | Promise
Gives the run an Undo button. Receives what execute returned.
"auto" | "confirm" | "human"
default:"confirm"
How it runs. See Risk levels. Overrides annotations.
string
The line the chat shows while it runs, like "Checking fares". Up to 60 characters. Without it, one is made from the name: set_pickup becomes “Setting pickup”, export_csv “Exporting CSV”.
string
A card made on the desk, by its widget name (like "cart_summary"). When execute returns an object, the chat shows it as that card (up to 32 KB of JSON). See Widgets.

Risk levels

When required inputs are missing, the chat asks for them first (“A couple of details first”): a short list of choices (up to 6) becomes buttons, a longer one a select, a yes/no two buttons, a date a date picker, an email an email field. Then the visitor presses Continue.

Override risk on the desk

Your site sets a default, but the desk decides. Add website-actions.yaml to your agent config: a map of action name to auto, confirm or human, or to run: and status:.
website-actions.yaml
Actions it doesn’t name keep the site’s own setting. You can set the same thing without YAML: Agent → Actions has an On your website section listing every website action the desk has seen, with a picker on each card. The override applies wherever actions are offered: live chats, the Agent page’s test chat and jun eval.

After it runs

Once a run starts, it shows as a step under the AI’s answer: a state icon, a title made from the name (“Add to cart”), and a short chip with the result (or the readable inputs). Opening a step shows the description, inputs, and result or error. Several steps fold under one “N steps” header. Undo sits on the step when you registered undo.

Multi-step requests

Some requests take several actions: “find a flight to Lisbon on Friday and add it to my cart” might be a search, then a selection, then an add. After each result, the AI gets one more turn, and in that turn it must decide: call the next action, or finish. So requests chain one action per turn:
  1. The visitor asks.
  2. The AI calls search_flights (auto). It runs and returns JSON.
  3. The AI reads the result and calls add_to_cart (confirm). The visitor confirms. It runs.
  4. The AI writes a short reply, or ends the chain.
Limits that keep chains safe:
  • At most 12 AI messages in a row after the visitor’s message.
  • Calling the same action with the same inputs again ends the chain instead of repeating it.
  • Each confirm step still waits for the visitor.
With the island, the whole run stays one live line (“Checking fares… · step 2”) and only grows when the visitor has to confirm or fill something in.

Many actions on one page

The loader sends up to 60 actions with each message: actions on screen first, then the rest in registration order, then other WebMCP tools. The desk picks up to 30 for each reply: words from the visitor’s question that match an action’s name, description or context count most, being on screen counts too, and registration order breaks ties. Actions defined on the desk (actions/*.yaml) can be limited to pages the same way, with pages: ["/orders/*"]. With page control on, its five actions (read_page, click and the rest) are always offered as well, after your own.

React

useJunAction from packages/widget-react (MIT, React 18+ as a peer, no other dependencies) registers an action while the component is mounted and removes it on unmount. If the loader hasn’t loaded yet, it waits for the jundesk:ready event.
The second argument works like useEffect’s dependency list: the action is registered again when it changes.
The package lives in the repo at packages/widget-react and isn’t published to npm. Copy src/index.ts into your app, or install it from the repo. Its types don’t list status yet; the loader accepts it anyway.

Card buttons that run page actions

A widget card’s button can run one of your page actions: give it the action type page:<name>, for example {"type": "page:remove_from_cart", "payload": {"id": "mug"}}. The press counts as the visitor’s yes, so the action runs at once with the payload and the card’s fields, unless a required input is still missing. A human action still goes to your team. There’s no AI turn for the press.

Test page actions

Eval cases in tests/*.yaml take an actions: list and a page: path, and can expect an action:
tests/plans.yaml
See Tests. To try it by hand, open Chat widget → Install → Open demo page: it registers upgrade_plan (confirm, with undo), export_csv (auto) and a plain WebMCP show_usage.

Actions

Server-side actions that call your API.

JavaScript API

registerAction and everything else on JunDesk.