Run your workspace
Leads & human takeover
Leads (/app/leads; it was called Inbox,
and /app/inbox still forwards here) lists every captured
lead. Opening one shows the lead's details and status, and the
conversation that produced it, to read. To take the chat over or
reply, Open conversation opens the conversation
screen — the one place a team member takes over a chat.
A strip above the list shows how many leads are New, Qualified, Contacted, Won and Lost across all of them (the lead pipeline that used to be on the Dashboard). Each count opens the list filtered on that status. Home announces leads that are still New.
The period control — All time (the default),
Last 7 days, 30 or 90
(?period=7d|30d|90d) — narrows the list, the status
counts and the totals to leads captured in those days: today and the
days before it, from midnight, the same stretch as Conversations and
Results. Home's New leads tile opens Leads with the last 7
days, so the list shows the number on the tile. A lead sent from a
test chat in your dashboard carries a Test label: it is listed
so you can check the form and the email, but Home and Results do not
count it.
Leads and Conversations can be filtered to one AI employee: a chip
names it, links back to it and removes the filter. This replaced the
separate lists each AI employee used to have; their addresses
(/app/agents/{id}/leads and
/app/agents/{id}/conversations) open the filtered lists.
The figures of the filtered list count that AI employee only.
Layout
/app/leads lists leads, newest first. Opening one shows
the conversation that produced it on the left — visitor messages on
one side, the AI employee's replies and past human replies on the
other — with Open conversation and, when someone is in
the chat, who that is. The lead's email, name, phone, status and where
it was delivered sit on the right. The conversation screen links back
with Open lead in its side panel.
The Status column is an inline select — change it in place to move a lead through new → qualified → contacted → won / lost. The list also auto-refetches when the tab regains focus or you navigate back from a conversation thread, so a status edit you made elsewhere shows up without a manual reload.
The conversations index at /app/conversations covers
every conversation regardless of whether it produced a lead — useful
for spelunking past sessions that didn't convert.
Live updates
Updates arrive by polling, not over websockets. An open conversation screen reloads its messages every 2 seconds while the chat is waiting for a person or taken over, and every 8 seconds otherwise (only while the tab is visible; clicking the Live pill pauses it). The visitor's widget asks for new team replies and the takeover state every 3 seconds while it is open. The server does broadcast events on private channels when a broadcast driver is configured, but no screen subscribes to them.
Taking over
On the conversation screen, click Take over. (Until card #667 the lead page had its own Take over, Hand back and reply box; they are gone, so there is one screen with one set of words.) A few things happen:
- The conversation gets
claimed_by_user_id+claimed_atset to you (route:POST /app/conversations/{conversation}/claim). - On its next 3-second poll the visitor's widget sees the takeover: it shows a short "<Name> joined the chat" notice ("An agent joined the chat" when names are hidden) for about 8 seconds, and a Live agent pill in the chat header for as long as you hold the chat.
- The AI is paused. Every visitor message waits for you in the conversation. Each reply you send (
POST /app/conversations/{conversation}/reply) is stored as a message with roleassistantand modelhuman:<your user id>; the widget picks it up on its next poll and shows it as a team member's bubble.
Releasing
Click Hand back to release (route:
POST /app/conversations/{conversation}/release). The next
visitor message goes through the RAG pipeline again. The chat gets
the line "The operator has stepped away. The AI assistant is back
to help.", and the widget drops the Live agent
pill on its next poll. Useful when:
- You answered the off-script question and the rest is back to FAQ territory.
- The visitor is satisfied and likely to leave.
- You're ending your shift — handing back keeps coverage 24/7.
Capturing leads
Leads come in two ways:
- Visitor-driven — the widget's inline lead form, fired by a behavior rule or by the visitor explicitly asking to be contacted. Submitted leads land in
/app/leads. - Webhook-driven — every captured lead fires the
lead.capturedoutgoing webhook so you can fan it into your CRM. See Outgoing webhooks.
Live in-app toasts
The admin shell polls GET /app/leads/feed every 30
seconds for newly captured leads in the workspace and surfaces each
one as a sonner toast in the bottom-right of every page in the
admin SPA. Click the toast to jump straight to the lead.
The bell button in the top header asks the browser for native notification permission. Once granted, every new lead also fires an OS-level notification so workspace members get pinged on tabs that aren't focused. Permission is per-domain — denying it once can only be reversed from your browser's settings.
Polling pauses on hidden tabs to keep idle dashboards from burning
HTTP. The cursor lives in sessionStorage so opening a
second tab doesn't double-toast already-seen leads.
Email notifications
By default every captured lead is emailed to every workspace owner
and admin. Per AI employee, Lead capture ›
Who is told about a new lead can add up to 10 extra
addresses (they need no account) and switch off Also send to the
workspace owners and admins, so only those addresses get it. The
notification (App\Notifications\NewLeadCaptured) is
queued — the visitor's HTTP request never waits on SMTP, so a slow
mailer cannot slow lead capture or chat.
Two requirements for the email to actually arrive:
-
A queue worker is running. In production we use the
databasedriver — make surephp artisan queue:work --queue=defaultruns on a process supervisor (the in-cluster worker takes care of this on Laravel Cloud). Without it, queued notifications pile up injobsand never send. -
MAIL_MAILER+ matching credentials are configured in.env. The default islog— fine for dev but no actual email is sent. Switch tosmtp/resend/postmarkin production and verify withphp artisan tinker --execute 'Mail::raw("ping",fn($m)=>$m->to("you@example.com")->subject("test"));'.
Team recipients are filtered down to workspace_users.role IN
('owner', 'admin') with accepted_at IS NOT NULL.
Pending invites, editors and viewers do not receive lead emails unless
their address is one of the extra recipients; an extra address that
already belongs to such an owner or admin gets one mail, not two. The footer
of the email reflects your white-labelled site title (set in
Settings → System).
Cleaning up the conversation log
A new Conversation row is written every time a visitor
loads the widget for the first time in 24 hours. Visitors that drive
by without typing still produce a row, which means
/app/conversations can pile up with empty sessions over
time. Two affordances keep it manageable:
- Engaged-only filter (default). The list hides conversations with no visitor messages. Toggle Show all in the filter bar to see every session — useful when you're looking for bot traffic or QA loads. The dashboard's Conversations tile and the agents list count with the same rule, so the numbers agree with this list. Under Needs you and Live now every matching session shows, also one where the visitor asked for a person before typing anything (the button in the chat header works straight away): the pills and Home count those, so the list shows them too.
-
Period filter. All time / Last 7 days /
Last 30 days / Last 90 days (
?period=7d|30d|90d) bounds the list on the conversation's start time and is kept across search, the pills, the engaged toggle and pagination. Last 7 days is today and the six days before it, from midnight — the same stretch as Results and Home's This week. The count next to the control is the number of conversations in that window under the current filters — the quickest answer to "how many real conversations did we have this week". Unknown values fall back to all time, so an old bookmark never errors. -
Per-row delete. Admin and Owner roles see a
trash icon on hover. Deleting cascades to messages, leads, and
applied tags via DB foreign keys; analytics events are kept
(FK
nullOnDelete) so aggregate stats stay intact. -
Bulk delete empty. The button in the filter bar
deletes the empty sessions it counts: those of the AI employee the
list is filtered to (
?agent=), or of the whole workspace when it is not, and the confirmation says which. A session is empty when the visitor sent no message, did not ask for a person and no team member took it over. Sessions where the visitor wrote, or is waiting for a person, are never touched. -
Bulk delete selected. Tick the checkboxes on
any visible rows, then click Delete N selected.
The workspace global scope on
Conversationprotects cross-tenant ID smuggling — IDs from other workspaces are silently dropped server-side.
Viewers and Editors do not see delete affordances; the
ConversationPolicy::delete +
WorkspacePolicy::bulkDeleteConversations checks require
Admin or Owner. Deletion is irreversible — there is no soft-delete
trail and the audit log does not yet capture conversation removals.
Audit log
Taking over and handing back (claim, release) do not write
audit_logs rows. The thread itself shows the hand-back
line and, after a transfer, an internal note naming both team
members. Platform staff can browse a workspace's audit log at
/admin/workspaces/{workspace}/audit; there is no audit
page for customers.