Support inbox that routes itself#
Problem: new tickets land in one pile and wait hours to be sorted. Build: one request per ticket picks the queue, ranks urgency and decides whether to refund on the spot.
{
"model": "decisionnode-latest",
"state": "Customer: I was charged twice and nobody has replied for 3 days.",
"questions": {
"route": {
"type": "choice",
"instructions": "Where should this go?",
"criteria": { "billing": "money", "bug": "broken", "account": "login" }
},
"urgency": {
"type": "score",
"instructions": "How urgent is this?",
"criteria": ["routine", "today", "urgent", "critical"]
},
"refund": {
"type": "truth",
"instructions": "Refund this automatically?"
}
}
}a = answers
if a["refund"]["truth"] >= 0.8:
issue_refund(ticket)
elif a["route"]["confidence"] < 0.7:
reply(ticket, template="ask_for_order_id") # the safe path, still automatic
else:
queues[a["route"]["choice"]].put(ticket, priority=round(a["urgency"]["score"]))Moderation pipeline#
Problem: comments need a decision in the time it takes to post them. Build: Flash answers three questions per comment; clear cases are published or removed on the spot, and the uncertain middle is shown with limited reach while the full model re-checks it. Here the model is sure the comment pushes buyers off the platform (0.96) even though the action itself is less certain.
{
"model": "decisionnode-flash-latest",
"state": {
"surface": "listing comments",
"comment": "DM me on telegram for half price, this seller is a fraud"
},
"questions": {
"action": {
"type": "choice",
"instructions": "What should happen to this comment?",
"criteria": {
"allow": "fine to show as is",
"limit": "show it with limited reach for now",
"remove": "breaks the community rules"
}
},
"off_platform": {
"type": "truth",
"instructions": "Is the comment trying to move a buyer off the platform?"
},
"toxicity": {
"type": "score",
"instructions": "How hostile is the language?",
"criteria": ["none", "mild", "strong", "severe"]
}
}
}const { action, off_platform, toxicity } = answers;
if (off_platform.truth >= 0.9 || (action.choice === "remove" && action.confidence >= 0.8)) {
await removeComment(comment, { reason: off_platform.truth >= 0.9 ? "off_platform" : "rules" });
} else if (action.choice === "allow" && action.confidence >= 0.8 && toxicity.score < 1) {
await publish(comment);
} else {
await limitReach(comment); // unsure: the safe action first
const recheck = await decide({ ...request, model: "decisionnode-latest" });
if (recheck.action.choice === "remove") await removeComment(comment, { reason: "rules" });
}Receipt and invoice checker#
Problem: expense claims arrive with a photo of the receipt and sit unpaid until they are checked. Build: send the claim as JSON and the receipt as an image; the model reads the printed total, currency and date and answers against the claim.
{
"model": "decisionnode-latest",
"state": {
"expense_claim": {
"amount": 48.2,
"currency": "EUR",
"date": "2026-09-28",
"merchant": "Northside Cafe"
}
},
"images": [
{ "id": "receipt", "media_type": "image/jpeg", "data": "<base64>" }
],
"questions": {
"matches": {
"type": "truth",
"instructions": "Does the receipt image show the same total, currency and date as the claim?"
},
"currency": {
"type": "choice",
"instructions": "Which currency is printed on the receipt?",
"criteria": {
"EUR": "euro",
"GBP": "pound sterling",
"USD": "US dollar"
}
},
"legible": {
"type": "truth",
"instructions": "Is the total on the receipt clearly legible?"
}
}
}a = answers
if a["legible"]["truth"] < 0.8:
ask_for_new_photo(claim)
elif a["matches"]["truth"] >= 0.9 and a["currency"]["choice"] == claim["currency"]:
approve(claim)
else:
reject(claim, reason="receipt does not match the claim") # and request a corrected receiptListing photo verifier#
Problem: a marketplace wants listing photos to show the actual item in the stated condition. Build: compare the listing record with its first photo. The score answer gives an expected condition of 2.15 (between good and like new), with the probabilities to back it.
{
"model": "decisionnode-latest",
"state": {
"listing": {
"title": "Oak dining table, seats 6",
"condition": "like new",
"finish": "natural oak"
}
},
"images": [
{ "id": "photo_1", "media_type": "image/webp", "data": "<base64>" }
],
"questions": {
"shows_item": {
"type": "truth",
"instructions": "Does photo_1 show the item described in the listing?"
},
"condition": {
"type": "score",
"instructions": "What condition is the item in, judging by the photo?",
"criteria": ["damaged", "worn", "good", "like new"]
},
"photo_kind": {
"type": "choice",
"instructions": "What kind of photo is this?",
"criteria": {
"own": "a real photo of this item",
"stock": "a catalogue or stock image",
"unclear": "cannot tell"
}
}
}
}Lead scoring#
Problem: sales wants the signups worth a reply today at the top. Build: score fit on your own rubric, classify intent, and filter out personal projects, all from the text of the form.
{
"model": "decisionnode-flash-latest",
"state": "Signup form. Company: 40-person logistics startup. Role: head of engineering. Message: We classify about 200k shipping exceptions a day with an LLM and it is too slow and too expensive. Want to test this next week.",
"questions": {
"fit": {
"type": "score",
"instructions": "How well does this lead fit a high-volume API product?",
"criteria": ["poor", "possible", "good", "excellent"]
},
"intent": {
"type": "choice",
"instructions": "What does this person want right now?",
"criteria": {
"buying": "ready to buy or start a trial",
"researching": "comparing options",
"support": "an existing customer with a problem",
"other": "none of these"
}
},
"personal": {
"type": "truth",
"instructions": "Is this a personal or student project?"
}
}
}const { fit, intent, personal } = answers;
const priority = personal.truth > 0.5 ? 0 : fit.score * (intent.choice === "buying" ? 2 : 1);
await crm.update(lead.id, { priority, intent: intent.choice });Statement reader that chases late invoices#
Problem: finance reads customer statements by hand to decide who gets a reminder. Build: two Number questions count the overdue invoices (4, probability 0.84) and read how late the oldest one is (78 days), and a Choice picks the reminder. The reminder goes out on its own.
{
"model": "decisionnode-latest",
"state": "Customer statement\nAccount: Norrvik Supply AB (30-4471)\nStatement date: 2026-10-01\nTerms: net 30, amounts in EUR\n\nInvoice Issued Due Amount Status\nINV-2041 2026-06-15 2026-07-15 1,240.00 open\nINV-2058 2026-07-03 2026-08-02 410.00 paid 2026-08-01\nINV-2063 2026-07-21 2026-08-20 860.00 open\nINV-2069 2026-07-29 2026-08-28 395.00 paid 2026-09-02\nINV-2077 2026-08-06 2026-09-05 1,120.00 part paid, 300.00 open\nINV-2081 2026-08-13 2026-09-12 640.00 open\nINV-2090 2026-08-31 2026-09-30 780.00 paid 2026-09-29\nINV-2094 2026-09-10 2026-10-10 520.00 open\nINV-2102 2026-09-24 2026-10-24 915.00 open",
"questions": {
"overdue": {
"type": "number",
"instructions": "How many invoices on this statement are overdue on the statement date?",
"min": 0,
"max": 30
},
"oldest_days": {
"type": "number",
"instructions": "How many days past its due date is the oldest unpaid invoice?",
"min": 0,
"max": 180
},
"reminder": {
"type": "choice",
"instructions": "Which reminder should go out today?",
"criteria": {
"none": "nothing is overdue, send nothing",
"friendly": "a friendly reminder for one recent invoice",
"firm": "a firm reminder listing every overdue invoice",
"final": "a final notice before the account is passed to collections"
}
}
}
}overdue, oldest, reminder = a["overdue"], a["oldest_days"], a["reminder"]
if overdue["confidence"] >= 0.7 and reminder["confidence"] >= 0.6:
send_reminder(account, template=reminder["choice"],
overdue=overdue["number"], oldest_days=oldest["number"])
else:
# unsure: the gentlest reminder is the safe path, still sent automatically
send_reminder(account, template="friendly")Agent that asks before it acts#
Problem: an agent with tools can do real damage on one bad call. Build: pass each proposed tool call to DecisionNode and run it only when the answer clears your bar. The full recipe is in Guardrails.