A morning at the dispatch desk
The dispatcher pulls up the dashboard at 7:42am. There are three things to handle before the first install crew rolls out.
She types: "Who's my closest crew member to 1140 Industrial Park Way?"
The dashboard answers in a sentence. (Or rather, it would — that one's a roadmap stub today; the agent surfaces what's wired and what isn't, which is part of the point.)
She types: "What in-person meetings do I have this week?"
The dashboard returns a list. Not a screen with filters to set — a list, in the chat, sorted by start time, with the location type and the invitee name on each row.
She types: "Move Barry White to hired and send him the offer at $20,000/year."
The dashboard asks: "You want to flip Barry's application from 'offer' to 'hired' and create + send an offer letter at $20,000 base salary, USD, no bonus, no specified start date. Confirm?"
She types: "Confirm."
The dashboard does both, in order, and reports both — the application status update + the offer-letter id + the candidate's email — in two lines.
This is the dashboard.
What the dashboard agent reaches
The slide-in chat panel on the right of every Business Dash dashboard page is the platform's primary surface — not an add-on. It has structured access to roughly 95 actions across 25 domains: scheduling, CRM, contracts, quotes, invoices, hiring, time clock, reviews, the widget dashboards, the parallel-page (programmatic SEO) routes, the DNS verification flow, billing state, Stripe Connect readiness, and the org's media folders.
You'd usually call this kind of thing "an AI feature." We built it as the dashboard's main interaction model instead. Every new feature shipped on the platform ships an agent tool alongside the UI; the chat surface stays current as the platform grows.
The chain is the trick
The interesting part isn't the count of tools. It's that the agent walks them in sequence to answer questions a single tool can't.
You type: "When did we last talk to Carla Anderson?"
What happens behind the scene:
- Resolve the name → contact id.
- Read the activity log for that contact, grouped by channel.
- Sort the channels by latest interaction.
- Reply with: "Last contacted 6 days ago. Total interactions: 14. Most recent: email (6d ago), call (3w ago), meeting (8w ago)."
You type: "Has Arnold P signed his install contract?"
Behind the scene:
- Resolve the name → contact id.
- List contracts for that contact, filtered to
status = signed. - Reply with whichever contracts came back, or "no signed contract on file yet — the latest one is still in 'sent' state, signed 0 of 2 fields."
You type: "Send Eddie the standard service contract."
Behind the scene:
- Resolve the name.
- List contract templates, pick the one matching "standard service" (or ask if there are several).
- Confirm with you.
- Create the contract from the template, bind it to Eddie's contact id, flip the status to 'sent' so the signer link goes out.
Every chain is one sentence on the user's side, several tool calls on the platform side. The user never has to know which screen would have held that data; the dashboard answers from the chat.
Whose timezone? Asked, not assumed.
When a dispatcher in New York types "schedule an interview with Charles tomorrow at 2pm," the dashboard has a choice to make. The dispatcher's timezone is Eastern. Charles, the candidate, is in California — Pacific. "Tomorrow at 2pm" could mean either.
The dashboard reads the relevant timezones, sees they differ, and asks: "Charles is in Los Angeles. Should 2pm tomorrow be 2pm Eastern (your time) or 2pm Pacific (his)?"
We thought of this as a small detail. Then we thought about it more, and it isn't. A meeting scheduled in the wrong timezone is a meeting one person shows up to and the other misses. The dashboard asking before the side effect lands turns an entire class of small operational scars into nothing.
The rule the agent follows:
- Self-action (clock in, my calendar, my dashboard) → caller's timezone.
- Org-level read ("jobs this week", "revenue today") → the org's most-common-staff timezone if it differs from the caller's. The reply names which one was used.
- Counterparty action (book a customer, interview a candidate, log activity at a specific past time) → check both timezones, ask if they differ.
Confirmation is the floor
Every action tool that mutates state asks for confirmation before running. The agent never silently sends an email, never silently flips a status, never silently archives a dashboard. The user types the question, the agent picks the tools and assembles the action, the agent shows the full intended side effect, the user confirms.
This isn't a UX nicety. It's how we make the LLM-driven surface auditable. The user has the final keystroke on every side effect, and the audit log has the user's id on it.
Permission inheritance
A staff member's RBAC defines what the agent can do on their behalf. The agent can list dashboards a user can see; it can archive only the dashboards that user owns. The agent can read its caller's time entries; it can read the org-wide timesheet only if the caller has staff.schedule.manage. The agent can move a hiring application to 'hired' only if the caller has hiring.manage.
We didn't build a parallel permission system for the agent. The same RBAC that runs the screens runs the chat. A dispatcher's chat can do what the dispatcher's dashboard can do — no more, no less.
What this changes in a normal workweek
The dispatcher's morning above used to be four screens — the schedule, the hiring board, the CRM peek panel, the activity log. The platform takes the same actions in any of those screens. The dashboard agent makes the screens optional. You can still use them; the chat is faster for everything that isn't a deep visual layout (kanban boards, calendars, designs).
The compounding effect: every new feature we ship on Business Dash ships an agent tool alongside it. The chat surface's competence grows month over month at the same rate as the platform's feature set, without an extra build cycle. When we ship the dispatch travel-time engine, "who's my closest tech?" stops being a roadmap stub and becomes a one-sentence answer.
A short list of things the chat now answers in a sentence
- "What in-person meetings do I have this week?"
- "Schedule an interview with Charles tomorrow at 2pm."
- "When was the last time Carla was contacted?"
- "Has Arnold P signed his install contract?"
- "What's the address for Harrison Hues?"
- "Add the note 'wants install Tuesday' to Eddie Stewart."
- "Send Orlando a verification email + a follow-up text for the install."
- "Move Barry to hired and send him the offer at $20,000/year."
- "Which vendor can I order 45 of solar panel X from at the best price?"
- "How many hours has my crew clocked today?"
- "Resend invoice 14 with a fresh pay link."
- "Send Eddie the standard service contract."
- "What DNS records still need to be set for example.com?"
- "Why won't my domain verify?"
- "Are we current on billing?"
What's underneath
For the curious: the agent is wired through a typed tool registry (src/lib/dashboard-agent/), one module per domain. Each module defines the slice of the org's tRPC caller it needs + the tool definitions on top. The registry intersects every domain's caller into one super-caller, concatenates every tool array into one registry, and hands the whole thing to the LLM loop. The system prompt names every domain in its capability map so the LLM knows what's available. Adding a new domain is one module file + four lines in the registry.
What it adds up to
The dashboard answers "who's my closest crew member" in a sentence, and asks about the timezone before it schedules anything. That is the AI-first surface we wanted on a platform we run ourselves.
