Install the AI agent
One script line, pasted once. Penzago ships every improvement to your agent automatically — you never touch the line again.
The line
Your Penzago contact gives you a widget key (agent_…). Paste this anywhere in your page — it floats a chat button in a corner:
<script async src="https://www.penzago.com/agent.js" data-reflow-agent="agent_YOURKEY"></script>Or embed the agent as a card in your layout. With data-mode="inline" the card renders exactly where the line sits, so paste it at the spot in the page the card should occupy:
<script async src="https://www.penzago.com/agent.js" data-reflow-agent="agent_YOURKEY" data-mode="inline"></script>Or dock it to the foot of the page. With data-mode="dock" you get a slim bar centred along the bottom, carrying the prompt you set in Penzago and cycling your starter questions; tapping it opens the same panel the corner button opens:
<script async src="https://www.penzago.com/agent.js" data-reflow-agent="agent_YOURKEY" data-mode="dock"></script>Or add nothing visible at all. With data-mode="trigger" the buttons your page already has are what open the conversation:
<script async src="https://www.penzago.com/agent.js" data-reflow-agent="agent_YOURKEY" data-mode="trigger"></script>| Attribute | Required | What it does |
|---|---|---|
data-reflow-agent | yes | Your widget key. Without it, nothing renders. |
data-mode | no | inline for the in-page card, dock for the bar at the foot of the page, trigger for no visible chrome at all. Anything else (or nothing) is the floating launcher. |
data-position | no | left or right — pins the launcher to that corner, overriding the side configured in Penzago. |
Your own buttons
Any button or link on your page can open the widget with a question already asked, so the call to action you have already written becomes the way in. Add one attribute to the markup you have:
<button data-reflow-agent-ask="Can someone look at my roof this week?">Request an inspection</button>
<a href="/contact" data-reflow-agent-ask="" data-reflow-agent-prevent>Get a free estimate</a>| Attribute | What it does |
|---|---|
data-reflow-agent-ask | Opens the panel and sends this text as the visitor's first message. Leave it empty to open with no message, or with the default question you have set in Penzago. |
data-reflow-agent-prevent | Also cancels what the element would normally do, so a link stops navigating. Without it the panel opens and the link still follows its href, which is what a page that did not opt in would expect. |
data-reflow-agent-key | agent_…. Names which widget reacts, for the rare page carrying two keys. |
This works alongside the launcher and the dock as well as in trigger mode. It does not apply to the inline card, which has no panel to open. Your own click handlers run first, and Cmd, Ctrl, Shift and Alt clicks, along with middle and right clicks, keep their usual browser meaning. If you cannot edit the markup on your site, we can point the widget at a CSS selector instead.
Conversion events for Meta Pixel and GA4
The widget tells your page what the conversation just did, so the pixel you already run can count it. Every step of the funnel fires a reflow-agent:funnel event on the page (it bubbles, so listen on window) and, when your page has a Google Tag Manager dataLayer, pushes the same step there as penzago_<event>. Nothing the visitor typed rides on it: the detail is the step name, the placement, which message it was, and whether it came from a starter question. No names, no email addresses, no phone numbers, no transcript.
window.addEventListener('reflow-agent:funnel', function (e) {
var d = e.detail;
if (typeof fbq !== 'function') return;
var opts = { eventID: d.eventId };
if (d.event === 'agent_message_sent' && d.turn === 2) fbq('trackCustom', 'AgentEngaged', {}, opts);
if (d.event === 'lead_captured') fbq('track', 'Lead', {}, opts);
if (d.event === 'agent_booked') fbq('track', 'Schedule', {}, opts);
});detail.event | When it fires |
|---|---|
agent_card_viewed | The widget was on the visitor's screen. |
widget_opened | The visitor opened the panel (launcher, dock and trigger placements). |
agent_composer_focused | The visitor tapped into the composer (via: "input") or a starter question (via: "chip"). |
agent_message_sent | A message went to the AI agent. turn counts them from 1, so turn === 2 is the moment a visitor asked a second question. |
lead_captured | The AI agent captured the visitor's contact details. |
agent_booked / agent_booking_failed | An appointment was booked, or the attempt failed. |
conversation_ended | The visitor left the page after a conversation. |
event.detail is frozen and carries exactly event, mode (inline, launcher, dock or trigger), via and turn where they apply, agentKey, and eventId. Pass eventId to your pixel as the event id (the snippet does this): Penzago also reports these steps to Meta server side, and the shared id is what keeps one conversion from being counted twice. The snippet above fires AgentEngaged once per conversation, at the second question, which is the shape an ad set can optimise toward; for GA4 swap the fbq calls for gtag('event', 'agent_engaged'). In Tag Manager, a Custom Event trigger on penzago_agent_message_sent with a Data Layer Variable named turn does the same without code, and PixelYourSite can map a custom JavaScript event the same way.
Once your pixel has received AgentEngaged, create a custom conversion for it (Events Manager → Custom conversions → your dataset → event AgentEngaged) or pick the custom event directly as the ad set's conversion event.
A note for real-estate advertisers: ads that run under Meta's Housing Special Ad Category cannot use interest or lookalike targeting, but custom conversions and website Custom Audiences built from Pixel events are still allowed, so an audience of visitors who engaged with the AI agent is the retargeting lever. Visitors with Do Not Track or Global Privacy Control set never emit these events, and your Penzago contact can switch them off for your site entirely.
Link an ad straight into the chat
Add ?penzago=chat to the URL of any page that carries the launcher, the dock or the trigger and the panel opens as the page loads — an ad, an email or a QR code lands the visitor in the conversation rather than on the page. Add penzago_ask to send a first message as well:
https://www.example.com/?penzago=chat
https://www.example.com/?penzago=chat&penzago_ask=Lakefront%20homes%20under%20%243MBoth parameters are removed from the address bar the moment the widget reads them, so a reload or a copied URL opens nothing, and every other parameter on the link (utm_*, fbclid) is left exactly as your ad wrote it. The inline card ignores the link; it has no panel to open. A page that rewrites its own query string on load (some search pages do) can be linked through the fragment instead: #penzago=chat.
How updates reach you
The pasted line is a tiny loader with a stable URL. On each page load it reads a short-lived manifest and loads the current agent runtime — a content-hashed, immutably-cached file. When we ship an improvement we publish a new hash; your pages pick it up within minutes, and the line you pasted never changes. If anything in that chain ever fails, the widget simply does not appear — your page is never affected.
Before it will render
Your widget answers only on domains you have approved — that is a feature, not a hurdle: your key is visible in your page source, and the allowlist is what makes it useless to anyone who copies it onto another site. The match is exact, so https://www.example.com and https://example.com are separate entries — give your Penzago contact every hostname you embed on, staging included. On a domain that is not on the list, the widget renders nothing and your page stays untouched.
React, Vue and other SPAs
If your framework re-renders the DOM, load the runtime with a bare line (no key) and place the element yourself, wherever your component tree wants it:
<script async src="https://www.penzago.com/agent.js"></script>
<reflow-agent agent-key="agent_YOURKEY" mode="inline"></reflow-agent><reflow-agent> is a standard custom element: render it from JSX/templates like any tag. Attributes are read once when the element enters the page — to change one, remove the element and re-add it. A remount restores the visitor's conversation automatically.
Styling the inline card
The inline card inherits your page's font by default and exposes a small set of CSS custom properties — set them on the element and leave the rest alone:
reflow-agent {
--rfa-primary: #0f5132;
--rfa-bg: #fbfaf7;
--rfa-ink: #1b1b1b;
--rfa-radius: 4px;
}Your values outrank the office theme, which outranks the built-in defaults. The floating launcher ignores host styling entirely — it is the business's presence and looks the same on every page. Class names inside the widget's shadow root are private and change without notice; the custom-property surface is the only stable styling API.
Content-Security-Policy
If your site ships a CSP, add these to your existing policy:
| Directive | Add | Why |
|---|---|---|
script-src | https://www.penzago.com | the pasted agent.js and the versioned runtime it injects |
connect-src | https://www.penzago.com | manifest, config, chat stream and analytics beacon — every request the widget makes |
style-src | 'unsafe-inline' | the widget's own stylesheet, injected inside its shadow root |
frame-src | https://www.penzago.com | the map card, which frames our own /w/map wrapper page — never a Google URL |
img-src | https://www.penzago.com | the Google Maps attribution mark, and your logo if your theme carries one |
A complete policy for a fully locked-down page — our test suite runs the widget under exactly this policy on every build, so it cannot drift from reality:
default-src 'none'; script-src 'self' https://www.penzago.com; connect-src https://www.penzago.com; style-src 'unsafe-inline'; frame-src https://www.penzago.com; img-src 'self' https://www.penzago.com; base-uri 'none'For the reviewer's checklist: frame-ancestors is not involved — the widget is a custom element in your own DOM and never frames your page. It does frame exactly one page, our own /w/map wrapper behind a map card, which is what frame-src covers; block it and the card falls back to its “Open in Google Maps” link rather than breaking. The img-src entry is for the Google Maps attribution mark shown beside those answers; it also covers your agent's logo if your theme carries one. No font-src entry is needed — themes use font stacks, never webfont URLs.
If your policy uses 'strict-dynamic', host allowlists are ignored by design — put your nonce on the pasted <script> tag instead (nonce="…" next to async). The loader injects the agent runtime as a non-parser-inserted script, so under 'strict-dynamic' it inherits the pasted tag's trust automatically; connect-src still needs the entry above.