Docs menu

Cookbook

Recipe: score inbound leads

Pick what the sender wants, rate how well the company fits your ideal customer and check whether they ask for a call, then fill the CRM fields and book the call.

View as Markdown

Every inbound form gets an intent, a fit score and a meeting flag as it arrives, and sales starts from filled CRM fields. On the same model version the same lead always gets the same score, so the ranking holds from one day to the next.

The request

  • State. The form as JSON and the page it came from. intent and wants_meeting point at {{form.message}}, while fit points at the whole {{form}} object so it reads the company size and country too. Leave out what the decision does not need, such as the email address.
  • intent is a pick of five reasons to write. They are mutually exclusive, so one pick is better than five checks, and spam is the way out.
  • fit is a rate in beta on four levels. The criteria define your ideal customer profile in one sentence: an EU company with more than 50 employees that handles thousands of messages, tickets or documents a month. Change the sentence and the scale follows.
  • wants_meeting is a check: one yes or no fact about the message.
  • min_confidence on every question, so an unsure answer leaves its CRM field empty instead of guessing.
{
  "state": {
    "form": {
      "name": "Sanne de Vries",
      "company": "Noordhaven Logistics",
      "company_size": "250 employees",
      "country": "NL",
      "message": "We route about 40,000 customer emails a month by hand and want to automate the first triage. Can we get pricing for that volume and set up a call next week?"
    },
    "source": "pricing page contact form"
  },
  "questions": {
    "intent": {
      "type": "pick",
      "instructions": "What does the sender of {{form.message}} want?",
      "options": {
        "buy": "Wants to buy or asks about pricing",
        "partner": "Proposes a partnership or reselling",
        "support": "Needs help with an existing account",
        "job": "Applies for a job",
        "spam": "Unsolicited sales pitch or nonsense"
      },
      "min_confidence": 0.5
    },
    "fit": {
      "type": "rate",
      "instructions": "How well does the company in {{form}} fit our ideal customer?",
      "criteria": "Our ideal customer is an EU company with more than 50 employees that handles thousands of messages, tickets or documents a month.",
      "levels": ["Poor", "Fair", "Good", "Excellent"],
      "min_confidence": 0.3
    },
    "wants_meeting": {
      "type": "check",
      "instructions": "Does the sender of {{form.message}} ask for a call or a meeting?",
      "min_confidence": 0.6
    }
  }
}

Run it

Put a test key in DEX_API_KEY (see the Quickstart). Save the Python code as lead_scoring.py and run python lead_scoring.py. Save the TypeScript code as lead-scoring.mts and run npx tsx lead-scoring.mts: the code uses await at the top level, and the .mts ending makes the file an ES module. Install the SDKs from Downloads.

curl https://api.thinqit.ai/v1/decide \
  -H "authorization: Bearer $DEX_API_KEY" \
  -H "content-type: application/json" \
  --data-binary @- <<'DEX_REQUEST'
{
  "state": {
    "form": {
      "name": "Sanne de Vries",
      "company": "Noordhaven Logistics",
      "company_size": "250 employees",
      "country": "NL",
      "message": "We route about 40,000 customer emails a month by hand and want to automate the first triage. Can we get pricing for that volume and set up a call next week?"
    },
    "source": "pricing page contact form"
  },
  "questions": {
    "intent": {
      "type": "pick",
      "instructions": "What does the sender of {{form.message}} want?",
      "options": {
        "buy": "Wants to buy or asks about pricing",
        "partner": "Proposes a partnership or reselling",
        "support": "Needs help with an existing account",
        "job": "Applies for a job",
        "spam": "Unsolicited sales pitch or nonsense"
      },
      "min_confidence": 0.5
    },
    "fit": {
      "type": "rate",
      "instructions": "How well does the company in {{form}} fit our ideal customer?",
      "criteria": "Our ideal customer is an EU company with more than 50 employees that handles thousands of messages, tickets or documents a month.",
      "levels": ["Poor", "Fair", "Good", "Excellent"],
      "min_confidence": 0.3
    },
    "wants_meeting": {
      "type": "check",
      "instructions": "Does the sender of {{form.message}} ask for a call or a meeting?",
      "min_confidence": 0.6
    }
  }
}
DEX_REQUEST
# Save as dex_request.py, then run: python dex_request.py
import json

from thinqit_dex import Client

client = Client()  # reads DEX_API_KEY from the environment

request = json.loads(r'''
{
  "state": {
    "form": {
      "name": "Sanne de Vries",
      "company": "Noordhaven Logistics",
      "company_size": "250 employees",
      "country": "NL",
      "message": "We route about 40,000 customer emails a month by hand and want to automate the first triage. Can we get pricing for that volume and set up a call next week?"
    },
    "source": "pricing page contact form"
  },
  "questions": {
    "intent": {
      "type": "pick",
      "instructions": "What does the sender of {{form.message}} want?",
      "options": {
        "buy": "Wants to buy or asks about pricing",
        "partner": "Proposes a partnership or reselling",
        "support": "Needs help with an existing account",
        "job": "Applies for a job",
        "spam": "Unsolicited sales pitch or nonsense"
      },
      "min_confidence": 0.5
    },
    "fit": {
      "type": "rate",
      "instructions": "How well does the company in {{form}} fit our ideal customer?",
      "criteria": "Our ideal customer is an EU company with more than 50 employees that handles thousands of messages, tickets or documents a month.",
      "levels": ["Poor", "Fair", "Good", "Excellent"],
      "min_confidence": 0.3
    },
    "wants_meeting": {
      "type": "check",
      "instructions": "Does the sender of {{form.message}} ask for a call or a meeting?",
      "min_confidence": 0.6
    }
  }
}
''')

decision = client.decide(
    request["state"],
    request["questions"],
)
for question_id, answer in decision.answers.items():
    print(question_id, answer)
// Save as dex-request.mts, then run: npx tsx dex-request.mts (Node.js 18 or newer)
import { Client, parseRequest } from "@thinqit/dex";

const client = new Client(); // reads DEX_API_KEY from the environment

// parseRequest keeps the key order of the text (JSON.parse would move labels such as "1" to the front).
const request = parseRequest(`{
  "state": {
    "form": {
      "name": "Sanne de Vries",
      "company": "Noordhaven Logistics",
      "company_size": "250 employees",
      "country": "NL",
      "message": "We route about 40,000 customer emails a month by hand and want to automate the first triage. Can we get pricing for that volume and set up a call next week?"
    },
    "source": "pricing page contact form"
  },
  "questions": {
    "intent": {
      "type": "pick",
      "instructions": "What does the sender of {{form.message}} want?",
      "options": {
        "buy": "Wants to buy or asks about pricing",
        "partner": "Proposes a partnership or reselling",
        "support": "Needs help with an existing account",
        "job": "Applies for a job",
        "spam": "Unsolicited sales pitch or nonsense"
      },
      "min_confidence": 0.5
    },
    "fit": {
      "type": "rate",
      "instructions": "How well does the company in {{form}} fit our ideal customer?",
      "criteria": "Our ideal customer is an EU company with more than 50 employees that handles thousands of messages, tickets or documents a month.",
      "levels": ["Poor", "Fair", "Good", "Excellent"],
      "min_confidence": 0.3
    },
    "wants_meeting": {
      "type": "check",
      "instructions": "Does the sender of {{form.message}} ask for a call or a meeting?",
      "min_confidence": 0.6
    }
  }
}`);

const decision = await client.decide(request);
console.log(decision.answers);

Expected output

{
  "id": "req_01M3JE1VPZ9WWB18Z4F27NS6C9",
  "object": "decision",
  "created": 1790546341,
  "model": "dex-1.0.1",
  "served_by": "gpu",
  "calibration": "cal-20260926-1",
  "answers": {
    "intent": {
      "type": "pick",
      "choice": "buy",
      "probabilities": {
        "buy": 0.9433,
        "partner": 0.0159,
        "support": 0.0165,
        "job": 0.0116,
        "spam": 0.0127
      },
      "confidence": 0.9268,
      "abstained": false
    },
    "fit": {
      "type": "rate",
      "rating": 2.3044,
      "levels": ["Poor", "Fair", "Good", "Excellent"],
      "probabilities": [0.0127, 0.0362, 0.585, 0.3661],
      "confidence": 0.5997,
      "abstained": false
    },
    "wants_meeting": {
      "type": "check",
      "probability": 0.9458,
      "confidence": 0.8915,
      "abstained": false
    }
  },
  "usage": {
    "input_tokens": 216,
    "state_tokens": 87,
    "question_tokens": 129,
    "allowance_tokens": 0,
    "paid_tokens": 0,
    "charge_micro_cents": 0,
    "unit_price_micro_cents": 0,
    "tier": "test"
  }
}

Captured from the live API on 2026-09-27 with a test key: model dex-1.0.1, calibration cal-20260926-1, served_by: gpu, 216 input tokens (87 for the state, 129 for the questions). A test key is charged nothing, so tier is test and the charge is 0. On this exact version the same request always returns these answers.

Question Type Answer Confidence min_confidence Abstained
intent pick buy (0.9433) 0.9268 0.5 no
fit rate rating 2.3044, most likely Good (0.585) 0.5997 0.3 no
wants_meeting check yes with probability 0.9458 0.8915 0.6 no

The sender wants to buy (0.9433) and asks for a call (0.9458). The fit rating of 2.3044 is closest to Good (0.585), with Excellent second at 0.3661. No answer abstained, so every CRM field gets a value.

Act on it

lead_id and the functions update_crm and book_call stand for your own code. Each typed answer fills one CRM field: the pick's label, the nearest level of the rating (2.3044 rounds to 2, which is Good) and a yes or no for the meeting. Here all three are filled, and the lead wants to buy, fits well and asks for a call, so the code books it.

crm = {}
intent = decision.pick("intent")
if not intent.abstained:
    crm["intent"] = intent.choice

fit = decision.rate("fit")
if not fit.abstained:
    crm["fit"] = fit.levels[round(fit.rating)]  # the nearest level

meeting = decision.check("wants_meeting")
if not meeting.abstained:
    crm["wants_meeting"] = meeting.probability >= 0.5

update_crm(lead_id, crm)  # an abstained answer leaves its field empty for sales
if crm.get("intent") == "buy" and crm.get("fit") in ("Good", "Excellent") and crm.get("wants_meeting"):
    book_call(lead_id)
const { intent, fit, wants_meeting: meeting } = decision.answers;
const crm: Record<string, string | boolean> = {};

if (intent?.type === "pick" && !intent.abstained) crm.intent = intent.choice;
if (fit?.type === "rate" && !fit.abstained) crm.fit = fit.levels[Math.round(fit.rating)]; // the nearest level
if (meeting?.type === "check" && !meeting.abstained) crm.wants_meeting = meeting.probability >= 0.5;

updateCrm(leadId, crm); // an abstained answer leaves its field empty for sales
if (crm.intent === "buy" && (crm.fit === "Good" || crm.fit === "Excellent") && crm.wants_meeting === true) bookCall(leadId);

As in the support recipe, the TypeScript answers have the general Answer type, so the code narrows each one on type.

Adapt it

  • Your profile, in one sentence. Put what makes a good customer in criteria, in English, as facts a form can show: country, size, volume. Dex reads the form; it does not look the company up. When you have data from your own sources, pass it as state fields.
  • Rank, not only label. Store fit.rating as a number from 0 to 3 next to the level name, and sort the sales queue on it.
  • Forms are written by strangers. Text in the state can try to steer the answer. Add a guardrail check as in the agent tool gate recipe before a score triggers anything you cannot undo.
  • Tune on leads you already know. Run past forms whose outcome you know with a test key, and move the floors and the booking rule until the booked calls are the ones sales wants. See Abstention.