Skip to content
● Grav 2.2 is out: 2x faster cold starts and 96% less memory on big sites. Read the announcement →
Premium / Helpdesk Pro
Premium Plugin · v1.0.1

Helpdesk Pro

A complete helpdesk, running inside your Grav site

Helpdesk Pro is a helpdesk and request tracker that runs inside your own Grav site. Your team works tickets in Admin Next, your clients follow their requests in a client portal on your site, and your knowledge base is ordinary Grav pages you already know how to write. Every ticket, reply and file sits in a database you own, on your server, and backs up with the rest of your site. There's no per-agent monthly bill, so adding a colleague to the desk costs nothing. Clients never need a password: they sign in with a link we email them, or just reply to our emails, because email works both ways. Replies, new requests and follow-ups arrive by webhook from your mail provider or from an IMAP mailbox, thread onto the right ticket, and go back out as clean, branded email that reads like it came from a person.

Grav 2.0+

Helpdesk Pro is built for Grav 2 and requires the free API, Login, Email and Form plugins. It is not compatible with Grav 1.7.

Everything you need. Nothing you don’t.

Over 85 distinct features and integrations, out of the box.

The Staff Desk

Run the whole desk from the keyboard

Your team works tickets inside Admin Next, and every screen has its own address, so a filtered list, a board or a ticket is a link you can bookmark or send to a colleague. Press ? anywhere for the shortcut sheet shown here: c starts a new ticket, / jumps to search, g and a letter moves between My work, All tickets, Triage and Notifications, and r or n opens a reply or a note. Shortcuts never fire while you type, and single keys can be turned off per browser.

The keyboard shortcut sheet open over a ticket in the desk
01
My work groups the tickets assigned to you by what they need: needs your reply, in progress, waiting on the client, and recently solved.
02
All tickets with filter chips counted over the whole result, a sort, and a search box that finds a ticket by number, subject, requester name or email address, and the words of its conversation and internal notes.
03
Replies and internal notes in one composer, each with its own draft saved as you type. A note is tinted and labelled so it can never pass for a reply, and it never reaches a client by any route: not the portal, email, search, live updates or the API.
04
A collision guard: if the client wrote in while you were typing, you see the new messages with Send anyway and Edit first before anything goes out.
05
Triage for new requests, oldest first, with three decisions: Accept, Duplicate of… and Decline with a reason. Each decision emails the client the right thing, and single keys move you through the queue.
06
Statuses in six fixed categories (New, Open, Waiting on client, Waiting on us, Resolved, Closed) that you can rename and add to, plus labels, priorities and kinds (support, bug or feature).
07
Projects decide who may submit and read tickets and which staff work them: private, members only, Grav groups, or public. Each has its own default assignee, email alias and triage switch.
08
Project boards with a column per status. Drag a card to change its status with the mouse, or do the whole move from the keyboard with every step announced to screen readers.
09
Merging tickets brings both conversations, files and email threads together on one ticket, and the desk warns you before a merge would let anyone read more than they can today.
10
Saved replies with placeholders like {{client.first_name | there}}, personal or shared, plus saved views and bulk edit of up to 200 tickets at once.
11
New tickets on a client's behalf: find the client by name or email (or type a new address), set the kind, priority and assignee, attach files and choose whether to email them.
12
Keyboard shortcuts throughout (c for a new ticket, / to search, j and k in lists, r to reply, n for a note, ? for the full sheet) that never fire while you type and never clash with Admin Next's own keys.

Email In and Out

Test an email before a real one arrives

Most people who write to a helpdesk just want to use their mail app, so Helpdesk Pro lets them and keeps every conversation on the right ticket. The Email screen shows how mail reaches the desk and when the last message came in. Try an email takes a pasted message and runs it through the same checks a real one gets: the loop guard, who sent it, which ticket it belongs on, and what is left once the signature and quoted history are trimmed. Nothing is saved and nobody is emailed.

The Email screen with the inbound mailbox receiving and a dry run of a pasted email
01
Replies by email: clients and staff answer Helpdesk Pro's emails from their mail app and the reply lands on the right ticket. A new email to your support address, or to a project's own alias, opens a ticket.
02
Receive mail the way that suits you: inbound webhooks from Postmark, Mailgun, SendGrid, Amazon SES, Resend and MailerSend (through their Email plugin providers), Cloudflare Email Routing with a small Email Worker, a signed webhook of your own, an IMAP mailbox such as Gmail with an app password, or a Postfix or Exim alias that pipes mail straight into the plugin.
03
Threading that holds up: a reply finds its ticket by the thread headers, a reply address made for that recipient (support+t…@), or a hidden signed reference, and only when the sender is allowed to add to that ticket. A staff member's reply by email posts as them, and a reply to a note email becomes a note.
04
Clean replies: quoted history, signatures and Sent from my iPhone footers are cut from Gmail, Apple Mail, Outlook, Yahoo, Thunderbird and Proton Mail replies, with On … wrote: recognized in twelve languages. HTML-only mail is converted to text, with hidden content and tracking pixels dropped.
05
A loop guard ignores out-of-office replies, bounces, mailing lists and its own mail, and rate-limits each sender. A staff reply that fails DMARC, DKIM and SPF is held for an admin to release.
06
An inbound log of every received email with its state and the full decision trail, Show original, Retry and Release, plus Try an email, a dry run of a pasted email that saves nothing.
07
Client email that reads like personal email: the agent's name, their reply and a View request button, with no ticket numbers and no quoted history. A reply sent with mark solved goes out as one email, not two.
08
Branded HTML email with your logo (and a dark-mode logo), accent and button colors, a font choice and a Markdown footer line. It's 600px wide, readable on a phone, and supports dark mode in Apple Mail, iOS Mail and Outlook.com. The plain-text part carries the same links.
09
Files travel with the reply: pictures show in the email, other files are attached up to a size cap (10 MB by default), and the rest become links that sign the client in and open the file.
10
Proper mail headers: Message-ID threading, RFC 8058 one-click unsubscribe, Auto-Submitted and X-Auto-Response-Suppress, all sent through whatever engine your Email plugin already uses.

The Client Portal

A request form that tries to answer first

The portal is a page on your own site, at /help or wherever you put it, drawn by your theme and written for people who aren't technical. Guests write in with the request form: their name and email, a short summary and the message, any custom fields you ask for, and files dropped in. As they type, the form suggests help articles that match, so plenty of questions are answered before a ticket exists. Nobody sets a password: the email that confirms the request carries a link to follow it.

A signed-in client's Your requests page, with Open, Waiting on you and Closed tabs
01
A help center home with a big search box, the visitor's latest requests, topic cards and popular articles, then Contact us last.
02
Sign-in links instead of passwords: clients type their address and get a link that works once and expires. Only a hash of it is stored, and the link opens a page with a single button, so the corporate mail scanners that open every link in an inbox can't use it up.
03
Signed in from every email: each client email links the request with a sign-in link made for that recipient, so a guest or email-only client lands on their request already signed in.
04
No account needed to write in: guests use the request form, and someone who only ever emails you never gets a Grav account at all. An account is created the first time a person uses a sign-in link.
05
Your requests with Open, Waiting on you and Closed tabs, search, and New reply markers. Each request shows the public conversation, a banner that says what happens next, a reply box with file uploads, and This is solved, Reopen and Stop emails actions.
06
Status names in the client's words (Received, In progress, Waiting for your reply, Solved, Closed). Your internal status names, notes, assignees and priorities never appear.
07
Spam protection on the guest form: the Form plugin's captcha (Cap by default, which runs on your own site and calls no third party), a honeypot, a signed time trap, hourly limits per IP address and per email address, and a blocklist of addresses, domains, IP ranges and words.
08
Works with JavaScript off, as ordinary forms that post and come back. With JavaScript on, the request updates in place and a reply appears without a page load.
09
Looks native on any theme: every color, radius and space comes from --helpdesk-* custom properties that follow your theme's Pico variables when it has them, in light and dark mode and at phone width. Every template can be overridden from your theme.
10
Deep links survive signing in: a client who opens a request link before signing in comes back to that exact request afterwards.
11
Twig functions to bring the portal into the rest of your site: helpdesk_url(), helpdesk_ticket_form(), helpdesk_my_requests() and helpdesk_nav().

Knowledge Base & Search

Search that knows what people mean

People rarely search with the words your articles use, and they don't have to. Here the help center answers can't log in with articles about signing in, thanks to a synonym list and typo correction on the bundled YetiSearch index, which holds tickets and articles together and needs nothing extra installed or running. Every result respects who is asking, so a guest only finds public articles. When nothing fits, Still stuck? opens the request form with their search already filled in.

Help center search results for can't log in, finding articles about signing in
01
Articles are Grav pages that use the Help Article page type. Write them in the Admin Next page editor you already use, with categories, a project, related articles and group access on a Help Center tab.
02
Browse or search: an articles index, a page per topic with its own description and icon, breadcrumbs, More in this topic, previous and next links, reading time and Was this helpful?
03
Still stuck? Ask us: answering No on an article opens the request form with the visitor's last search as the summary, and the form suggests matching articles as they type.
04
Before writing in: the ticket keeps the searches and articles the client went through first, so nobody sends them the article they already read.
05
One search index for tickets and articles on the bundled YetiSearch library (SQLite FTS5 with BM25 ranking). There is nothing to install and no service to run, and the index rebuilds itself if the file goes missing or stale.
06
Synonyms and typo correction, so a search for can't sign in finds the article that says log in. The desk also shows similar tickets on every ticket.
07
Results follow permissions: guests find public articles, clients find their own tickets and the articles they may open, and internal notes live in a separate staff-only index that no client search can reach.
08
Optional semantic search through any OpenAI-compatible embeddings API, off by default.

Rules, Macros & SLA

Saved replies that also do the busywork

Rules and SLA targets run in the background, and saved replies speed up the part people still do by hand. A saved reply fills in names and ticket details through placeholders like {{client.first_name | there}}, stays yours or is shared with the team, and can be offered in one project only. Open Also change the ticket and it becomes a macro: sending it can set the status and priority, assign the ticket to whoever sends it, add or remove labels, move it to another project and change its kind.

A saved reply editor that also sets the status, priority and assignee when it's sent
01
Rules in plain when, if, then form. When a ticket is created, someone replies, a note is added, a field changes, or a ticket has sat in a status for some hours. If it matches all or any of your conditions. Then change the ticket, add a note, notify someone, send the client a saved reply, or post to your team channels.
02
Test against a ticket: a dry run shows which conditions a real ticket meets and what the rule would change, saving nothing.
03
Rules can't run away: a change made by a rule never triggers rules again, a rule never fires twice for the same event, and a failing rule is logged and shown on the rule while the others carry on.
04
A built-in auto-close (off by default): a solved ticket whose client stays quiet gets a friendly nudge, then closes a few days later unless they reply.
05
Macros: a saved reply can also set the priority, assign the ticket, add or remove labels, move it to another project and set its kind. The composer tells you what it will do before you send.
06
SLA policies with first response and resolution targets, matched by project, priority and kind. The first match wins, and one policy can be the default.
07
Business hours with a timezone, several ranges a day, overnight ranges and holidays, with daylight saving handled. Or count around the clock.
08
One SLA clock per ticket that pauses while you wait on the client, stops at resolution and carries on after a reopen. The assignee gets a warning before a target is missed and a notification when it is.
09
Due badges on All tickets (in 2h, 35m late), Breached and Due soon filters, and a Due soonest sort. Clients never see a due time or a breach.

Notifications & Live Updates

Staff hear about what matters to them without drowning in email, and the whole team can follow along in the tools it already watches.

01
The bell first, email second: a notification is emailed only when it wasn't read in the desk in time, batched so a busy ticket sends one email, and held while you are active in the desk. A daily digest arrives in each person's own timezone.
02
Three levels per person (everything, only what's aimed at me, nothing), switches per kind of notification, and a mute on any ticket.
03
Team channels: post chosen ticket events to a Slack channel, a Discord channel, a signed webhook of your own, or a team inbox, for every project or just some. Deliveries retry, a failing channel is flagged, and Send a test message shows the endpoint's own answer.
04
Channels stay discreet: a channel message carries the subject, requester, status and a desk link, but never a note, a staff reply or a custom field.
05
Live updates over the Sync plugin: a new reply or note appears in an open ticket without a reload (your half-written draft stays put), lists refresh, and the sidebar badge follows the bell. Polling works everywhere, and Mercure or Ably push updates instantly.
06
Presence: Anna is viewing, Anna is replying… and The client is viewing this request above the composer. Clients never see staff presence.
07
Everything works without Sync: the desk refetches what's on screen when you come back to it, and presence runs on its own heartbeat.

Clients, Organizations & Custom Fields

The details your business runs on

Know who you're talking to, which company they're with, and the details that matter to your business. Custom fields add questions like an order number or a plan, in nine types from text and dates to select lists. Each field decides who sees it: clients fill it in on the request form, clients can only read it, or it stays with staff. A field can be required, shown as a column on All tickets and limited to some projects, and filters, rules and the API all find it by its key.

The custom field editor with its visibility, required and column options
01
Client organizations with email domains. A client with a proven address at one of those domains joins by themselves, and free-mail domains like gmail.com are refused.
02
Shared requests, switched on per organization: colleagues read each other's requests in the portal under My organization. They can't reply unless they're on the request, and they're never emailed about the ones they only read.
03
Custom fields on tickets: text, long text, number, select, multi-select, checkbox, date, URL and email, for every project or just some.
04
Public, read-only or internal: public fields are asked on the request form, read-only fields are shown to the client, and internal fields are for staff only. An internal value never reaches a client through the portal, email, the API, search or live updates.
05
Fields where staff need them: a Fields panel on each ticket, list columns and filters, saved views, rule conditions and a Set a field rule action.
06
People in one place: everyone Helpdesk Pro knows, with their requests and details, and actions to block, send a sign-in link, link or unlink a Grav account, or merge two people into one.

Reports & Satisfaction

Know how the desk is really doing

Reports are counted from what every ticket already records, so they cover your whole history from the first ticket, with nothing to switch on. Choose the last 7 or 30 days, this month, last month or your own range, for one project or all of them, for everyone or one person. Then read tickets created, resolved and reopened, the open backlog day by day, reply and resolution times, SLA results and satisfaction, and breakdowns by channel, assignee, organization and custom field, each of which can be downloaded as CSV.

The Reports screen with ticket totals, tickets by status and the open tickets chart
01
Reports over the last 7 or 30 days, this month, last month or a custom range: tickets created, resolved, reopened and open, the open backlog and created versus resolved charts, median and 90th percentile first reply and resolution times, SLA and satisfaction.
02
Breakdowns by channel, project, assignee, organization and custom field, plus the help center searches that found nothing. Every table and chart downloads as CSV, and every report is a link you can bookmark.
03
Honest numbers: days are your site's days (daylight saving included), deleted and merged tickets are left out, and staff only see the projects they work.
04
Customer satisfaction (off by default): How did we do? with Great, Okay and Not good in the solved email and on the request page. Clients can change their answer and add a comment, and a Not good rings the bell of the staff member it's credited to.
05
Rating links are scanner-safe: opening a link only shows the page, so a mail scanner that follows every link in an inbox records nothing.
06
A Ratings screen and a Rating card on every ticket, plus a Helpdesk widget for the Admin Next dashboard and a Helpdesk report on Admin Next's Reports page.

Privacy, Files & People's Data

Everything you hold about someone

When you run the helpdesk, you're the one holding the data, and a person's page shows exactly what that means. It lists their requests and details, and What Helpdesk Pro holds about them counts every kind of record, from received emails and replies to sign-in links and help center searches: the starting point for a subject access request. The same menu blocks them, merges them into another person, places a legal hold or erases them. All of it lives in one SQLite file on your own server.

A person's page with its actions menu open and what Helpdesk Pro holds about them
01
Your data stays on your server, in one SQLite file under user/data/helpdesk-pro/, with deny-all protection written next to it.
02
Erasure on request, in two modes: anonymize a person, or also remove every message and subject they wrote and delete their files. Their name is taken out of other people's words on the tickets they were on too.
03
Legal holds that stop an erasure, and a person page that shows everything Helpdesk Pro holds about someone, the starting point for a subject access request.
04
Hashes, not secrets: sign-in links, rating links and rate-limit counters are stored only as hashes, and a guest request stores nothing about the visitor's browser or IP address.
05
Private files: attachments are only ever served through access-checked routes, never a bucket or CDN address, and a file on a note is as private as the note. Files can also be kept in a private S3-compatible bucket (Cloudflare R2, Amazon S3, Backblaze B2 or MinIO).
06
Upload rules: size and count limits, an extension allowlist checked against each file's content, and program and script types always refused.
07
Not found, never forbidden: someone who can't read a ticket or file is told it doesn't exist, so nobody can probe which ticket numbers are in use.
08
Deleting a Grav account never deletes a helpdesk record: the person and their tickets stay, because a support history is a business record.

For Developers & AI Agents

The desk in Admin Next is built entirely on Helpdesk Pro's own API, so anything a person can do there, a script or an AI agent can do too.

01
A REST API under /api/v1/helpdesk-pro/ on the Grav API plugin, with paging, ETags, problem-details errors and the same access rules as the desk.
02
132 MCP tools through grav-mcp, so Claude, Cursor and other AI clients can read and work tickets with exactly the permissions of the API key they use. Tools you can't use are never offered.
03
Notes before replies: an AI can draft an internal note for you to review before the client hears anything, and a reply written against a conversation that moved on is refused.
04
Scoped API keys are respected: a key limited to helpdesk-pro.desk can't delete a ticket, even when its owner is an admin.
05
Eight permissions, from working tickets to configuring the desk, granted to your Grav groups in Admin Next.
06
Extension points for add-ons: domain events, custom rule triggers, conditions and actions, extra notification channel types (Teams, Mattermost…), report cards, ticket panels and person data that joins merges and erasure.
07
A background job queue for email, notifications, inbound mail and indexing, with retries and backoff. Jobs a request queues run right after its response, and a catch-up pass keeps email going on a site whose cron isn't set up yet.
08
Fully translatable: portal and email text comes from the plugin's language files.
A help center on your own site

A help center on your own site

Clients land on a help center that looks like the rest of your site, because it is the rest of your site: a Grav page, rendered by your theme, in light or dark. They search first, browse topics if they'd rather, and Contact us is always one click away. When they do write in, they never have to set a password. A sign-in link in every email takes them straight to their request.

Every ticket, the whole story

The ticket screen puts the conversation in the middle and the details beside it: status, priority, kind, assignee, project, who can read it, and the SLA clock with how long is left. Internal notes sit in the same thread, tinted so they can never be mistaken for a reply, and they never reach the client. Replies that came in by email say so, with a Show original link to the email as it arrived.

Every ticket, the whole story
Email that reads like a person wrote it

Email that reads like a person wrote it

Client email carries your logo, colors and footer, the agent's name and the reply, with a proper View request button and no ticket numbers or quoted history. When a reply also solves the request, it goes out as one email with How did we do? underneath. The client can just hit reply: with inbound email on, their answer goes straight back onto the ticket.

A knowledge base made of pages

Articles are ordinary Grav pages with the Help Article page type, so there's no second editor to learn and no separate content store. Each one gets breadcrumbs, related articles, More in this topic and Was this helpful? Answer No and the request form opens already filled in with what you searched for, and the ticket shows staff which articles you read before writing in.

A knowledge base made of pages
Rules you can test before they run

Rules you can test before they run

Rules read like sentences: when a ticket is created, if the subject contains invoice, set priority to High. Conditions cover projects, statuses, labels, channels, requesters, message text, custom fields, organizations, SLA state and ratings. Test against a ticket shows what a rule would do to a real ticket right now, and saves nothing.

SLA clocks that know your hours

Set first response and resolution targets per project, priority or kind, and count them in business hours with your own timezone, several ranges a day and holidays, or around the clock. The clock pauses while you wait on the client. Staff see in 2h or 35m late on every ticket, get warned before a target slips, and your clients never see any of it.

SLA clocks that know your hours

Your helpdesk, on your own site

No per-agent pricing, no support history sitting on someone else's servers, and no second login for your clients to forget. Helpdesk Pro runs on the same host as your Grav site, backs up with it and moves with it. Your staff work in the Admin Next they already use, and your clients deal with your site and your email address, nothing else.

Up and running

  1. Install the plugin along with the API, Login, Email and Form plugins, and enable it.
  2. In Admin Next, add a page with the Help Center page type, for example Help. Its route becomes your help center, and its content becomes the welcome text.
  3. Give your staff api.access and helpdesk-pro.desk (a helpdesk-agents group is a good start). The Helpdesk item appears in the Admin Next sidebar.
  4. Set your support address, and add the Grav scheduler to your crontab.
  5. Write a few articles with the Help Article page type.

When you're ready, turn on inbound email on the Email & Notifications tab of the plugin settings, and your clients can reply straight from their inbox.

A CLI for the operational side

BASH
bin/plugin helpdesk-pro status        # database, schema, job queue and worker health
bin/plugin helpdesk-pro migrate       # apply pending migrations (--status to preview)
bin/plugin helpdesk-pro work          # run one worker pass
bin/plugin helpdesk-pro jobs          # list, retry or cancel background jobs
bin/plugin helpdesk-pro reindex       # rebuild the search index
bin/plugin helpdesk-pro imap:poll     # read the IMAP mailbox now
bin/plugin helpdesk-pro blocklist     # manage blocked addresses, domains and words
bin/plugin helpdesk-pro erase         # erase a person's data

Full setup and configuration documentation lives on the Learn site.

A peek at sites running Helpdesk Pro.

Frequently Asked Questions

The most commonly asked questions about the Helpdesk Pro plugin

Is this a one-time purchase or a subscription?

Helpdesk Pro is a one-time purchase. You get all future updates and new features for free as long as your license stays active. There's no per-agent pricing and no cap on how many staff or clients you have, so your helpdesk doesn't get more expensive as your team grows.

Do my clients need an account or a password?

No. Clients can write in by email or through the request form as a guest, and someone who only ever emails you never gets a Grav account at all. When they want to see their requests, they type their address and we email them a sign-in link. The link in every client email signs them in too.

The first time a client uses a link, Helpdesk Pro creates a Grav account for them with site login only, in the groups you choose. An account that can reach the admin is never signed in this way, and staff never get sign-in links.

Does Helpdesk Pro work on Grav 1.7?

No. Helpdesk Pro is built for Grav 2 and Admin Next, and it needs the API plugin for the desk. If you are still on 1.7, take a look at the Migration chapter on the Learn site.

Do I need a database server?

No. Helpdesk Pro stores everything in one SQLite file, created and migrated the first time it's needed, at user/data/helpdesk-pro/db/helpdesk.sqlite. There's nothing to set up and no MySQL or PostgreSQL option to think about. It backs up and moves with your site like the rest of user/.

Helpdesk Pro writes a deny-all .htaccess next to the database for Apache. On nginx, Caddy or any other server that ignores .htaccess, deny that folder in your server config.

How do replies by email work?

Point your mail provider's inbound webhook at the address Helpdesk Pro gives you under Operations → Email in the desk, or let it read an IMAP mailbox every couple of minutes. Postmark, Mailgun, SendGrid, Amazon SES, Resend and MailerSend work through their Email plugin providers, Cloudflare Email Routing works with a small Email Worker, and a Postfix or Exim alias can pipe mail straight in.

Inbound email needs Email plugin 5.3.0 or later. Until you switch it on, everything else works as normal: client emails say View or reply to your request and link to the portal, and replies land in your support mailbox.

Do I need cron?

You should add the Grav scheduler to your crontab, because it runs the background worker that delivers email, digests, SLA warnings, time-based rules and IMAP polling. Most jobs run straight after the request that queued them anyway, and on a site whose cron hasn't run for a couple of minutes a small catch-up pass keeps notification email going. Treat that as a safety net, not a plan.

How does it look with my theme?

The portal inherits your theme's fonts and colors. Every color, radius and space comes from --helpdesk-* custom properties, and on a Pico-based theme such as Quark2 they follow Pico's own variables, so links and buttons use your theme's primary color with no work at all. One line of CSS (:root { --helpdesk-accent: #8428df; }) sets the accent anywhere else, and every portal template can be overridden from your theme.

Can AI agents work the desk?

Yes. Helpdesk Pro describes its API as 132 MCP tools, and grav-mcp hands them to Claude, Cursor and other AI clients. A tool can never do more than the API key it runs with, and tools that key can't use aren't offered. Ask the agent for an internal note first if you want to review a draft before the client sees anything.

What does Helpdesk Pro need to run?

Grav 2, PHP 8.3 or newer with pdo_sqlite, sqlite3, mbstring and dom, and these free plugins:

  • API for the desk, the REST API and MCP
  • Login for signing in
  • Email for sending mail (5.3.0 or later for inbound email)
  • Form for the request form and its captcha

The Sync plugin is optional and adds live updates. With Sync Mercure (1.2.2 or later) or Sync Ably, updates arrive instantly instead of by polling. Search is bundled, so you don't need YetiSearch Pro.

Can I use Helpdesk Pro on multiple sites?

Per the Grav Premium License, you need a license for each site. Development and staging copies of the same site are covered by the same license. Get in touch if you need bulk pricing.

$100 Buy Now

Helpdesk Pro Changelog

v1.0.1

24 minutes ago

    • The update_preferences MCP tool's email enum quotes its off value, so strict YAML and JSON-Schema validators read it as the string it is, not a boolean

v1.0.0

1 day ago

    • Plugin skeleton: service container, SQLite database through grav-db-kit (namespace-prefixed with Strauss, together with YetiSearch), infrastructure migration, job queue with the cron worker and an inline drain after each request, Admin Next sidebar item and desk page, API route registry, portal router, Twig function registry, permissions, MCP manifest
    • CLI: bin/plugin helpdesk-pro work, migrate, status and jobs
    • GET /helpdesk-pro/status and the get_status MCP tool
    • jobs.catch_up (on by default): on a site whose cron has not run for two minutes, a web request runs up to five due jobs after its response, at most once a minute site-wide, so notification email still goes out without cron; jobs.catch_up_exclude keeps slow job types for the worker
    • Core domain: people linked to Grav accounts by storage key (tombstones on delete, staff/client sync on every request, earlier requests claimed only with a proven email), projects with private/members/groups/public visibility and per-project staff access, statuses in six fixed categories, labels, tickets, replies and internal notes rendered once on write, participants and watch-on-participation, and an append-only activity log
    • One access policy decides who reads and does what, with the visibility matrix test in place (query exit green, the other exits reported pending with their owning leaf)
    • Domain events emitted after commit with a public/internal visibility flag, the onHelpdeskEvent and onHelpdeskIntake Grav events, and privacy.ip_hashing
    • Seeded statuses (New, Open, Waiting on client, Waiting on us, Resolved, Closed) and the public Support project
    • REST API for the desk and for integrations: bootstrap, config, counts and the sidebar badge; tickets with filters, whole-set facet counts, paging, ETags and an optional inline timeline; replies and notes with the thread_changed collision guard; participants, mute, people, projects and members, statuses and labels
    • Accounts without Helpdesk Pro permissions can use the ticket routes as clients: their own tickets, public content only, and another client's ticket is a 404
    • An MCP tool for every route but the badge poll, and the onHelpdeskTicketSerialize event for add-on fields under extra
    • The visibility matrix's API and MCP exits are green
    • The staff desk in Admin Next: My work grouped by what each ticket needs, All tickets with filter chips counted over the whole result, search and sort, and the ticket screen with its conversation, details and people panels
    • Reply and Note composer: separate drafts saved as you type, a note tinted and labelled so it cannot pass for a reply, "then set status" on send, attachments, and a TipTap editor loaded on first use (assets/dist/admin-editor.js)
    • Collision guard: a reply written while the conversation moved on shows the new messages with "Send anyway" and "Edit first"; a field change against a stale ticket reloads it instead of overwriting
    • Setup screens for projects (with members), statuses and labels; a notifications screen on the bell API with "Mark read" per row, "Mark all read" and the unread count in the side menu
    • New ticket (#/tickets/new): staff open a ticket for a client found by name or email (or a new address), with kind, priority, assignee, files and "Email the requester"; its first message reads "on behalf of" the client
    • Screens refetch on every visit, when their side-menu item is clicked again and when the browser tab comes back into focus, and the side-menu counts with them; "Discard changes" deletes a ticket's saved draft and its uploaded files
    • Sidebar badge from GET /helpdesk-pro/badge, the Helpdesk dashboard widget (admin2.dashboard) and the Helpdesk report, shown only to accounts holding helpdesk-pro.reports; desk.default_view
    • Client portal under the Help Center page: the help center home, a new request form, "Your requests" with Open and Closed tabs and search, and each request with reply, "This is solved", reopen and "Stop emails"; clients see public replies and milestones in their own words, never notes, internal activity or staff status names, and a request they cannot read is a 404
    • Works fully without JavaScript (form posts that redirect back); with it, htmx updates the request in place, loaded as its own chunk only when a page needs it
    • Staff who open a portal request are sent to the desk; portal pages are never cached
    • Theme-agnostic styles driven by --helpdesk-* custom properties, overridable templates, and the helpdesk_url, helpdesk_ticket_form and helpdesk_my_requests Twig functions
    • The help center finds its page by the helpdesk template and remembers its address for links in queued emails; new portal.guest_submissions and portal.client_groups settings on a Portal tab
    • Staff notifications: a bell first, with email only for what was not read in time. One notification per person per event by specificity (mention, assignment, client reply, staff reply or note, field changes), watch-on-participation, per-ticket mute, three levels (everything, aimed, nothing), email modes fallback, digest and off with per-kind switches, a grace period for urgent kinds, batching with coalescing so a busy ticket sends one email, hold while the person is active in the desk, and a daily digest in each person's timezone
    • Outbound email through the email plugin: milestone emails for clients (client-received, client-reply, client-resolved with the reply folded in, client-triage) that read like personal email with no ticket numbers, and staff-notification, staff-batch, staff-digest and staff-test for staff. Threading with Message-ID, In-Reply-To and References around a virtual root id, RFC 8058 one-click List-Unsubscribe, Auto-Submitted and X-Auto-Response-Suppress, a hidden body reference for reply matching, and ClientMailGuard refusing any internal note or note file in client mail, even from an add-on
    • A client who opens a request in the portal gets the client-received acknowledgement: the requester is also the event's actor there, and the "never tell people about their own actions" rule no longer drops it
    • Opening a ticket through the API (include=timeline, or the timeline with mark_read=1) marks the caller's notifications about it read on the server, so what was seen in the desk is never emailed
    • Links in email are made when the email is built, from email.site_url when set, else the address of the current web request, else the one the last web request recorded, so mail the cron worker sends links absolutely; with no address known the mail still goes and the log says what to set
    • GET /helpdesk-pro/notifications, POST /helpdesk-pro/notifications/read, POST /helpdesk-pro/notifications/read-all, GET and PATCH /helpdesk-pro/me/preferences, POST /helpdesk-pro/mail/test, their MCP tools, the portal's /unsubscribe page and one-click endpoint, the onHelpdeskMailBuild event, and the mail.send, notify.deliver and digest.sweep jobs
    • Attachments on replies and notes: files take their message's visibility, so a note's files are staff-only; drafts are visible only to their uploader; every refusal is a 404
    • Files are served only through access-checked routes ({mount}/_file/{id}/{name} for the portal, GET /helpdesk-pro/attachments/{id} for the desk) with Cache-Control: private, nosniff, inline display only for raster images, and never a bucket or CDN URL
    • Draft uploads for the portal and desk composers ({mount}/_upload, POST /helpdesk-pro/attachments), pasted images named automatically, and removing a draft before sending
    • Upload limits: size, files per message, an extension allowlist checked against the file's content, program and script types always refused, dangerous inner extensions neutralized, and an hourly upload rate
    • Content-addressed storage beside the database with an optional private S3-compatible bucket (R2, S3, B2, MinIO) and pull-through, and the hourly attachments.gc job that expires drafts and deletes unused files after a grace period
    • Visibility matrix: the file exit is green
    • Files everywhere a message is shown: the API's POST /helpdesk-pro/tickets and POST /helpdesk-pro/tickets/{id}/messages take attachments_token, every timeline message lists its files (a client sees public files only), the portal thread links each file, the portal forms attach files with or without JavaScript, and emails list files as links (portal links for clients, desk links for staff, never a note's file to a client); the API, portal and email exits of the visibility matrix check real files
    • Search: one index for tickets and knowledge base articles on the bundled YetiSearch, with internal notes in a separate staff-only index clients' searches can never reach; every search filtered by permission and every hit checked again; reindexed after each change, self-healing when the file is missing, stale or corrupt, with a plain-match fallback while it rebuilds
    • Knowledge base: any page with the new Help Article page type (kb-article) is an article, indexed on save, with categories, a project, related articles and group access on a Help Center tab; the help center gets a search box with suggestions as you type, a results page ({mount}/search), popular articles and categories, and "Did this help?" with "Still stuck? Ask us" opening the request form filled in with the last search
    • The request form suggests articles and the person's own requests while they type; the ticket keeps what they searched and read first, shown to staff as "Before writing in" and through GET /helpdesk-pro/tickets/{id}/context
    • Synonyms seeded in data/synonyms.json (so "can't sign in" finds "log in"), typo correction when nothing matches, and similar tickets for the desk (GET /helpdesk-pro/tickets/{id}/similar)
    • Optional semantic search through any OpenAI-compatible embeddings API, off by default, run by the search.embed job on the worker only, sharing yetisearch-pro's semantic: settings when left empty
    • GET /helpdesk-pro/search, GET /helpdesk-pro/kb/suggest, POST /helpdesk-pro/search/rebuild, their MCP tools, bin/plugin helpdesk-pro reindex, the search.index, search.rebuild, search.embed and kb.sync jobs, the kb.feedback event, knowledge base Twig functions, a Search settings tab and a Setup → Search screen
    • Visibility matrix: the search exit is green
    • Triage (#/triage; #/new-requests still redirects there): the queue of new requests, oldest first, with three big actions in plain words. Accept (optionally into another project, with an assignee, priority and kind; the project's default assignee takes it otherwise), Duplicate of… (with or without letting the client follow the original) and Decline with a reason, each with an optional note for the team; keys a, d and x
    • Each decision emails the client the right thing: client-triage for a decline or duplicate (a link to the original when they follow it, nothing when "Don't email the client" is ticked), and on Accept the acknowledgement a request held back as suspected spam never got. Decisions are kept with their reason and note in helpdesk_triage_decisions, and ticket.triaged carries notify, reason and followed_original
    • Merging tickets from the ⋯ menu in the ticket header: both conversations, their files and email threads end up on one ticket, the people and labels come along, and the merged ticket stays as a closed pointer. The desk and the API warn, and wait for a yes, when a merge would let anyone read more than today (409 access_widens)
    • Saved replies (#/saved-replies), personal or shared, per project or for all, with placeholders such as {{client.first_name | there}} filled on the server, an optional status to set after sending, an Insert reply picker in the composer that fills the open tab, and usage counts from saved_reply_id on sent messages
    • Saved views on All tickets, personal or shared, and bulk edit of up to 200 tickets (status, assignee, priority, project, labels, delete), each ticket saved or refused on its own; a watcher gets one notification for a whole bulk change
    • Routes POST /helpdesk-pro/tickets/{id}/triage, GET and POST /helpdesk-pro/tickets/{id}/merge, POST /helpdesk-pro/tickets/bulk, saved views and saved replies with render, their MCP tools, and saved_views in the desk bootstrap
    • Live updates over the Sync plugin: one inbox channel per person (helpdesk-pro:p/{id}.{tag}, only its owner may subscribe), so a desk or portal tab runs one pull loop however much it shows; frames carry ids only and every change is refetched through the permission-checked API and portal pages
    • A client's channel carries frames for public events only, and never a frame of any kind for a note, an edit of a note, an attachment on a note or internal activity (VisibilityMatrix realtime exit, SpySync tests)
    • The desk: open tickets show new replies and notes without a reload (the draft is kept), lists refresh, and the Notifications count and sidebar badge follow the bell as it rings; polling, Mercure (sync-mercure 1.2.2 or later, which publishes privately) and Ably (over its SSE endpoint, no library)
    • The portal: "Your requests" and an open request update live for signed-in clients, through sync's own pull endpoint and the site session (no proxy needed), leaving a half-written reply alone
    • Presence: "Anna is viewing", "Anna is replying…" and "The client is viewing this request" above the composer, from POST /helpdesk-pro/tickets/{id}/presence and {mount}/_live/presence heartbeats, which work with or without the Sync plugin; clients never see staff presence
    • GET /helpdesk-pro/live-config, {mount}/_live/config, the realtime.enabled, realtime.poll_idle_ms and realtime.presence_heartbeat_seconds settings, and migration 0030_presence
    • Project boards in the desk (#/board/<project>), one per project with the board setting on, reached from a List | Board switch on All tickets: a column per status grouped by category, cards sorted by priority then last activity showing the number, subject, requester, assignee initials, priority (normal left out) and labels, the list's filter chips, and the closed column behind a "Show closed" toggle kept in the address
    • Drag a card to another column with the pointer, or with the keyboard (Space picks it up, arrows choose the column, Space drops it, every step announced to screen readers) to change the ticket's status; the move is saved with the card's ETag and recorded in the ticket's activity, and a card someone else changed meanwhile is reloaded as it is now instead of being overwritten
    • GET /helpdesk-pro/projects/{id}/board and the get_board MCP tool (staff only; 404 for a project without a board), and desk.board to turn every board off
    • Keyboard shortcuts in the desk: c new ticket, / search, g m, g t, g r, g n, g b to move between screens, j/k, Enter or o, a and e in lists and boards, r, n, a, s, l and e on a ticket, and ? for the sheet. They never fire while typing or under a dialog, never use Cmd, Ctrl or Alt (so Admin Next's own keys keep working), and each person can turn them off for their browser
    • Clients sign in without a password: {mount}/login emails a sign-in link that works once and expires (portal.magic_link_minutes), stored only as a hash, rate-limited per address and per IP, and answered with the same page whatever the address; unknown, blocked and staff addresses never get one. Using a link creates the client's Grav account on first use and signs them in through the Login plugin, with its two-factor step when the site and account have it on; an account that can reach the admin is refused
    • A sign-in link opens a page that names the address and has one button, and only that button (a POST) uses the link and signs the person in, so mail scanners that open every link in an inbox can no longer use one up; someone already signed in as that person goes straight to the request. Asking for a new link ends the outstanding ones, and changing the password on the person's account ends every link they hold
    • Every client email links its request with a sign-in link for that recipient (portal.email_link_days), so a guest or email-only client lands on the request signed in; an expired link offers a new one with the address filled in
    • A signed-in client whose account address is not proven sees "Verify your email to see earlier requests"; the link proves the address and their earlier email-only requests join the account, and any sign-in link to that address counts as proof
    • The guest form at {mount}/new: name and email fields, the Form plugin's captcha (portal.captcha, invisible Cap by default), a honeypot and a signed time trap, per-IP, per-address and overall hourly limits (portal.limits), and a blocklist of addresses, domains, IP ranges and words (bin/plugin helpdesk-pro blocklist); spam still arrives, in Triage with the spam label and no acknowledgement; guest uploads join the request; {mount}/new/thanks is the same page for everyone
    • Client emails say "Reply to this email" only when inbound email is on; until then the footer reads "View or reply to your request"
    • New settings on the Portal & Access tab, the client-magic-link email, the magic_link.send job, and docs for sign-in links, the guest form and privacy
    • People in the desk (#/people, in the Library group): search by name or address, filter by kind and state, and a page per person with their requests, their details, what Helpdesk Pro holds about them, and every action on them: edit, block and unblock, send a sign-in link (never to staff), link or unlink a Grav account, merge, legal hold and erase. The ticket's People panel links the requester's person page and has "Send sign-in link"
    • One requester per address: the New ticket form's search (GET /helpdesk-pro/people?for=requester) shows one entry per email address, the person a ticket for it goes to, marked "account not verified" when an account has the address without proving it; such an account is never made a ticket's requester until its address is proven
    • Merging two people moves every ticket, message, participant row, notification, queued email, Message-ID, upload, sign-in link, membership and help center record to the kept person, refuses two different Grav accounts, and notes it in each ticket's activity
    • GDPR erasure as the people.erase job, in two modes: anonymize (name and address become "Erased person #id", account link, preferences, bell, sign-in links, KB history and raw emails go, waiting mail is cancelled) and delete_content (also every message body and request subject they wrote becomes "[removed]" and their files are deleted, the files collected straight after). Idempotent, one transaction, an internal activity line per ticket, and the search index rebuilds every affected ticket
    • An account proving an address an email-only person already had goes through the same merge, so a claim moves everything a staff merge would (notifications, uploads, sign-in links, help center history, saved views and replies, presence, add-ons' tables) and notes it on each moved ticket
    • Legal holds on people: a held person cannot be erased and a queued erasure stops; a merge moves the hold
    • bin/plugin helpdesk-pro erase, the people routes (privacy, magic-link, merge, erase, account, legal-hold) with their MCP tools, the people extension point for add-ons with their own person columns (also the onHelpdeskRegisterPersonData Grav event, so an add-on registers from a listener), and docs for people and erasure
    • An address with no screen behind it opens My work with a short note, instead of the old placeholder home; the database, schema and job queue moved to Setup → System (#/setup/system), which counts due and scheduled jobs apart (the recurring sweeps are always booked for later) and warns only about overdue or failed ones. GET /helpdesk-pro/status gains jobs.due, jobs.scheduled and jobs.next_run_at
    • The Settings button above the desk opens the plugin settings page, so the side menu drops its own settings entry; below tablet width the side menu is one drop-down that keeps its Work, Library and Setup groups, and row buttons no longer wrap
    • Boards leave the side menu for a List | Board switch on All tickets (the only board, a short list to pick from, or the filtered project's board) and on the board itself; a category heading only over two or more statuses, empty columns folded to a strip that opens while a card is moved, faded edges when the board is wider than the page, and the keyboard help under "Moving cards"
    • The ticket screen: the sidebar leaves out a panel with nothing to say (Triage off the queue, Before writing in without a help center trail, Similar tickets without matches); the requester's links join the People panel; Insert reply and Insert article pickers sit beside Attach files in the composer (search, arrow keys, Enter, Escape); Rename and Merge live in a ⋯ menu in the header and a click on the subject renames it; no status pill beside the Status menu; one watching action for yourself; labels can be added from the Details panel, or created from there when none exist
    • My work with nothing assigned offers "See N unassigned tickets"; Statuses is one table grouped by category with one Add status, counts that link to the tickets and "default" only where a category has a choice; a project's board setting sits under Basics and its KB category suggests the ones in use; New ticket's Email the requester is off while Requester is empty; list counts sit under the title with the pager only past one page; empty Saved replies, People and Notifications carry one primary and no repeated line
    • Settings tabs follow the admin's tasks: Desk (with the board switch), Portal & Access (the Portal and Guests and Sign-In tabs merged), Email & Notifications, Attachments, Search, Live Updates, and Advanced (database and jobs). Every setting keeps its name, so saved configuration still applies
    • The help center home has a heading, its search field fills the card and joins its button, and the request form keeps each hint under its own field with the space between fields instead; the sign-in pages share one even spacing with the button centred
    • Inbound email: clients and staff reply to Helpdesk Pro's emails by email, and a new email to the support address (or a project's email alias) opens a ticket. Mail arrives by webhook at /_helpdesk/inbound/{receiver}/{secret} (the built-in generic and cloudflare signed receivers, or the Postmark, Mailgun, SendGrid, Amazon SES, Resend and MailerSend Email plugin providers), by polling an IMAP mailbox, or piped to bin/plugin helpdesk-pro inbound:feed; each email is stored first and processed by a job, so the provider gets an answer at once and a retry never adds anything twice
    • Replies find their ticket by In-Reply-To, References, the thread root, a +t{token} reply address or the signed ref: marker, and only when the sender may add to that ticket; a closed ticket's reply opens a follow-up ticket that links to it. A staff member's own reply token posts as them (a note token as an internal note), a client's email is always a public reply, and a staff reply that fails DMARC, DKIM and SPF is held for an admin to release
    • A loop guard ignores out-of-office replies, bounces, list mail and our own mail, and rate-limits each sender; +u{token} addresses unsubscribe; attachments follow the upload rules and skip winmail.dat and signature logos
    • The inbound log for admins (states, the match trace, the reply cleanup, each attachment, "Show original", Retry and Release), GET /helpdesk-pro/inbound/status with the webhook address and anything that stops mail arriving, a dry-run test, a message's original email for staff, eight MCP tools, and inbound_failed and imap_failed bell alerts
    • With an Email plugin older than 5.3.0 inbound says "needs Email plugin 5.3.0 or later" and the webhook answers 503, and nothing else changes
    • Inline email images show where they sat: Apple Mail's U+FFFC marks in a text part become ![name](cid:…) for its inline images (in the HTML part's order, or after the text when there are no marks), and a cid: image in a message shows from its file for each reader: in the desk through the API with the reader's token, in the portal from the access-checked /_file link, in email as a link. A reader who may not open the file sees its name
    • Image files under a message show as thumbnails that open full size, in the desk and the portal, with other files as chips or links; an inline image shown in the message is not listed again. Attachments in the API carry their content_id
    • The client-received acknowledgement names the files that came with a request ("We received 2 files: a.jpg, b.pdf.") and, for a request by email, each file that was not kept and why: too large (with the limit), a type that isn't accepted, too many files (with the limit), attachments off, or unreadable. The inbound log records each skipped file's reason as a why code with its limit
    • Reply cleanup for inbound email: ReplyStripper keeps only the new words of a reply, cutting at the first quote header (Gmail, Apple Mail, Outlook desktop, web and mobile, Yahoo, Thunderbird, Proton Mail, "On … wrote:" in twelve languages, -----Original Message-----), a trailing > quote, a -- signature and "Sent from my iPhone" style footers; forwards are never cut, a body that would come out empty with no quote found keeps its full text, and the provider's own stripped text is compared but never used. HtmlToMarkdown converts HTML-only mail, dropping hidden content and tracking pixels, keeping remote images as alt text only and turning cid: images into placeholders for their attachments
    • Apple Mail's link leftovers in a text part (address <mailto:address>, https://example.com <https://example.com/>, View report <https://…>) are stored as the address or URL once, or as a Markdown link whose words come from the HTML part's link, so the stored reply and its quoted copy in email read cleanly (PlainTextLinks)
    • A reply by email with no text of its own (only the quoted email, perhaps a signature) is stored as "Replied without adding any text." with the quote folded under "Show quoted text" in the desk and the portal, and added quietly: the status stays, no reply time moves, nobody is notified, and to a closed ticket it adds nothing (ignored/no_new_text). Before, it kept the whole quoted email as its text
    • Reply addresses: while inbound email is on and can receive, every email about a ticket has Reply-To: support+t{token}@… with a token for that recipient, ticket and audience, so a reply finds its ticket by the token alone. Client email gets a client token (a public reply), staff notifications a staff token (a public reply posted as them), and any staff email that shows an internal note a note token (the reply becomes a note). List-Unsubscribe gains a mailto:support+u{token}@… entry
    • The reply address is inbound.address, else the project's email alias, else email.support_address, else the From; inbound.plus_addressing off (for mail systems that reject +detail) leaves tokens out and relies on the thread headers and the hidden reference
    • "Reply to this email" appears in client and staff email only when a reply really reaches the helpdesk (never on digests or batches across tickets); staff email showing a note says "Reply to this email to add an internal note." With inbound off, or an Email plugin without inbound, email is sent exactly as before and no tokens are issued
    • Digests, batches across several tickets and the test email ask for replies at the staff member's digest address (a token that names no ticket), so a quick reply to one is logged as ignored (reply_to_digest) instead of opening a ticket from the support address
    • Setup → Email (#/setup/email, admins only): whether inbound email is receiving, the receiver, each problem as a sentence linking to the Email & Notifications settings tab, the webhook address with its secret masked (Reveal, Copy) and the receiver's setup steps, the IMAP mailbox with its last and next read, where replies to our email go (the reply address, per-project aliases, and what turning plus addressing off costs), and Try an email, a dry run of a pasted email that saves nothing
    • Setup → Inbound log (#/setup/inbound-log): every received email with its state and why, state chips counted over the whole log, search by sender, subject or Message-ID, and a page per email with the decision trail (loop guard, sender, ticket match, reply cleanup, result), what was kept on the ticket, its attachments and headers, Show original fetched only when asked, and Retry, Process again and Release (the last two ask first)
    • The ticket thread's Show original is a quiet link on messages whose email is kept, and shows the header lines correctly
    • The decision trail puts each step's detail on its own short lines across the card, in the headers' and settings' words (In-Reply-To, References, reply token, hidden reference, project alias, default project) with Message-IDs set in code, and the loop guard and sender evidence (the header, the DMARC, DKIM and SPF results) the same way
    • Setup → Email's setup steps are a numbered list; for the built-in generic and cloudflare receivers they say which secret signs the requests, and a separate inbound.signing_secret is shown under the webhook address with its own Reveal and Copy. A signing secret under 32 characters, and a site with neither email.support_address nor inbound.address, are listed as problems
    • The Inbound log in the Setup sub-nav counts its failed and held emails (inbound_attention in GET /helpdesk-pro/counts, 0 without helpdesk-pro.settings)
    • GET /helpdesk-pro/inbound/log/{id} gains message (the body the email became on its ticket); GET /helpdesk-pro/inbound/status gains replies (whether replies arrive and why not, the reply address with an example, inbound.address, inbound.plus_addressing and each project's alias) the IMAP block's host, folder and interval_seconds, steps, signing_secret_set and signing_secret
    • The All tickets search box searches for real: besides the subject and #number, staff find a ticket by its requester's name or email address and by the words of its conversation and internal notes (the search index's 100 best matches, then the chips, sort and pager as usual). A ticket found through a note says Matched in an internal note under its subject. GET /helpdesk-pro/tickets?q= rows carry matched_in_note for staff; a client's q stays a subject match and never carries it
    • Erasing a person also takes their name out of other people's words on every ticket they were on: a saved reply's "Hi Nia,", a note naming them, the subject, activity details, triage reasons and notes, and mail queued about those tickets now read "Erased person #id", and the search index is rebuilt from them. Whole words in any case; a first name under three letters or one that is also a common word (Will, May, Grace…) is replaced only after a greeting ("Hi Al,"). Other tickets are never touched (docs/privacy.md)
    • A Help Center tab on the settings page: the portal's nav row (portal.nav) and the knowledge base topic descriptions and icons (kb.categories, one row per category with its description and an icon picked from the shipped set), both config-only before
    • Desk navigation cannot loop: navigating to the screen already open does nothing (no remount), and an address that changes while the Discard changes question is open waits its turn instead of asking twice
    • A desk screen you open starts at its top, and Back returns to where you had scrolled on the one before (per history entry, through the browser's Navigation API)
    • Relative times under a minute read "just now", never "7s ago"
    • All tickets fits its pane: columns drop by the room the list has, least useful first (Project, Labels and custom field columns, then Priority and Updated, then Assignee), the row actions stay pinned at the end of each row, and Assign to me moves into the ⋯ menu when there is no room for it
    • All tickets has one More filters fold for channel, organization and custom fields, which says what is set inside it while folded ("More filters · Channel: Email · Plan: Pro") and stays open once opened; list extensions mount into it with place: 'filters'
    • The desk's side menu works and looks like KahunaCart's: five collapsible bands (Work, Library, Automation with Rules and SLA, Setup, and Operations with Search, Email, Inbound log and System), One section at a time or bands left as you leave them, a pin that keeps a band open, a Find a screen box, a « fold to a rail of icons, and a count on a closed band when something inside wants attention. The band you are on is open on a deep link or Back, the browser remembers the rest (helpdesk-pro.nav.*), and below 900px the same bands replace the old drop-down. Addresses are unchanged (docs/desk.md#the-side-menu)
    • Rules and automations (docs/rules-and-automations.md): when a ticket is created, the client or staff reply, a note is added, or the status, assignee or priority changes, or a ticket has been in a status for a number of hours, and it matches all or any of its conditions (project, status, status category, priority, labels, channel, assignee, requester email or domain, subject or message text, kind), a rule changes the ticket, adds an internal note, notifies the assignee, the project's staff or one person, or sends the client a shared saved reply
    • Rules act as the Helpdesk system person with the rule named in the activity (via: rules, rule_id, rule), and every field change goes through TicketService::apply(). A change made by a rule never triggers rules again, a rule never fires twice for the same event (helpdesk_rule_runs), a time rule fires once per ticket per stay in the state, and a failing rule is logged, shown on the rule and skipped
    • The built-in auto-close, off by default: a resolved ticket whose client stays quiet for rules.auto_close.nudge_days gets a nudge, and is closed rules.auto_close.close_days later unless the client replies. Switched in Setup → Rules or on the settings page's Rules tab
    • Setup → Rules (#/setup/rules, for helpdesk-pro.settings): the rules in the order they run, each with its switch, what it does in words, when it last fired and how often, and its last error; Move up, Move down and Delete in the row's menu; the auto-close with its own switch. The editor (#/setup/rules/<id>) asks before you leave with unsaved changes, and Test against ticket says which conditions a ticket meets and what the rule would change, saving nothing
    • Macros: a saved reply can also set the priority, assign (to whoever sends it, nobody, or one person), add and remove labels, move the ticket to a project and set its kind, applied through TicketService::apply() after the reply is sent. A saved reply that changes the ticket may have no text, and picking it applies the changes at once. The composer says what the saved reply will also do before you send
    • API: GET/POST /helpdesk-pro/rules, GET /helpdesk-pro/rules/catalog, GET/PATCH/DELETE /helpdesk-pro/rules/{id}, POST /helpdesk-pro/rules/order, POST /helpdesk-pro/rules/test and POST /helpdesk-pro/saved-replies/{id}/apply; MCP tools list_rules, get_rule_catalog, get_rule, create_rule, update_rule, delete_rule, reorder_rules, test_rule and apply_saved_reply; the rules.sweep job every ten minutes; the rule notification type
    • Add-ons add their own triggers, conditions and actions on the onHelpdeskRulesRegister event (docs/extending.md)
    • Custom fields in rules: a condition per field (field.<key>) with tests that suit its type (a select is one of / is not one of its options, a checkbox is yes or no, a number or date is at least / at most, text contains or is empty), Changed field, the A custom field changes trigger (ticket.fields_changed), and Set a field (set_field), which writes through the custom fields service as the rule so the value is checked and the activity names the rule
    • An archived custom field leaves the rule editor; a rule that already tests or sets it keeps the condition or action as it was, never matches on it (or skips it), and says so in the rules list ("Needs attention") and the dry run ("Never matches") without breaking. Restoring the field brings the rule back
    • Organizations in rules: Organization is one of / is not one of / has none, and The organization changes (ticket.organization_changed)
    • SLA in rules: An SLA target is due soon (ticket.sla_warning), An SLA target is breached (ticket.sla_breached, not for a breach found late), SLA state (breached, due soon, on track, no policy) and SLA target (first response or resolution). An SLA event a rule's change brought on carries the rule's rule_id and never triggers rules
    • Ratings in rules: The client rates the ticket (ticket.rated), The client comments on their rating (ticket.rating_commented), Rating (is one of / is not one of / has none) and Rating comment (is filled in / is empty / contains)
    • The rules list says each rule whole, conditions included: "When a ticket is created, if the subject contains “invoice”: set priority to High." Long value lists and many conditions are shortened. The API's rules gain summary, conditions_text and problems, the catalog's conditions gain words, and the dry run's conditions gain text, words and unavailable
    • The rule editor draws each field's own control: organization and option pickers, number and date boxes, "is yes" / "is no" for a checkbox, and for Set a field the field, then the control its type needs
    • Custom fields, organizations, SLA and ratings register through the same RuleRegistry calls an add-on makes; the registry gains addConditionSource() for conditions that come and go with the site's data, and ConditionField gains words, phrases, normalize and unavailable (docs/extending.md)
    • SLA: policies with first response and resolution targets, matched by project, priority and kind (first match wins, with an optional default), counted in business hours (a timezone, several ranges a day, overnight ranges, holidays, daylight saving handled) or around the clock
    • One SLA clock per ticket: due times set at creation and recomputed when the priority, kind or project changes, paused while waiting on the client, stopped at resolution and carried on after a reopen; the first public staff reply meets the first response
    • The sla.scan job stamps breaches and sends one warning (sla.warning_minutes before) and one breach notification to the assignee, or to the project's staff when nobody is assigned; ticket.sla_warning and ticket.sla_breached domain events; sla.recompute after a policy or hours change
    • Desk: a due badge on All tickets ("Due in 2h", "Overdue 35m"), Breached and Due soon filter chips counted over the whole result set, a Due soonest sort, an SLA panel on the ticket, and Setup → SLA for policies and business hours
    • SLA API routes and 12 MCP tools; SLA is staff-only, under extra.sla on staff ticket payloads and never in the portal, client email, the client API or search (docs/sla.md)
    • The Due badge on All tickets is short under its Due heading ("in 2h", "35m late"), with the full sentence and due time on hover
    • Saving an SLA policy or business hours goes back to Setup → SLA, like the other Setup editors
    • The ticket's SLA panel puts each target's size under its name and the badge on its own line with the due time quietly under it
    • Warnings and breaches a setup change brings about (a first policy on open tickets, a shortened target) are stamped but notify nobody, so a new policy does not flood every bell; their events carry retroactive: true
    • Customer satisfaction ratings (csat.enabled, off by default): when a request is solved, its client requester is asked "How did we do?" with Great, Okay and Not good, in the client-resolved email (HTML and text) and at the top of their solved request in the portal. One ask per resolution: reopening closes it, solving again asks again. csat.projects limits the asking to some projects and csat.token_days (14) sets how long the client can answer and change their answer
    • The email's links are safe from mail scanners: GET {mount}/rate/{token} only shows the page with the answer preselected and a small script sends it as a POST, so a scanner that fetches the links records nothing (the reasoning is in docs/csat.md). Only token hashes are stored. The page thanks the client, lets them change the answer and offers a comment; after Not good it asks "What could we have done better?" (csat.ask_comment_on_not_good). Unusable links say why: expired, reopened, solved again since, or ratings off
    • Each ask credits the assignee, or the staff member who solved an unassigned request. A new Not good rings the credited staff member's bell (notification type rated, aimed) when csat.notify_not_good is on
    • In the desk: a Rating card on the ticket sidebar with the answer, the comment, the credit and earlier resolutions' answers; a mark beside the subject of rated tickets in the list; and a Ratings screen (Work group, shown while ratings are on) with answer chips counted over the whole filtered list, "With a comment", "Credited to me" and a project filter
    • API and MCP: GET /helpdesk-pro/ratings (list_ratings), GET /helpdesk-pro/ratings/summary with score and response rate, per staff member or project (get_ratings_summary, reports permission) and GET /helpdesk-pro/tickets/{id}/rating (get_ticket_rating). Ticket payloads carry extra.csat (full for staff, the requester's own answer for them, nothing for anyone else), and every answer emits the internal ticket.rated domain event
    • Ratings take part in person merges and erasure through onHelpdeskRegisterPersonData: anonymizing keeps the answer as an anonymous figure without its comment, delete_content deletes it. Expired rating links are pruned daily
    • The rating page after an answer says thank you once
    • The Ratings screen hides its Comment column when no row on the page has a comment
    • Custom fields on tickets (helpdesk_fields, helpdesk_field_values, migration 0065): text, long text, number, select, multi-select, checkbox, date, URL and email, each for every project or a list, and public (the client fills it in), readonly (the client sees it) or internal (staff only). Values are stored in typed columns
    • The request form and the guest form ask a project's public fields through helpdesk/partials/form-extra.html.twig, following the chosen project without a page load; a required field left empty or a refused answer comes back beside its field. Email, desk and API tickets skip required fields
    • The desk: a Fields panel on the ticket sidebar that edits in place and records each change in the activity, fields on New ticket, list columns and Field filters on All tickets (cf_<key> in the address and the API, kept by saved views), and Setup → Fields with reorder and an editor route with a dirty guard. A field with values is archived rather than deleted, and deleted permanently only from the archive
    • The portal's request page shows public and readonly values; internal values never reach a client (portal, client email, client API and MCP, search, live updates), checked exit by exit in FieldVisibilityTest. Staff notifications list a ticket's field values; client email carries none
    • extra.fields on API tickets, fields on POST /helpdesk-pro/tickets, the field routes with their MCP tools (list_fields, get_field, create_field, update_field, delete_field, restore_field, reorder_fields, get_ticket_fields, set_ticket_fields), and the ticket.fields_changed domain event, one per audience
    • Erasing a person clears the public field values on their requests and blanks them in the activity that recorded them
    • A change of field values reads as what changed in the ticket's activity ("set Plan to Pro", "cleared Plan", or the fields by name past two), not "fields"
    • Setup → Fields: a new field's page no longer shows the visibility hint as its sub-line, and the ↑ ↓ order arrows show only where a row can move
    • Client organizations: a company or team with email domains, staff-only notes and a sharing switch. Clients join by themselves when a proven address is at one of its domains; public email providers (gmail.com and the like, plus organizations.blocked_domains) are refused. A ticket takes its requester's organization when it is created and keeps it; staff move a ticket with the Organization card or PUT /helpdesk-pro/tickets/{id}/organization
    • With sharing on, members read each other's requests on the portal (My organization at {mount}/organization, a Mine switch, "Asked by" on each row): the public conversation only, never notes, read-only unless they are on the request, and no email about requests they merely read. Leaving the organization or turning sharing off ends it at once. Portal search follows the same rule
    • Desk: an Organizations screen in the Library group (list, a page per organization with members, tickets, domains and notes, and an editor route that asks before leaving unsaved changes), the organization on a person's page with Set organization…, an Organization card on each ticket, and an Organization filter on All tickets
    • API routes and 12 MCP tools for organizations and membership, the ticket list's organization filter, extra.organization on tickets for staff, and the organization.created, organization.updated, organization.deleted, organization.member_added, organization.member_removed and ticket.organization_changed events. Merging people keeps an organization and erasing a person takes them out of theirs
    • An organization's Members and Tickets cards sit a card's gap apart, and an organization with no members offers the clients already at its domains ("Add 1 person from rhuk.net"); GET /helpdesk-pro/organizations/{id} gains joinable, how many that is
    • Notification channels (helpdesk_channels, helpdesk_channel_deliveries, migration 0075): post chosen ticket events to a Slack channel (Block Kit through an incoming webhook), a Discord channel (one embed, kept inside Discord's length limits, mentions switched off), a webhook of your own (JSON with a schema version, signed with X-Grav-Signature: t=…,v1=… like the inbound receivers) or a team inbox (the staff-channel email with a List-Id, sent through the email queue, never threaded to the ticket). Each channel covers every project or a list, and picks from New ticket, Waiting in triage, Client replied, Ticket assigned, Resolved or closed, SLA due soon, SLA breached, Rated Not good and Rule posts
    • Messages carry the subject, requester, project, status, priority, assignee and a desk link, never a note, a staff reply or a custom field; "Include the first lines of a client's message" is a per-channel option, off by default, with the privacy trade-off in docs/channels.md and docs/privacy.md. A test checks that no note or internal field reaches any channel
    • Delivery runs from the channel.deliver job after the change is saved, never in the request: a timeout (channels.timeout_seconds), retries after 1, 2 and 4 minutes for timeouts, rate limits and server errors (channels.max_attempts), a Failing flag at the first failure, a pause after channels.pause_after_failures deliveries in a row fail for good, one delivery per event per channel, and a delivery log pruned after channels.log_days
    • Webhook URLs and signing secrets are masked in every API response and taken out of every error before it is logged or shown; a value may be env:NAME to keep it out of the database
    • An open registry: add-ons register more channel types (Teams, Mattermost…) with an id, a label, the fields the editor asks for and a payload builder through the new onHelpdeskNotificationChannels event (docs/extending.md)
    • Desk: Automation → Channels, with each channel's type and destination, projects, events and last delivery, a count on the Automation section of the side menu for channels failing or paused, and an editor route with a dirty guard, the type's own fields, Send a test message (showing the endpoint's own answer, and trying unsaved edits) and the latest deliveries; rules gain a Post to channels action
    • API routes under /helpdesk-pro/channels with the list_channels, get_channel, create_channel, update_channel, delete_channel and test_channel MCP tools, all behind helpdesk-pro.settings; GET /helpdesk-pro/counts gains channels_attention
    • A new site starts with a Team inbox email channel, switched off and with no address, ready for a team address (migration 0076); no channel can be switched on while a field it needs is empty
    • Reports in the desk (#/reports, Work group, for helpdesk-pro.reports): tickets created, resolved, reopened and open over Last 7 days, Last 30 days, This month, Last month or a custom range, with the project, staff and field choices kept in the address. Days are the site's (system.timezone), including the ones daylight saving makes longer or shorter
    • Cards: the open backlog per day and created versus resolved as inline SVG charts, tickets by status now, the median and 90th percentile of first reply and resolution times, SLA from the SLA report, customer satisfaction from the ratings, and tables by channel, project, assignee, top organizations, one select or checkbox custom field, and help center searches that found nothing. Each table and chart downloads as CSV; a card with nothing to show says why and links to where that changes
    • Staff see only the projects they work, and deleted and merged tickets are left out everywhere. Every figure is a grouped query (day buckets and percentiles worked out in SQLite), never a query per ticket
    • GET /helpdesk-pro/reports with the get_report MCP tool, and the onHelpdeskReports event for add-on report cards in the same card contract the core cards use (docs/extending.md)
    • Admin Next's Helpdesk report leads with the last 30 days and an Open Reports link to the desk, and counts only the projects its viewer works
    • A nav row on every portal page and help article (Help center, Articles, My requests with how many requests wait on the person, Contact us, and their name with Sign out, or Sign in), current page marked with aria-current; portal.nav turns it off and helpdesk_nav() places it anywhere in a theme
    • Deep links survive signing in: someone opening {mount}/tickets/12, their requests or the form before signing in comes back to that exact page, through the emailed sign-in link and through the Login plugin's password form; the return address is checked strictly (a path under the help center on this site, nothing encoded, no doubled slash or dot segment) and anything else lands on "Your requests"
    • Open a request by number: 12 or #12 in the help center search, or in the search on Your requests, goes straight to a request the person may read and is an ordinary search otherwise, the same answer for someone else's number and a number that does not exist
    • Your requests gains a Waiting on you tab next to Open and Closed, each with its count
    • Staff who reach a portal request page (one they sent, in a project they do not work) get an "Open in the desk" link
    • A browsable knowledge base: the articles index at {mount}/articles (every category with its count and first articles, uncategorised ones under General, and a search box) and a page per category at {mount}/articles/{category} with summaries, 20 to a page; categories on the help center home open their page, plus "Browse all articles"
    • Articles get a breadcrumb (Help center › category › article), "More in {category}" and previous/next links within their category; lists follow folder order then title and show the page's own summary (the text above ===) when it has one
    • New helpdesk_kb_articles() and helpdesk_kb_article_nav() Twig functions for building a theme's own KB menu; helpdesk_kb_categories() now gives each category's slug and url, and helpdesk_kb_url() knows articles and category
    • Portal pages under a Help Center page count as its children, so a theme's menu keeps Help highlighted across the whole portal
    • A calmer, modern help center that still looks native on any theme: one token block for the portal's colors, radius, spacing and type scale, read from the theme's own Pico variables where it has them (links and primary buttons share the theme's primary color), theme heading bars, list bullets and hyphenation turned off inside the portal, and every page checked in light and dark modes and at phone width with no sideways scroll
    • One search control everywhere (the help center, the articles index, search results and Your requests): the field and button joined, with a search icon in the field
    • The help center home in the order people look for help: a hero with the heading, intro and big search, the signed-in person's latest requests, "Browse by topic" as a grid of topic cards, "Popular articles" in two columns, and "Still need help?" with Contact us last
    • Topic cards on the home, the articles index and each topic's page, with an optional description and icon per topic from the new kb.categories setting (config only for now) and a small inline line icon set shipped with the plugin; a topic with no icon gets a letter tile
    • Article pages in one reading column: breadcrumb and title aligned with the text, a meta line (topic · updated · minutes to read), article typography for headings, lists, code, images and tables, "Was this helpful? Yes / No" on one quiet line, and Related articles and More in {topic} side by side
    • Article summaries in lists and search results keep sentences apart: headings, code and tables are left out and list items end their sentence, so steps no longer run together
    • A quieter nav row (underline for the current page, the waiting count in the accent color), and a page that needs an account shows the email sign-in form right there, carrying the way back, instead of a "Please sign in" button to another page
    • Requests: a banner that says what happens next, the client's own messages labelled "You" and the team's replies with the Support label and an accent edge, the reply box, Attach files and Send reply as one card, and "This is solved" and "Stop emails" as quieter actions; "Request #12" and "#12" everywhere a number shows
    • Your requests: underline tabs with counts, requests waiting for the person's reply stand out, an empty state with New request on every tab, and the separate "Open request #" field is gone (the search box opens a number)
    • The new request form says "Sending as Maria Lopez · Not you? Sign out" to a signed-in person instead of asking for a name and email, with the article suggestions right under the message
    • A message staff wrote for the client (a request opened on their behalf) now reads as the client's own on the portal, "You" with "added by our team", instead of showing the staff member's name
    • Portal times are in the reader's timezone (their Helpdesk profile's, their Grav account's or the site's system.timezone) in one format, "Sep 23, 2026, 2:22 PM", recent ones as "3 h ago" with the full time on hover, and the browser redraws them in its own timezone like the desk
    • "You can also reply to any of our emails" shows only once inbound email is on
    • Email sender names no longer double up: a site titled "Grav Helpdesk" sends as "Grav Helpdesk", not "Grav Helpdesk Helpdesk"
    • Desk: "See N unassigned tickets" (and the dashboard widget's Unassigned number) opens exactly the tickets it counts; on the Statuses screen a category with one status is one row with the category's description under it; at narrow widths an open ticket selects All tickets in the screen menu instead of "Ticket"; the Dashboard Widget setting's help says Triage
    • Every email redrawn in one layout: the business's logo or name, a white card with an accent line, a hidden preheader, a quiet footer that says why the email came, tables and inline styles for Outlook, 600px wide and readable on a phone, and dark mode for Apple Mail, iOS Mail and Outlook.com
    • Real buttons: "View request" on every client email about a request, "Open ticket" on staff notifications, a big "Sign in" on sign-in emails with the link printed under it, and "How did we do?" as three outlined buttons
    • The batch and the digest are scannable lists grouped by ticket: "5 updates on 3 tickets", each ticket's title as an underlined link, #number · project with status and priority pills, what happened as bullet lines, and its own "Open ticket" link, then one "Open the desk" button; the digest's three counts are tiles
    • Look and Branding settings (email.brand): business name, logo and a dark-mode logo, logo width, accent and button colours, a safe font list or a custom stack, a Markdown footer line, and light or dark (Auto matches each reader's mail app, or Always light or Always dark for everyone); the logo is always sent as an absolute URL and colours and fonts are checked before they reach the email
    • "Send a test email" on the desk's Setup → Email page, and the test email lists the look it was drawn in
    • Plain-text parts follow the same order with the same links as the HTML
    • A theme's copy of an email template now wins for mail the background worker sends too (the worker loads the theme before rendering)
    • A staff reply's files travel with the client's email: pictures shown under the message, other files attached, up to email.attach_max_mb (10 MB) in all; the rest stay links and the email says how many more are on the request. A note's file is never attached, and the attached parts are checked by ClientMailGuard too
    • File links in client email sign the client in and open the file; a signed-out visitor to a file link gets the sign-in form and comes back to the file instead of a bare 404, and a signed-in visitor who cannot open it sees who they are signed in as, with Sign out