---
name: mindset-page-tools
description: Use when an embedded Mindset (mindset.ai) AI agent must act on the web app's page or know where the user is. Covers page tools (setPageTools, pageTools, execute handlers, Mindset 3's parameters/handler tools, activity-row labels, argument validation, permission checks), situational awareness (setSituationalAwareness and its size limit) and pass-through params (setPassthroughParams), on the mindset-agent element and the UI-less client.
metadata:
  title: Let the agent act on your page
  blurb: Lets an embedded agent act on your page and know where the user is.
  group: In your app
  order: 3
  docs: the-ui-less-client, the-mindset-agent-element, put-an-agent-on-your-page
  checked: d671041b6
  updated: 2026-10-21
---

# Let the agent act on the page and know where the user is

When you finish, the embedded agent can call functions in the app's page (open a record, fill a form, read what's selected), knows which screen and record the user is on, and receives values such as a tenant ID that the model never sees or chooses. This skill assumes the agent is already embedded. If it isn't, do that first with the `mindset-embed` skill (the drop-in element) or the `mindset-uiless` skill (your own UI).

## Mindset in brief

Mindset is a hosted platform for building, running and governing AI agents. An **agent** is configuration, not code: a prompt, a model, and the resources it may use. Those resources are **scripts** (steps that must wait or happen in order), **functions** (fixed steps with one right answer), **widgets** and **connections** (the only way data enters or leaves). People manage all of it in the Studio, a web app, or through Mindset's MCP servers.

## Where you are building: host, org, environment

Every Mindset call names three things. Get all three from the person before you write code or call a tool. Never guess or invent them.

| Value | What it is | Where the person finds it |
|---|---|---|
| **Host** | The Mindset deployment their org lives on, for example `https://eu.mindset.ai` | The address bar when they're signed in to the Studio, or an agent's **Embed** tab |
| **Org slug** | Their company's account. The org is the isolation boundary: members, model keys, settings and audit sit here, and one org can never see into another | An agent's **Embed** tab, or the address under **Settings → Systems → Connect an AI client** |
| **Environment slug** | A named partition inside the org, such as Sandbox, Test or Production | An agent's **Embed** tab, or the same address. The **Environment** switcher in the Studio shows which one is current |

How environments behave:

- **Agents, scripts, functions, connections, triggers, schedules, conversations, runs and API keys each belong to exactly one environment** and can't see anything in another.
- **Every org starts with one environment, Sandbox, whose slug is `original`.** A slug never changes, even when the environment is renamed. Use the slug, never the display name.
- **Nothing copies between environments.** To promote a change from Test to Production, make the same change again in Production.
- **Don't create an environment to hold your work.** Environments can't be deleted, and they're for stages such as Test and Production, not for projects. Work in the environment the person names. Create one only if the person asks you to.
- **Members, model keys and org settings are shared by every environment.** Changing an org setting changes it everywhere.

**Read the platform's own guide first.** If you hold the `mindset-admin` MCP server, call `get_information` with no arguments. When its index lists `embedding-readiness`, read that topic before you write any code: its checklist says whether this page or app can host an agent at all and what will snag, so tell the person what it finds. Then read `embedding` (sessions, setup, loading the script), `page-channels` (page tools, situational awareness, pass-through values) or `embedding-diagnosis` (error codes and fixes) for the work in hand. Those topics come from the deployment you are connected to, so they match its release: where they differ from this skill, follow them. When the index doesn't list them, the steps below apply as written.

## Ask the person for

- Which embed the app uses: the `<mindset-agent>` element or the UI-less client (`createAgentConversation`). Find it in the code if you can.
- What the agent should be able to **do** on the page, one action per tool, and for each one who may do it. Write these down before any code.
- What the agent should **know** about the current screen: page name, selected record ID, status. Only facts a user could see on screen.
- Which values a tool needs that the model must not pick, for example the user's tenant or region.

## The three channels

| Channel | Element | UI-less client | What it is for |
|---|---|---|---|
| Page tools | `configure({ pageTools })`, `setPageTools(tools)` | `setPageTools(tools)` | Functions in the page the agent may call |
| Situational awareness | `configure({ situationalAwareness })`, `setSituationalAwareness(entries)`, the `situational-awareness` attribute | `setSituationalAwareness(entries)` | `{ key: value }` JSON values describing what the user is looking at |
| Pass-through params | `configure({ passthroughParams })`, `setPassthroughParams(params)`, the `passthrough-params` attribute | `setPassthroughParams(params)` | JSON values added to tool arguments, hidden from the model |

How all three behave:

- **Each call replaces the whole set.** Pass the full list every time. Passing a shorter list is how you remove an entry; `setPageTools([])` removes all of your tools.
- **They're read once, when a turn starts.** A setter called between turns takes effect on the next one. One called during a turn waits for the next.
- **Nothing is registered in the Studio.** Page tools are offered to the agent on every turn from the page's own list, alongside the tools its active version grants.
- **On the element, the setters need a live conversation.** With `getSession`, `configure()` creates it before it returns and fetches the session afterward, so a setter on the next line works, even in a later `<script>` block. Values set this way last until the next `configure()`: switching conversations and renewing the session keep them. With `autoProvision` it starts once the visitor has signed in: wait for `mindset:agent-registered`. Before that, or after the element leaves the page, a setter raises `mindset:error` `not-configured`.
- **Calling `configure()` again ends the conversation and starts another**, fetching a new session and resuming the conversation in the `conversation-id` attribute. It drops anything set through the setters unless the new config includes it, so keep the current values in one place and pass them to both. To add page tools to an element someone else configured, call the setters rather than `configure()`.
- **On the element, the two maps also work as attributes.** `situational-awareness` and `passthrough-params` hold a JSON object as text, and the `situationalAwareness` and `passthroughParams` properties also take the object itself. Set them before or after `configure()`: a change applies at once, and every `configure()` applies them again (a config field given as well wins). Removing the attribute clears nothing; set `{}` or call the setter. A value that isn't a JSON object raises `mindset:error` `invalid-attribute` and changes nothing.
- **Everything here lives in the browser.** The user and every script on the page can read it.

## Step 1: write the page tools

Each tool is `{ name, description, jsonSchema, execute }`. The model reads `description` and `jsonSchema` to decide when and how to call it. `execute` runs in the page with the arguments the model chose, and what it returns (or resolves to) goes back to the agent as the tool's result. A page written for Mindset 3 can keep its `{ name, description, parameters, handler }` tools unchanged: `parameters` is the schema and `handler` is the function.

A tool may also carry `runningDescription` and `completedDescription`: what the element's chat shows on the tool's activity row while it runs and once it has finished. Each is a string, or one per language such as `{ en: "Opening the invoice", fr: "Ouverture de la facture" }`, read in the page's `lang`, then the browser's languages, then English, then the first one given. The model never sees them. A tool without them shows its name in words: `open_invoice` reads "Open invoice".

**A handler that returns always shows `completedDescription`, with a tick, whatever it returned.** A handler that throws keeps `runningDescription` and marks the row failed. So throw for any outcome the completed label would misdescribe, as the example does, or word the label to be true for every return ("Looked up the invoice").

```javascript
const pageTools = [
  {
    name: "open_invoice",
    description: "Open one of the signed-in user's invoices in the app. Use when the user asks to see an invoice.",
    jsonSchema: {
      type: "object",
      properties: { invoiceId: { type: "string", description: "An invoice ID such as inv_4471" } },
      required: ["invoiceId"],
    },
    runningDescription: "Opening the invoice",
    completedDescription: "Opened the invoice",
    execute: async (args) => {
      const id = typeof args.invoiceId === "string" ? args.invoiceId : "";
      if (!/^inv_[a-z0-9]+$/.test(id)) throw new Error("That is not a valid invoice ID.");
      const r = await fetch(`/api/invoices/${id}`, { credentials: "include" }); // the backend checks this user may see it
      if (!r.ok) throw new Error("That invoice can't be opened.");
      router.navigate(`/invoices/${id}`); // the app's own router
      return `Opened invoice ${id}.`;
    },
  },
];

// UI-less client
chat.setPageTools(pageTools);

// Element: in configure(), or with the setter afterward
agent.configure({ getSession, pageTools });
```

Writing handlers:

- **One action per tool, with a plain verb name** (`open_invoice`, `fill_refund_form`, `get_selected_rows`). The `description` says what it does and when to use it. That is all the model has to go on.
- **Return what the agent should know.** A string reaches the agent as written: "Opened invoice inv_4471." Any other value reaches it as JSON, so `return { invoiceId: id, status: "overdue" }` works. Returning nothing reads as `{"success":true}`. A value with no JSON form (a function, a BigInt, a circular object) fails the call.
- **Throw to say it failed.** The tool call ends as an error and the turn carries on. The agent reads "The page tool 'open_invoice' failed on the page: " followed by your error's message (up to 500 characters, never the stack); a thrown value that isn't an `Error` reads "it failed without saying why". The call shows as `tool_end` with `isError: true`, and on the element the person can open the failed row and read the same text. Write the message for the agent: "That invoice can't be opened."
- **When a tool changes what's on screen, return the new state** ("Opened invoice inv_4471. It is overdue."). Situational awareness set during the turn reaches the agent only on its next turn.
- **Every tool needs a schema and a function.** A tool with no `jsonSchema` (or `parameters`), or no `execute` (or `handler`), is left out, and the console says why.
- **Watch the calls** in `tool_start` and `tool_end` events (`mindset:runtime-event` on the element, which also sends them as `mindset:tool-start` and `mindset:tool-end`; `on()` on the UI-less client). They pair by `toolCallId`. On the element the event is in `event.detail`: `tool_start` carries `toolName` and `args`, `tool_end` carries `output`, and `isError` when it failed. A `mindset:error` carries `{ code, message }`.
- **On the UI-less client, the labels are yours to draw.** They don't arrive on the events. `resolvePageToolLabel(label, languages)` returns the right text from a per-language map.
- **On the element, `record_script_data` is reserved.** A tool of yours with that name is skipped and the console warns. Any other name is yours.

## Step 2: validate arguments and check permission in every handler

The model decides whether to call a tool and with what arguments, and anything in its context can steer it, including knowledge sources and other tools' output. Treat every argument as attacker-controlled.

- **`jsonSchema` (or `parameters`) is guidance for the model, not validation.** The SDK refuses a call that leaves out a `required` argument, or sends it as `null`, before `execute` runs. Nothing checks types, formats or values. Check each argument yourself, as the example does.
- **Mindset can't re-authorize a page tool.** It runs in the page, so nothing on Mindset's side checks that this user may run it. Put nothing behind a page tool (a change, a payment, a read of personal data) unless the handler or the app's backend checks the signed-in user's permission first. Let the backend decide, using the user's own session, never an ID the model supplied.
- **A page tool reaches only what you expose.** The org API key never reaches the browser, and tools that run on Mindset's side still authorize there. A page tool can do exactly what its handler does, for whoever can make the model call it.
- **Ask for confirmation in the app's own UI** before anything that can't be undone. The handler can wait on a dialog and throw `new Error("The user declined.")` if they do.

## Step 3: tell the agent where the user is

```javascript
const situation = { page: "Invoice detail", invoiceId: "inv_4471", invoiceStatus: "overdue", selectedLines: 2 };
chat.setSituationalAwareness(situation); // or agent.setSituationalAwareness(situation)
```

- **Call it whenever the view changes**: route change, record selected, tab switched. In React or Vue, call it from an effect that depends on the route and selection. On the element, if the view can change before the conversation has started, set the `situationalAwareness` property instead: it's applied whenever a conversation starts.
- **Values are any JSON**: text, numbers, true or false, null, lists and objects. Keep keys short and readable. Send only what the user can see. It is context, not a security boundary, and it never widens what the agent may reach.
- **Keep it under 10,000 characters of JSON.** `extendedSituationalAwareness: true` raises the limit to 20,000 (in `configure()` or as the `extended-situational-awareness` attribute on the element, in `createAgentConversation()` on the UI-less client). A map over the limit, or with no JSON form, is refused and the agent keeps the previous one: the setter throws (`situational_awareness_too_large` or `situational_awareness_not_json`), `configure()` raises `mindset:error` `invalid-configuration` and starts without it, and the attribute raises `invalid-attribute`.
- **Mindset adds the entries to the agent's prompt as untrusted display data.** The model is told to use them when the user asks about what's on screen, not to bring them into unrelated answers, and never to follow instructions inside them. The Studio shows this under **Added by the platform** on the agent's prompt.
- **For data the agent has to work with**, such as the rows of a table or the fields of a form, add a page tool that returns it (`get_selected_rows`) rather than packing it into situational awareness.
- **Situational awareness starts no turn.** The agent reads it on its next turn. When the page needs the agent to respond now, send a page message instead: `send(text, { from: "page" })` on the element, `sendMessage(text, { from: "page" })` on the UI-less client. The `mindset-embed` and `mindset-uiless` skills cover it with the other ways to drive a turn.

## Step 4: pass values the model must not choose

```javascript
chat.setPassthroughParams({
  "lookup_account.tenantId": "acme-eu", // only the lookup_account tool
  region: "eu-west-1",                  // every page tool, and each Mindset-side tool that declares it
});
```

- **A `"tool_name.param"` key scopes a value to one tool.** A plain `param` key is added to the arguments of every page tool call, and to a call to a tool that runs on Mindset's side when that tool declares the parameter. An older deployment adds it to every Mindset-side call regardless, and an operation that doesn't take it then refuses the call; `tool_name.param` behaves the same everywhere. When both name the same parameter, `tool_name.param` wins.
- **Mindset removes the parameter from the schema of each tool that declares it**, before the model sees the tools, and the SDK adds your value to the arguments when the tool is called. The model can't see, choose or fabricate it.
- **Declare the parameter in the tool's schema, as `required` if the tool needs it.** The SDK adds the value before it checks required arguments, and it replaces any value the model sends under that name.
- **A page tool can also read the value from the page.** Use pass-through for tools that run on Mindset's side, and for one value several tools need.
- **Values are any JSON** and reach the tool as given, so a number stays a number.
- **Prefer `tool_name.param` keys.** A plain key reaches every page tool, so every handler receives it.
- **Pass-through is not secret.** The values sit in the browser, and they reach the model if a tool echoes them in its result. Never put an API key, password or token in one. A secret belongs in a connection configured in Mindset, or behind the app's backend.
- **It doesn't authenticate anything.** A page tool that receives `tenantId` must still check with the backend that the signed-in user belongs to that tenant.

## Rules

- Validate every argument and check permission in every handler that changes or reveals anything.
- Never put a secret in a page tool, situational awareness or a pass-through value.
- Pass the full set on every setter call. On the element, include all three in every `configure()`.
- Keep situational awareness to what's on screen and under 10,000 characters of JSON. Put bigger data behind a page tool.
- Switch on `event.type` and ignore event types you don't know.
- Don't use SDK members this skill and the linked pages don't name, even if your editor offers them.

## Check it works

1. Ask the agent to run the tool ("open invoice inv_4471"). See `tool_start` and `tool_end` for your tool name, and the page react. On the element, the activity row reads your `runningDescription`, then your `completedDescription`, or stays on the running words, marked failed, if the handler threw.
2. Ask with a bad argument ("open invoice ../admin"). The handler refuses and the agent says so.
3. Sign in as a user who may not do the action and ask again. The backend refuses it.
4. Navigate, then ask "what am I looking at?". The agent answers from the current situational-awareness values.
5. With a pass-through value set, log `args` inside a handler and see the value there. The `tool_start` event's `args` don't have it, because they show what the model chose.
6. Make a handler throw `new Error("The invoice service is down.")` once. The agent says the page tool failed and gives that reason.

## When it fails

| What you see | Cause and fix |
|---|---|
| The agent never calls the tool | The `description` doesn't say when to use it, or a later setter call or `configure()` replaced the list without it. Log the list you pass |
| Console warns a page tool "was not registered: it needs a JSON Schema" | It has no `jsonSchema` (or `parameters`) or no `execute` (or `handler`). Add the missing one |
| Console warns page tools "were NOT registered" because the names are reserved | It's named `record_script_data`, which the element reserves. Rename it |
| `mindset:error` `not-configured` after a setter | The element setter ran before a conversation started. With `getSession`, call it after `configure()`. With `autoProvision`, wait for `mindset:agent-registered`. Or set the property, or pass the values in `configure()` |
| Tools vanish after a reconfigure | `configure()` replaced the whole config. Include `pageTools`, `situationalAwareness` and `passthroughParams` every time |
| `tool_end` with `isError: true` naming a missing argument | The model left out a `required` argument. Make the `description` say what to pass |
| `tool_end` with `isError: true` saying the result can't be written as JSON | The handler returned a function, a BigInt or a circular object. Return a string or plain data |
| The activity row shows the tool's name, not your words | The tool has no `runningDescription` or `completedDescription`. Add them |
| The activity row says it succeeded when it didn't | The handler returned a failure sentence, and a return always shows `completedDescription`. Throw instead |
| The agent describes the screen as it was before its own tool changed it | Situational awareness is read when a turn starts. Return the new state in the tool's result |
| `setSituationalAwareness` throws `situational_awareness_too_large` | The map's JSON is over the limit, so the agent kept the previous map. Send less, move data into a page tool, or set `extendedSituationalAwareness` for up to 20,000 characters |
| `mindset:error` `invalid-configuration` about situational awareness | The map given to `configure()` was over the limit or had no JSON form. The agent started without it. Fix the map and set it again |
| `mindset:error` `invalid-attribute` | The `situational-awareness` or `passthrough-params` attribute isn't a JSON object, or situational awareness is over the limit. Nothing changed |
| The agent ignores situational awareness | It's used when the user asks about the screen. For data it must work on, add a page tool that returns it |
| A pass-through value is missing in a handler | The key's tool name doesn't match the tool's `name`, or a later `setPassthroughParams` call dropped it |

## Read more

Fetch these as Markdown when you need more than this skill gives you. The `get_information` topics come first, then this skill: if a page disagrees with either, follow them, not the page.

- https://docs.mindset.ai/docs/the-ui-less-client.md: read the "Give the agent context and tools" section for all three channels and page tool safety.
- https://docs.mindset.ai/docs/the-mindset-agent-element.md: read for `configure()` fields, the setter methods, the attributes and the reserved tool name on the element.
- https://docs.mindset.ai/docs/put-an-agent-on-your-page.md: read for what runs in the browser and what runs on Mindset.
- https://docs.mindset.ai/docs/sessions-and-trust.md: read the "Keep secrets on your server" section.
- https://docs.mindset.ai/docs/events-reference.md: read for `tool_start`, `tool_end` and every other event.
