Skip to content
TheThinkLoop

Template - Support

Predicate

Spam detection

Whether an inbound message is spam rather than a real customer contact.

A predicate decision for shared inboxes and contact forms, tuned to protect real customer messages from being discarded.

The decision

PredicateA probability from 0 to 1 that the condition holds.

Is this inbound message spam?

Evidence checks (asked in the same request)

  • Promotional content: The message is unsolicited promotional content.
  • References an account: The sender refers to a specific account, order, or ticket.

Expected output: A probability from 0 to 1 that the message is spam, plus one probability per evidence check.

Input

The Decisions API reads one input. Combine these fields into it as labeled text so the model sees the facts the decision depends on.

Input fields
FieldTypeRequiredDescription
messagestringYesThe customer's message, verbatim. Include the subject line if there is one.
sender_domainstringNoThe sender's email domain. Known customer domains are strong evidence against spam.

Worked examples

  1. Unsolicited SEO pitch

    Dear Sir, we offer guaranteed ranking on Google with our SEO services package. 500 backlinks for $99!!! Click here: http://seo-boost.example http://rank-fast.example

    A careful reviewer would say: Yes. Unsolicited promotion with no reference to the product or an account.

    Run this example
  2. A real customer who can't log in

    I can't log in to my account since this morning. The reset link never arrives. Ticket #5512 was closed without a fix.

    A careful reviewer would say: No. A specific account problem that references an existing ticket.

    Run this example

Expected answers are human judgments written for this template, not model output.

How it works

Spam filtering for a support inbox has a different cost profile from email spam filtering. Deleting one real customer message is worse than letting ten sales pitches through.

The "references an account" check is a counter-signal. If it fires, the message is probably real however promotional it reads.

Human review and thresholds

Recommended review policy

Never delete automatically. Move confident spam to a folder that someone skims weekly, and leave everything else in the queue. Review the folder for real customers until you trust the threshold.

Choosing thresholds

Set the bar for "spam" high. The cost of a false positive is a customer who believes you ignored them. The cost of a false negative is a few seconds of an agent's time.

Starting automation threshold
95%
Starting don’t-decide threshold
70%

Starting points for the playground, not recommendations. The right values depend on your costs and on how the model’s confidence tracks accuracy on your data. Calibrate them.

Limitations

  • Vendor outreach, partnership requests, and recruiting emails sit between spam and legitimate contact. Decide where they belong and say so in the question.
  • Phishing aimed at your team needs security tooling, not a triage classifier.
  • Short messages ("hello?", "call me") have too little evidence to classify reliably.

Use it in your code

The template definition is plain JSON: the question, the evidence checks, the input fields, and the starting policy. The SDK call below sends the same question and checks to the Decisions API from your server.

spam-detection.jsonjson
{
  "$format": "thethinkloop.template/v1",
  "slug": "spam-detection",
  "title": "Spam detection",
  "decides": "Whether an inbound message is spam rather than a real customer contact.",
  "question": {
    "name": "decision",
    "type": "predicate",
    "instructions": "Is this inbound message spam?"
  },
  "evidenceChecks": [
    {
      "name": "factor_1",
      "label": "Promotional content",
      "type": "predicate",
      "instructions": "The message is unsolicited promotional content."
    },
    {
      "name": "factor_2",
      "label": "References an account",
      "type": "predicate",
      "instructions": "The sender refers to a specific account, order, or ticket."
    }
  ],
  "inputs": [
    {
      "name": "message",
      "type": "string",
      "required": true,
      "description": "The customer's message, verbatim. Include the subject line if there is one."
    },
    {
      "name": "sender_domain",
      "type": "string",
      "required": false,
      "description": "The sender's email domain. Known customer domains are strong evidence against spam."
    }
  ],
  "expectedOutput": "A probability from 0 to 1 that the message is spam, plus one probability per evidence check.",
  "examples": [
    {
      "title": "Unsolicited SEO pitch",
      "input": "Dear Sir, we offer guaranteed ranking on Google with our SEO services package. 500 backlinks for $99!!! Click here: http://seo-boost.example http://rank-fast.example",
      "expected": "Yes"
    },
    {
      "title": "A real customer who can't log in",
      "input": "I can't log in to my account since this morning. The reset link never arrives. Ticket #5512 was closed without a fix.",
      "expected": "No"
    }
  ],
  "startingPolicy": {
    "automateAt": 0.95,
    "abstainBelow": 0.7,
    "note": "A starting point for exploration, not a recommendation. Calibrate against your own labeled examples."
  }
}
Equivalent SDK calljavascript
import OpenAI from "openai"; // openai >= 7.30.0

const client = new OpenAI(); // reads OPENAI_API_KEY on the server

const decision = await client.decisions.create({
  model: "gpt-6-luna",
  input: "Dear Sir, we offer guaranteed ranking on Google with our SEO services package. 500 backlinks for $99!!! Click here: http://seo-boost.example http://rank-fast.example",
  questions: [
    {
      type: "predicate",
      name: "decision",
      instructions: "Is this inbound message spam?"
    },
    {
      type: "predicate",
      name: "factor_1",
      instructions: "The message is unsolicited promotional content."
    },
    {
      type: "predicate",
      name: "factor_2",
      instructions: "The sender refers to a specific account, order, or ticket."
    }
  ],
});

// Answers come back in question order. A question can also be refused.
const [main, ...checks] = decision.answers;
if (main.type === "refusal") throw new Error("The model declined to answer.");
if (main.type === "predicate") {
  console.log(main.probability); // 0–1 estimate that the condition is true
}

Download it as spam-detection.json, or browse all developer resources.

Need this decision tuned to your policy?

We are running a small number of pilots with B2B teams: your policy, your examples, your thresholds. Request early access.