Docs menu

Cookbook

Recipe: triage documents and email

Pick the document type and the department, check for a deadline and rate the importance of a Dutch email in one call, then file it with the right department.

View as Markdown

A payment reminder arrives in Dutch. One request says what kind of document it is, which department gets it, whether it sets a deadline and how important it is. The email is read once for all four questions, and your code files it with the right department.

The request

  • State. The email as JSON: sender, subject, text and the name of the attachment. Dex reads text only, so it sees the file name, not the PDF. To use what the PDF says, extract its text first and pass it as a field.
  • A Dutch email, English questions. The instructions, options and criteria are in English while the email is in Dutch. Mixing languages is fine, and criteria in English are followed more reliably than Dutch ones: see Known limits.
  • Two picks, doc_type and department, each with other as the way out. They are separate questions because the answers do not map one to one: a contract can go to legal or to finance.
  • deadline is a check. Dex says whether a date is set; it does not return the date. Pulling values out of text is a job for your code.
  • importance is a rate in beta, with a house rule in criteria: a reminder that threatens interest or collection costs is at least Normal.
  • Four questions, one state. All four point at {{email.text}}, and the state is billed once: 157 state tokens, however many questions read it.
{
  "state": {
    "email": {
      "from": "administratie@groothandel-vandijk.nl",
      "subject": "Herinnering factuur 2026-0917",
      "text": "Geachte heer, mevrouw, volgens onze administratie staat factuur 2026-0917 van 1.284,50 euro nog open. Wij verzoeken u het bedrag uiterlijk 15 oktober over te maken. Bij uitblijven van betaling brengen wij wettelijke rente in rekening. Met vriendelijke groet, Van Dijk Bakkerijgroothandel"
    },
    "attachments": ["factuur-2026-0917.pdf"]
  },
  "questions": {
    "doc_type": {
      "type": "pick",
      "instructions": "What kind of document is {{email.text}}?",
      "options": {
        "invoice": "An invoice or a payment reminder",
        "contract": "A contract, offer or terms",
        "complaint": "A complaint about our service or products",
        "other": null
      },
      "min_confidence": 0.5
    },
    "department": {
      "type": "pick",
      "instructions": "Which department should receive {{email.text}}?",
      "options": {
        "finance": "Accounts payable and receivable",
        "legal": "Contracts, disputes and legal letters",
        "customer_service": "Questions and complaints from customers",
        "other": null
      },
      "min_confidence": 0.5
    },
    "deadline": {
      "type": "check",
      "instructions": "Does {{email.text}} set a deadline or a due date?",
      "min_confidence": 0.6
    },
    "importance": {
      "type": "rate",
      "instructions": "How important is it to act on {{email.text}}?",
      "criteria": "A payment reminder that threatens interest or collection costs is at least Normal.",
      "levels": ["Low", "Normal", "High"],
      "min_confidence": 0.3
    }
  }
}

Run it

Put a test key in DEX_API_KEY (see the Quickstart). Save the Python code as document_triage.py and run python document_triage.py. Save the TypeScript code as document-triage.mts and run npx tsx document-triage.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": {
    "email": {
      "from": "administratie@groothandel-vandijk.nl",
      "subject": "Herinnering factuur 2026-0917",
      "text": "Geachte heer, mevrouw, volgens onze administratie staat factuur 2026-0917 van 1.284,50 euro nog open. Wij verzoeken u het bedrag uiterlijk 15 oktober over te maken. Bij uitblijven van betaling brengen wij wettelijke rente in rekening. Met vriendelijke groet, Van Dijk Bakkerijgroothandel"
    },
    "attachments": ["factuur-2026-0917.pdf"]
  },
  "questions": {
    "doc_type": {
      "type": "pick",
      "instructions": "What kind of document is {{email.text}}?",
      "options": {
        "invoice": "An invoice or a payment reminder",
        "contract": "A contract, offer or terms",
        "complaint": "A complaint about our service or products",
        "other": null
      },
      "min_confidence": 0.5
    },
    "department": {
      "type": "pick",
      "instructions": "Which department should receive {{email.text}}?",
      "options": {
        "finance": "Accounts payable and receivable",
        "legal": "Contracts, disputes and legal letters",
        "customer_service": "Questions and complaints from customers",
        "other": null
      },
      "min_confidence": 0.5
    },
    "deadline": {
      "type": "check",
      "instructions": "Does {{email.text}} set a deadline or a due date?",
      "min_confidence": 0.6
    },
    "importance": {
      "type": "rate",
      "instructions": "How important is it to act on {{email.text}}?",
      "criteria": "A payment reminder that threatens interest or collection costs is at least Normal.",
      "levels": ["Low", "Normal", "High"],
      "min_confidence": 0.3
    }
  }
}
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": {
    "email": {
      "from": "administratie@groothandel-vandijk.nl",
      "subject": "Herinnering factuur 2026-0917",
      "text": "Geachte heer, mevrouw, volgens onze administratie staat factuur 2026-0917 van 1.284,50 euro nog open. Wij verzoeken u het bedrag uiterlijk 15 oktober over te maken. Bij uitblijven van betaling brengen wij wettelijke rente in rekening. Met vriendelijke groet, Van Dijk Bakkerijgroothandel"
    },
    "attachments": ["factuur-2026-0917.pdf"]
  },
  "questions": {
    "doc_type": {
      "type": "pick",
      "instructions": "What kind of document is {{email.text}}?",
      "options": {
        "invoice": "An invoice or a payment reminder",
        "contract": "A contract, offer or terms",
        "complaint": "A complaint about our service or products",
        "other": null
      },
      "min_confidence": 0.5
    },
    "department": {
      "type": "pick",
      "instructions": "Which department should receive {{email.text}}?",
      "options": {
        "finance": "Accounts payable and receivable",
        "legal": "Contracts, disputes and legal letters",
        "customer_service": "Questions and complaints from customers",
        "other": null
      },
      "min_confidence": 0.5
    },
    "deadline": {
      "type": "check",
      "instructions": "Does {{email.text}} set a deadline or a due date?",
      "min_confidence": 0.6
    },
    "importance": {
      "type": "rate",
      "instructions": "How important is it to act on {{email.text}}?",
      "criteria": "A payment reminder that threatens interest or collection costs is at least Normal.",
      "levels": ["Low", "Normal", "High"],
      "min_confidence": 0.3
    }
  }
}
''')

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": {
    "email": {
      "from": "administratie@groothandel-vandijk.nl",
      "subject": "Herinnering factuur 2026-0917",
      "text": "Geachte heer, mevrouw, volgens onze administratie staat factuur 2026-0917 van 1.284,50 euro nog open. Wij verzoeken u het bedrag uiterlijk 15 oktober over te maken. Bij uitblijven van betaling brengen wij wettelijke rente in rekening. Met vriendelijke groet, Van Dijk Bakkerijgroothandel"
    },
    "attachments": ["factuur-2026-0917.pdf"]
  },
  "questions": {
    "doc_type": {
      "type": "pick",
      "instructions": "What kind of document is {{email.text}}?",
      "options": {
        "invoice": "An invoice or a payment reminder",
        "contract": "A contract, offer or terms",
        "complaint": "A complaint about our service or products",
        "other": null
      },
      "min_confidence": 0.5
    },
    "department": {
      "type": "pick",
      "instructions": "Which department should receive {{email.text}}?",
      "options": {
        "finance": "Accounts payable and receivable",
        "legal": "Contracts, disputes and legal letters",
        "customer_service": "Questions and complaints from customers",
        "other": null
      },
      "min_confidence": 0.5
    },
    "deadline": {
      "type": "check",
      "instructions": "Does {{email.text}} set a deadline or a due date?",
      "min_confidence": 0.6
    },
    "importance": {
      "type": "rate",
      "instructions": "How important is it to act on {{email.text}}?",
      "criteria": "A payment reminder that threatens interest or collection costs is at least Normal.",
      "levels": ["Low", "Normal", "High"],
      "min_confidence": 0.3
    }
  }
}`);

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

Expected output

{
  "id": "req_01M3JE1QF2XKE5CZKRNCKFST2C",
  "object": "decision",
  "created": 1790546337,
  "model": "dex-1.0.1",
  "served_by": "gpu",
  "calibration": "cal-20260926-1",
  "answers": {
    "doc_type": {
      "type": "pick",
      "choice": "invoice",
      "probabilities": {
        "invoice": 0.9435,
        "contract": 0.023,
        "complaint": 0.0199,
        "other": 0.0136
      },
      "confidence": 0.9205,
      "abstained": false
    },
    "department": {
      "type": "pick",
      "choice": "finance",
      "probabilities": {
        "finance": 0.917,
        "legal": 0.0328,
        "customer_service": 0.0302,
        "other": 0.02
      },
      "confidence": 0.8842,
      "abstained": false
    },
    "deadline": {
      "type": "check",
      "probability": 0.9509,
      "confidence": 0.9017,
      "abstained": false
    },
    "importance": {
      "type": "rate",
      "rating": 1.0585,
      "levels": ["Low", "Normal", "High"],
      "probabilities": [0.0202, 0.9011, 0.0787],
      "confidence": 0.6909,
      "abstained": false
    }
  },
  "usage": {
    "input_tokens": 290,
    "state_tokens": 157,
    "question_tokens": 133,
    "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, 290 input tokens (157 for the state, 133 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
doc_type pick invoice (0.9435) 0.9205 0.5 no
department pick finance (0.917) 0.8842 0.5 no
deadline check yes with probability 0.9509 0.9017 0.6 no
importance rate rating 1.0585, most likely Normal (0.9011) 0.6909 0.3 no

The email is an invoice (0.9435) for finance (0.917), and it sets a deadline (0.9509). The importance rating of 1.0585 sits on Normal (0.9011), as the house rule asks for a reminder that threatens interest. No answer abstained.

Act on it

email_id and the functions route_to_person, file_email, add_label and set_priority stand for your own code. Here the email is filed with finance and labelled invoice and has_deadline. It keeps its normal priority, because 1.0585 is below the bar of 1.5.

department = decision.pick("department")
if department.abstained:
    route_to_person(email_id)  # two departments are close: a person files it
else:
    file_email(email_id, department.choice)

doc_type = decision.pick("doc_type")
if not doc_type.abstained:
    add_label(email_id, doc_type.choice)

deadline = decision.check("deadline")
if not deadline.abstained and deadline.probability >= 0.5:
    add_label(email_id, "has_deadline")  # Dex says a date is there; your code or a person reads it

importance = decision.rate("importance")
if not importance.abstained and importance.rating >= 1.5:
    set_priority(email_id, "high")
const { doc_type: docType, department, deadline, importance } = decision.answers;

if (department?.type === "pick") {
  if (department.abstained) routeToPerson(emailId); // two departments are close: a person files it
  else fileEmail(emailId, department.choice);
}
if (docType?.type === "pick" && !docType.abstained) addLabel(emailId, docType.choice);
if (deadline?.type === "check" && !deadline.abstained && deadline.probability >= 0.5) {
  addLabel(emailId, "has_deadline"); // Dex says a date is there; your code or a person reads it
}
if (importance?.type === "rate" && !importance.abstained && importance.rating >= 1.5) setPriority(emailId, "high");

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

Adapt it

  • Your own departments. Up to 255 options per pick; describe each department by what it receives. For a deep structure, pick the department first, then send a second request that picks a queue within it.
  • Dates and amounts in code. To know whether a due date is close, read the date in your code and pass days_until_due as a field. Dex reads; it does not calculate. See Known limits.
  • Keep criteria in English, even for a mailbox that only gets Dutch email, and test each rule on a few of your own messages.
  • Email is written by outsiders. Text in the state can try to steer the answer. For a high-impact action, add a guardrail check as in the agent tool gate recipe.