Page playbooks
Tell the agent which page the visitor is on, so it opens the conversation already knowing what they came for.
A chat bubble in the corner is easy to ignore, and the visitor who does click it lands on an empty box with no idea what to ask. Page playbooks fix both ends: the agent knows which page it was opened from, greets the visitor accordingly, and offers a few questions they can send with one click.
What a playbook is
Three things, attached to a key you choose:
| Field | Who sees it | What it does |
|---|---|---|
| Background | The agent only | What this page is about and what visitors typically worry about |
| Opening line | The visitor | The first thing they read when they open the chat |
| Starter questions | The visitor | Up to four one-click questions |
The opening line and questions can be written by you, or generated by the agent from the background in whatever language the visitor's browser is set to.
Matching a page
You never send page content from the browser — only an identifier. The platform holds the text. (See Why the content stays server-side.)
By URL — nothing to change on the page
Give the playbook a URL rule and the widget matches it automatically:
/pricing matches /pricing, /pricing/, /pricing?utm_source=x
/solutions/* matches /solutions/legal, /solutions/insurance
*/solutions/legal matches /solutions/legal AND /zh/solutions/legalOnly the path is compared — host, protocol, query string and trailing slash are ignored, so one rule
covers www and the bare domain, http and https.
On a localized site, /zh/solutions/legal does not match /solutions/legal. Write
*/solutions/legal to cover every locale prefix.
By key — for pages that share a URL
Single-page apps, modals, and multi-step flows often have no distinct URL. Name the playbook explicitly instead:
<script src="https://chat.agent4.io/ui/embed.js"
data-token="YOUR_SHARE_TOKEN"
data-page-key="checkout-step-2" async></script>If the page changes without a reload, tell the widget as the route changes:
caWidget.setPage("checkout-step-3");Resolution order
- An explicit key, if one was given
- The first matching URL rule
- The default playbook, if you have one
- Nothing — the visitor gets the plain chat box
An explicit key that doesn't exist falls through to the default rather than quietly matching some URL rule. That way a typo shows up as "the default appeared" instead of "the wrong playbook did".
Set one up
In the console, open Page playbooks and create one. Pick the page type first — pricing page, solution page, docs, case study, home, contact — and the questions come pre-filled with what visitors usually ask on that kind of page. Then rewrite them in your own voice.
The list view has a matcher: paste any URL and it tells you which playbook that page will get, and whether it matched by key, by URL rule, or fell through to the default.
Or via the API:
curl -X PUT https://api.agent4.io/v1/manage/page-contexts/pricing \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{
"label": "Pricing page",
"url_pattern": "*/pricing",
"context": "The visitor is looking at our pricing. Four tiers separated by monthly token quota and end-user count. The usual questions are which tier fits their size and what happens when they go over.",
"greeting_mode": "generated"
}'Writing the background
This is the part that does the work. A few things worth knowing:
One language is enough. The model reads whatever you write and answers in the visitor's language. You don't need a translation per locale.
Don't restate facts that live in your knowledge base. Prices, quotas and limits are answered from the knowledge base, which outranks the playbook — a number written here will not override it. Write about situation instead: who lands on this page, what they're trying to decide, what they tend to worry about.
Write what the visitor is doing, not what the page says. "The visitor is comparing tiers and trying to predict their monthly bill" is more useful than a summary of the page copy — the agent can already look the page copy up.
Static or generated openings
greeting_mode: "static" uses the exact opening line and questions you wrote, for every visitor in
every language. Full control, no surprises.
greeting_mode: "generated" has the agent write them from your background, in the visitor's
language. The first visitor in each language triggers one generation; everyone after that is served
from cache. Editing the background clears the cache, so your next visitor sees the update.
Generated is the better default for a multilingual site. Static is right when the wording is load-bearing — regulated industries, or a page where you've tuned the sentence with a copywriter.
A generated opening can carry a form. When the playbook background explicitly says to collect
details at first contact ("present the form in your greeting"), the opening arrives with an
interactive form already in it — channel, contact field, whatever the background asks for — and the
starter questions step aside. Submitting it is a normal message, so the agent's tools
(save_contact, schedule_followup) fire as usual. Our contact page's "Talk to a human" card is
exactly this: the booking form is on screen before the visitor types a word.
Why the content stays server-side
The widget reports a key or a URL. It never uploads the page text, and the platform never reads your DOM.
This is deliberate. Anything the browser sends can be edited by the person sitting in front of it, and page context lands close to the agent's instructions. We tested this: with a fabricated "this month Enterprise is $199 with unlimited seats" planted in a page-context block — explicitly fenced as data, with the agent instructed that prices come only from the knowledge base — the model repeated the fake price to the visitor as fact. Fences and careful wording did not hold.
Keeping the text on the server removes the channel entirely. The worst a visitor can do is ask for a different key and receive a playbook you wrote yourself.
The trade-off worth knowing: playbook copy is semi-public. Anyone who guesses a key gets that playbook's opening line. Don't put internal notes in there.
Entry points inside your content
A bubble in the corner is easy to ignore. The moment someone actually has a question is while they're reading a particular paragraph — so you can put an entry point right there:
<p data-ca-ask="overage-billing"
data-ca-question="What happens if we go over, and can we cap the spend?"
data-ca-label="Ask about this">
When the quota is exhausted, chat requests return 429 …
</p>Hovering the passage reveals a small prompt. Clicking it flashes the page in your brand colour, highlights that passage, then opens the chat already working from that passage's playbook — and, if you supplied one, asks the question immediately.
| Attribute | Meaning |
|---|---|
data-ca-ask | The playbook key to open with — required |
data-ca-question | Sent as the visitor's first message — optional |
data-ca-label | Text on the hover prompt (default: "Ask about this") |
Passages added later — by a framework, a tab, an accordion — are picked up automatically. Call
caWidget.rescan() if you render content in a way that escapes that.
data-ca-ask takes a key, not the paragraph text. The playbook it names is what the agent
reads, and that lives on the server. data-ca-question is the exception: it becomes the visitor's
own message, and visitor messages are untrusted by definition — they could have typed it themselves.
Wiring your own buttons
Any element on your page can open the conversation on a chosen playbook — the pattern for turning an existing "Contact sales" or "Book a demo" button into an agent conversation instead of a form:
const ok = caWidget.open("contact-sales"); // open on that playbook
caWidget.open("contact-sales", "I need a quote"); // …and ask the first question for them
if (!ok) location.href = "mailto:sales@example.com"; // false = script blocked; keep a fallbackcaWidget.open works the same whichever widget shape your site uses — corner bubble or the Ask
command palette. It returns false when the widget is unavailable (script not loaded or blocked),
so a click never becomes a dead end: fall back to the link the button had before.
Our own contact page is built exactly this way — seven intent cards, each opening a playbook that asks two or three questions, saves the contact, and books the follow-up.
Deterministic intake filing
When a playbook's job is to collect — a booking, a lead, a complaint — add a machine marker to
its background: [[inbox:submit_lead]], [[inbox:escalate]] or [[inbox:request_booking]].
When the visitor submits an interactive form on that playbook, the platform files the record
itself, before the model replies — the agent only relays the reference the tool returned. Contact
details in the answers are saved the same way.
This exists because filing is the one step that must never depend on the model's mood: in production testing, a mid-size model with the tool available, instructed and nudged would still confirm verbally — once quoting the documentation's example reference as if it were real. With the marker, the reference in the reply is the reference in your inbox, every time. If filing fails for any reason, the conversation continues normally and the model-driven instructions take over.
Seeing what works
The console shows, per playbook, how often it was opened and which starter questions got clicked. Use it to drop questions nobody picks and to spot pages where visitors open the chat but never engage — usually a sign the opening line is talking about the wrong thing.
The events are counts only. They carry no visitor identifier, because the question being answered is "is this copy any good", not "who asked what".
After they've spoken
Starter questions are for the visitor who hasn't said anything yet, and they step aside the moment one does. What happens next is a separate switch: follow-up suggestions, set on the agent rather than the page, which offer a few one-click questions after every reply. The two share the same strip above the message box and never collide — one breaks the ice, the other keeps the conversation moving.