Recently I wrote about how we use Jev in There There to detect spam, bounces, and automatic replies. Jev reads an incoming email and answers questions we define up front. We keep the probabilities and decide in our own code what counts as a match. The cheap checks on mail headers still run first.
That post ended with a promise that we'd find more uses for Jev. Here are two we have since added to There There: smart conditions for workflows, and automatic assignment to a team or person.
Workflows already let you match a subject, sender, or message body against text. That works when the words are predictable. A customer asking for a refund can also write "I was charged twice", "Could you reverse this payment?", or something in another language. A list of keywords gets long quickly.
With a smart condition, you describe what should match. Here's one of the example workflows in our local app:
The condition says "The customer asks for a refund or to cancel a payment." When it matches, the workflow adds a "Refund request" tag. We turn the description into a Jev Boolean question:
public static function question(string $description): Boolean
{
return new Boolean(
"The incoming message matches this workflow description: {$description}",
[
'true' => 'The incoming message fits the description.',
'false' => 'The incoming message does not fit the description.',
],
);
}
This question joins the spam and auto-reply questions from the first post in the same inbound classification request. The answer is stored with the message, and the workflow reads its probability from there. It doesn't call Jev again when the workflow runs.
The editor also lets you try a sample email or an existing ticket and see the score before you enable the workflow. Here, a customer asking about a duplicate charge gets a strong match:
Only incoming email is checked. Jev gets the subject, sender, and message text, not the whole ticket history.
When a ticket comes in, we need to get it to the right person. At Spatie, one person might handle billing questions, while someone else is better placed to help with a browser issue or a design question. We know who can handle what. The hard part is translating every way a customer might describe a problem into fixed routing rules.
In a workflow, you select the people Jev may choose from and add a short description of what each handles. Here, Dries handles frontend issues, Jef takes billing questions, Jimi handles design, and Zuzana picks up general support:
We ask Jev a Choice question with the people selected in the workflow. It chooses from that list, with one extra option for when nobody fits:
$response = Classification::of($state)
->questions(['assignee' => new Choice(
$instructions,
[
...collect($options)
->map(fn (Team|User $assignee) => $candidates->descriptionFor($assignee))
->all(),
'None of these' => 'None of the others clearly fits this ticket.',
],
)])
->timeout(10)
->classify();
We only assign when Jev has a clear favorite. Sending a vague ticket to the wrong person creates an unnecessary hand-off, so when nobody clearly fits, the action makes no change. Another workflow can pick it up later.
Before asking Jev, we remove disabled members and anyone who can't access the ticket's channel. The same approach can assign a team, with its own list and descriptions. Team and person assignment are separate actions, so choosing a team doesn't restrict which people Jev can choose from. Spam and automatic replies are skipped before either assignment action runs.
These additions make our workflows a lot more flexible. We can describe what we're looking for in plain language and route matching tickets to the people best placed to help. The workflow still decides what happens next.
If you missed the first part, read how we detect spam and auto-replies with Jev. You can also read the workflow conditions and workflow actions docs, or try There There.