← Back to the blog

A "Thank You" shouldn't reopen a ticket

Freek
Freek
May 30, 2026 · 4 min read
A "Thank You" shouldn't reopen a ticket

You close a ticket. The customer had a real problem, you solved it, and you've reached support inbox zero. Bliss! But one minute later, the ticket is back open, because the customer responded with: "Thanks, that did it."

While it's always nice to get a thank you as a response, it's slightly annoying that in most helpdesks such a response reopens the ticket. And you have to close it manually again.

It's a tiny moment, but it repeats all day, on every closed ticket a customer is kind enough to acknowledge. A thank-you shouldn't cost you a context switch.

So There There leaves a closed ticket closed when the reply is only a thank-you. Getting that right took more care than you'd think, so here's how it works.

The rule we wanted

We keep a ticket closed only when the reply has nothing to act on. A question, a follow-up, a link, a complaint, anything that needs a reply, and it reopens like normal.

One kind of mistake is much worse than the other. If we wrongly keep a ticket closed, a real customer message slips through unnoticed. If we wrongly reopen one, you just close it again. So whenever we're unsure, we reopen.

A cheap check before an expensive one

To avoid AI costs, we first try to detect with plain code when a reply isn't a regular thank-you. The obvious cases are cheap to rule out:

private function mightBeThanksOnly(string $messageText): bool
{
    $maxThanksOnlyLength = 500;

    // A genuine thank-you is short; anything long probably has substance.
    if (mb_strlen($messageText) > $maxThanksOnlyLength) {
        return false;
    }

    // A question mark means the customer is asking something.
    if (str_contains($messageText, '?')) {
        return false;
    }

    // A link means "take a look at this", so there's something to do.
    if (preg_match('#https?://#i', $messageText) === 1) {
        return false;
    }

    return true;
}

Anything that doesn't pass reopens the ticket without a model ever running, which handles most replies on its own.

A tiny agent with one job

Only the survivors reach a model, and it's the cheapest one we have, doing the smallest task there is: return a single boolean.

class DetectThanksOnlyMessageAgent implements Agent, HasMiddleware, HasStructuredOutput
{
    use Promptable;

    public function model(): string
    {
        return config('ai-pricing.cheap.name');
    }

    public function instructions(): Stringable|string
    {
        return view('prompts.ai.detect-thanks-only-message')->render();
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'is_thanks_only' => $schema->boolean()->required(),
        ];
    }
}

We ask for is_thanks_only, true or false, and Laravel's AI SDK hands back a validated shape, no parsing required. The prompt handles the nuance: it ignores quoted text, signatures, and "Sent from my iPhone" footers, counts "bedankt", "merci", and "danke" as thanks, and when in doubt classifies as not thanks-only.

Fail toward the safe behaviour

A model can be slow, rate-limited, or simply down. So the whole check fails in the safe direction.

try {
    $response = (new DetectThanksOnlyMessageAgent($ticket->workspace))
        ->prompt($messageText);
} catch (Throwable $exception) {
    Log::warning('Thanks-only detection failed; reopening ticket as usual', [
        'ticket_id' => $ticket->id,
        'message_id' => $message->id,
        'error' => $exception->getMessage(),
    ]);

    return false;
}

return (bool) ($response['is_thanks_only'] ?? false);

The expensive part is allowed to fail. The cheap fallback, reopening the ticket like we always did, is always underneath it.

Leave a trace

We never keep a ticket closed silently. Instead we record it as an activity on the ticket, with its own sparkles icon in the timeline.

if ($allowSuppression && $this->messageIsThanksOnly($ticket, $message, $textOverride)) {
    app(RecordTicketActivityAction::class)->execute(
        ticket: $ticket,
        type: ActivityType::ReopenSuppressed,
        properties: ['message_ulid' => $message->ulid],
    );

    return true;
}

app(ChangeTicketStatusAction::class)->execute($ticket, TicketStatus::Open);

So anyone scanning the ticket sees that a thank-you arrived and we chose to leave it closed. If we ever get it wrong, it's in the history instead of being a mystery.

In closing

This feature isn't rocket science. Nobody subscribes to a helpdesk for how it handles the word "thanks." But small things like this add up. They decide whether a tool feels good all day or like a thousand paper cuts.

We notice paper cuts like this because we use There There ourselves, every day, to support Mailcoach, Flare, and all other Spatie products. When something is annoying, we feel it before you do, and we fix it. Reopen suppression is just one of dozens of small fixes like that.

That's why we think There There can be one of the best helpdesks around: it's shaped by the people who use it every day.

Continue reading

The stack behind There There

The stack behind There There

A tour of the technology we used to build There There, and why we reached for the boring choices on purpose.

Freek
Freek · May 29, 2026 · 6 min read