Build your agent
Agents
An agent is the embeddable unit — persona, prompt, knowledge, theme, rules — that runs on a customer's site. This page covers creating, configuring, publishing, and rolling back agents.
Create an agent
From the customer app: /app/agents → New agent. You'll be
asked for a name and default language. Everything else has sensible
defaults you can refine later.
On signup we also auto-create a starter agent named after your domain. You can rename, replicate, or delete it freely.
The agent record
Every agent has these editable fields:
| Field | What it does |
|---|---|
name | Display name in the dashboard. Not shown to visitors. |
language_default | BCP-47 / ISO 639 code that pins the agent's reply language. Picks any locale auto-discovered from lang/*.json (132 ship out of the box; en/es/fr/tr are fully translated, others have UI chrome translated). Used when the visitor's Accept-Language doesn't match — the agent can also override per-conversation. Empty = follow the visitor's browser locale. |
persona | JSON: { name, tone }. Tone goes into the system prompt verbatim. |
theme | Widget colors, radius, position, launcher label. |
system_prompt | Optional override. Appended to the built-in prompt — doesn't replace the safety / RAG instructions. |
guardrails | JSON: { avoid: [topics], max_chars }. |
starter_prompts | Up to 6 chips shown above the input on first open. 80 chars each. |
confidence_threshold | 0–1. Below this score (after rerank) the agent says "I don't know" instead of guessing. |
strict_knowledge | "Answer only from the knowledge base" (card #730). When on and nothing in the knowledge base matches a real question, the visitor gets the fixed reply below and the model is not asked. Off by default. |
strict_fallback_message | The fixed reply for strict_knowledge (up to 500 characters). Empty = a default that offers the team, translated into the visitor's language. |
allowed_origins | Strict list of scheme://host origins where the widget may load. |
restricted_paths | Per-origin path blocklist — paths the widget refuses to render on even when the origin is in allowed_origins. Set to suppress the bar on checkout, account, or admin paths where a sales widget would feel wrong. See Allowed origins. |
auto_index_visited_pages | If on, pages visitors land on get queued for crawl + index. |
lead_prompt_strategy | When the bot offers to capture a lead. Today: after_n_turns (default), on_intent, off. Lets you tune capture aggressiveness per agent. |
wp_integration (JSON, encrypted) | WordPress / WooCommerce companion-plugin context. Stores the shopper-signing secret and any per-store config the LLM tools (e.g. lookup_order) need. Populated automatically when the WordPress plugin connects via API token; not edited by hand. |
Confidence threshold
The retriever scores every chunk against the visitor's question. Anything
below confidence_threshold is dropped (a chunk passes on the
higher of its similarity and rerank score). A turn's confidence is the
best score among the chunks that survived; when none survived it is
0.3, the turn is flagged low_confidence, and the
system queues a "knowledge gap" entry you can review. The visitor's
current page is given to the model as context but does not raise the
confidence: before card #729 it counted as 85%, so answers built on
nothing looked grounded and no gap was logged.
A threshold of 0 counts as unset and the platform default
applies. It could only come from an empty
RAG_CONFIDENCE_THRESHOLD= line in .env, which
read as 0 and switched the check off; an empty value now counts as
unset too.
Defaults are tuned per provider:
- Cloudflare bge-base-en-v1.5 embeddings — default
0.5. Cosine scores run lower than OpenAI's, so the bar is lower. - OpenAI text-embedding-3-small — default
0.78. Set this on signup if you switch providers.
With the multilingual bge-m3 embeddings, similarity
scores bunch together (0.44–0.66 for right and wrong pages alike on one
measured site), so a threshold of 0.5 lets almost every candidate
through on similarity alone. Measure
before you change it: php artisan pitchbar:probe-retrieval
shows both scores for your own questions (see the RAG pipeline page).
Grounded answers
The agent may only tell visitors what its knowledge says (card #729). The prompt says so, and because small models don't always listen, the answer is also checked in code before the visitor sees it, on the live widget, the JSON endpoint and the playground alike:
- Links — a link must be one of the turn's sources, a link that appears inside them, the visitor's current page, the root of one of those sites, or a page this agent has indexed. Any other link is removed; a markdown link keeps its words.
- Cards — a product, pricing or case-study card is shown only when its link is one of those pages and its name appears in the turn's sources (card #859). A pricing card's price must be the first price the sources give after that plan's name; when no pricing card passes, the answer links the pricing page instead. A product card whose price the sources don't give keeps its name and link but shows no price. Sentences that describe a made-up card go with it, and so does a "Here are a few options:" left with nothing under it. When every product card named something the knowledge base doesn't have, the visitor reads "I couldn't find matching products in our catalogue…"; when nothing was found at all, that line is the whole reply. A card the model wrote as a list item leaves no empty bullet behind.
- Plan prices in the text (card #859) — "Developer plan: $99/month" is held to the same rule as a pricing card: the price must be the first one the sources give after that plan's name. A line that fails is removed and the answer links the pricing page. Only a price that directly follows the word "plan" is read as the plan's price ("plan: $20", "plan costs $20"), so a sentence about an add-on price is left alone. English answers only.
- Citations — a
[n]marker must point at a source the model was given, and that source must have a web address a visitor can open. Pasted text without a source URL, Google Docs and Notion pages have none, so the answer still uses them but shows no source link for them. - Made-up tags — any self-closing tag other than the
known card markers (for example
<solution …/>) is removed from the answer and from the conversation history, and the widget hides it while the answer streams. A marker the model left unfinished because it ran out of tokens is removed as well, and if it was a card, its name is checked like any card's (card #859). A card link written relative ("/pricing/") is read as a page on the site the sources come from, and a list label left behind by a card ("* Developer Plan:") goes. - Knowledge gaps — when nothing in the knowledge base matches a real question, the prompt says so plainly. The agent then says it doesn't have that information, names no product, model, specification, price, client or project, and says how the team can help: through the lead form, or — when the lead strategy is "never" — through the contact or booking page the sources give. Follow-up chips are dropped, and the lead form is offered (unless the lead strategy is keyword-only or off).
- Product codes — when the visitor names a model code
(for example
MXA920) that no source mentions, the prompt says so, so the agent says it has no details instead of borrowing another product's facts. - Closing rules — the grounding rules are repeated at the end of the prompt, after the site-type rules and the owner's instructions: never present a product as something it is not (an amplifier is not a projector), facts about one product never apply to another, a product announcement is not a case study, never claim a client, installation or result the sources don't describe, and earlier replies are not sources. Work is credited the way the sources credit it: when a news item says another company installed or supplied a system, the agent names that company instead of saying "we" did it. A service (installation, rental, repair, training) is offered only when a source says the business provides it; a request form or a find-a-partner page is described as what it is. When a visitor describes a project or asks for a quote the sources can't give, the agent offers a consultation or a quote, with the same follow-up: the lead form, or the contact page when the form is off.
- No promises the chat can't keep (card #859) — the chat can't book calls, meetings or demos, send calendar invites or emails, or arrange for anyone to contact the visitor, and the prompt says so. The answer is checked too: a sentence that books or confirms a call, sends an invite, asks for preferred dates or times, or talks about a call that was never booked is removed, and so is "we'll reach out / call you / email you" while the visitor has left no email address or phone number. An offer to schedule a call stays only when it carries a link (a booking page from your sources) or the agent has a CTA that links to a page. "We'll call you tomorrow at 10" counts as booking a call; "we'll review your application and be in touch" is a promise, and a sentence that sends the visitor to the contact page ("visit our contact page and we'll be in touch") stays. In their place the visitor reads one line asking for a name and email (for a removed promise, without the part about booking), and the lead form opens. When the lead strategy is "never" or the visitor already left their details, the line points to the contact details on your website instead. The prompt itself follows the same setting: it mentions the form only when the agent has one. A reply that asks the visitor to leave a name and email in the form also opens the form, unless the lead strategy is "never" or the visitor already left their details.
- No talk about sources — "According to the sources" and "based on the sources we have" are removed: visitors don't know what the agent's sources are.
- No empty replies — when the model sends only follow-up chips (it happens after "Ok" or "Thanks"), the visitor reads "Is there anything else I can help you with?" instead of an empty bubble. Every line the check adds is translated into the conversation's language.
- Chips are the visitor's questions — a follow-up chip is sent as the visitor's own message when clicked, so a chip in which the agent questions the visitor ("Are you looking for a cloud-based or on-premises solution?", "What challenges are you facing?", "How do you currently handle…?") is removed. Otherwise a click would make the agent answer as if it were the visitor. The check reads English phrasing; questions in other languages rely on the prompt rule.
Answer only from the knowledge base
The grounding checks above make invented facts rare, but a model that has nothing to go on can still write a wrong sentence. For agents that must never do that, switch on Answer only from the knowledge base under the agent's settings (card #730). When nothing in the knowledge base matches a real question (a greeting is not a question):
- the visitor gets the agent's own message, or, when that is empty, "I don't have information about that yet. Would you like me to put you in touch with our team?" in the visitor's language (English, Dutch, Spanish, French, German, Turkish and Arabic are built in; other languages through the Translation manager);
- the model is not called, so nothing can be invented, and the turn costs no tokens;
- the lead form and any matching call-to-action are offered, the turn is saved with low confidence, and the question appears under Content gaps;
- the page the visitor is on does not count as knowledge. Questions about the page are answered when the page is in the knowledge base (pages visitors open are added automatically while auto-indexing is on).
The playground and the JSON endpoint follow the same rule. In the
super-admin turn debugger these turns show as strict.
Draft → Published
Editing an agent never affects live visitors. The widget runtime reads
from a snapshot called agent_version. Clicking
Publish writes the current state to a new version and
points the agent's published_version_id at it.
Versions are immutable. To revert, open the agent's history and click Roll back on a previous version — that just updates the pointer. Nothing is deleted.
Embed the agent
The agent's settings page shows a one-line install snippet with your
data-agent-id baked in. The widget URL includes a
cache-busting hash that mutates whenever the bundle is rebuilt, so
customers don't get stuck on stale versions. See
Install snippet for details.
Delete an agent
Two ways to remove an agent:
- From the Agents index page (
/app/agents): hover any row, click the trash icon next to the gear in the Settings column. Confirm in the prompt and the agent is gone. - From the agent detail page (
/app/agents/{id}): click the destructive Delete button in the danger zone, confirm in the dialog.
Deletion is a hard delete. Buyer report 2026-05-19:
soft-delete left phantom playground conversations + child DB rows
visible after the customer "deleted" their agent, so the destroy
path now calls forceDelete(). Every child row cascades
via FK at the database layer: conversations,
messages, leads, sources,
documents, chunks, experiments,
visitor_page_views, etc. The widget embed snippet for
the agent stops resolving immediately.
The vector store (Cloudflare Vectorize / Qdrant) is not on the same
DB connection, so DB cascade can't reach it. Destroy queues
PurgeAgentVectorsJob just before
forceDelete(): the job calls
QdrantClient::deleteByFilter with
['agent_id' => $id] against the
services.vector_collection index. It's idempotent and
swallows transport errors so a temporary Vectorize outage doesn't
block the user-facing delete. The php artisan vectors:audit
reconciliation pass catches any leftovers from a failed purge.
Because the delete is permanent, there's no
withTrashed() restore path. The platform-admin
destroy at /admin/agents/{id} uses the same semantics
(audit 2026-05-30) — it previously left soft-deleted rows that the
customer couldn't see, defeating the orphan-cleanup intent the
super_admin action was added for.
Authorization: requires owner, admin, or editor role on the workspace. Viewers and members of other workspaces hit a 403.
Bulk delete agents
Select multiple rows on the Agents index page (/app/agents)
via the checkbox column. The header checkbox toggles all rows on
the current page; shift-click a row checkbox to range-select between
the last clicked row and the current one. Selection persists across
pages until you click Clear.
A sticky bar at the bottom of the page surfaces the count and a
Delete action. Confirm in the prompt — every
selected agent is hard-deleted (same semantics as single-agent
destroy: DB cascade reaches child rows;
PurgeAgentVectorsJob queued per agent to clean
Vectorize). Cross-workspace ids are silently dropped from the
result (the per-row policy gate rejects them without leaking
existence).
The same useBulkSelection hook + BulkActionsBar
component now power bulk delete on every index page across the app:
/app/agents— bulk delete agents (this page)./app/workflows— bulk delete workflows./app/conversations— bulk delete conversations./app/inbox— bulk delete captured leads./admin/agents— super_admin bulk delete agents across workspaces./admin/leads— super_admin bulk delete leads./admin/conversations— super_admin bulk delete conversations./admin/workspaces— super_admin bulk soft-delete workspaces (cannot delete the workspace you're currently signed into)./admin/users— super_admin bulk soft-delete users (skips self, last super_admin, and workspace owners).
Selection model is identical on every page: header tri-state checkbox,
shift-click range select on row checkboxes, persistent selection across
pagination, sticky bar with count + Delete + Clear. Each bulk endpoint
accepts { ids: string[] } (max 100 per request) and runs
every id through its resource's policy gate.
Multiple agents per workspace
You can run as many agents as you want — one for marketing, one for the help center, one for the in-app upsell flow, etc. Each has its own knowledge base, persona, and embed snippet. Conversations and leads stay scoped to the agent that handled them.