---
title: "Classify support tickets in C#"
url: https://recursiveai.org/onellm/cookbook/classify-support-tickets/
description: "Classify support tickets with an LLM in C#. A complete .NET 10 example that runs offline with OneLLM, real output and a runnable download."
version: "1.0.1"
---

# Classify support tickets in C#

**OneLLM** is a free, open-source LLM and agent library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneLlm) · [GitHub](https://github.com/recursive-ai-oss/onellm-csharp) · [Docs](https://recursiveai.org/onellm/docs/)

Use an LLM to sort text into fixed labels and get a typed C# result.

```shell
dotnet add package RecursiveAI.OneLlm
```

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using OneLlm;
// ticket, Route, TicketJson and Llm.Default:
// see the full program.
Route route = await Llm.AskAsync(
    "gpt-6-luna", "Classify: " + ticket,
    TicketJson.Default.Route);
```

Returns a typed route.

**[Download this example (.zip)](https://recursiveai.org/onellm/cookbook/classify-support-tickets/classify-support-tickets.zip)**: unzip, then `dotnet run`.

## The full program

The download runs offline with canned replies, no API key needed.

`gpt-6-luna` is a model name in the shipped OpenAI catalog, not a variable defined by this program ([model names](https://recursiveai.org/onellm/docs/getting-started/#model-names)).

The download supplies two sample tickets and two scripted replies. `Llm.AskAsync` requests a JSON object using the generated serialization metadata, then reads it into `Route`. The allowed-team check is application code, separate from parsing the JSON.

```csharp
using OneLlm;
using OneLlm.Testing;

var runtime = new Runtime();
var scripted = new ScriptedProvider("openai");
runtime.Providers.Set("openai", scripted);
Llm.Default = runtime;

string[] tickets = ["I was charged twice.", "My parcel has not arrived."];
scripted.EnqueueText("""{"Team":"billing"}""");
scripted.EnqueueText("""{"Team":"shipping"}""");
foreach (string ticket in tickets)
{
    var answer = await Llm.AskAsync("gpt-6-luna",
        "Classify this ticket. Team must be billing, shipping, or other. " + ticket,
        TicketJson.Default.Route);
    string team = answer.Team;
    if (team is not ("billing" or "shipping" or "other"))
        team = "other";
    Console.WriteLine($"{ticket} -> {team}");
}

public sealed record Route(string Team);
[System.Text.Json.Serialization.JsonSerializable(typeof(Route))]
internal partial class TicketJson : System.Text.Json.Serialization.JsonSerializerContext;
```

`dotnet run` prints:

```text
I was charged twice. -> billing
My parcel has not arrived. -> shipping
```

Invalid JSON for the requested type throws `StructuredOutputException`; an unrecognized team in valid JSON falls back to `other` in this program.

### With a real model

The [pinned getting-started docs](https://recursiveai.org/onellm/docs/getting-started/#install-and-keys) name `OPENAI_API_KEY` for OpenAI. Set that environment variable to your own credential outside source code. Remove the `ScriptedProvider` declaration, its `runtime.Providers.Set("openai", scripted)` registration, and every `scripted.Enqueue...` call from the full program. The runtime uses the built-in provider instead:

```csharp
using OneLlm;
var runtime = new Runtime();
Llm.Default = runtime;
```

This replaces the scripted setup; keep the remaining task code. OneLLM reads the environment key when the first OpenAI request is made, as the docs describe. A missing variable throws `AuthException` naming it.

This live-model path was not run here: no API key was used. The output shown above comes from the scripted provider, not from OpenAI.

## Variations

**Add a manual-review fallback.** The full program’s allowlist can route unexpected values to a review queue instead:

```csharp
string team = answer.Team;
if (team is not ("billing" or "shipping" or "other"))
    team = "review";
Console.WriteLine(team);
```

With a scripted `{"Team":"sales"}` reply this prints `review`. That is an application fallback, not a model confidence score.

## Related recipes

- [Require human approval for LLM tool calls in .NET](https://recursiveai.org/onellm/cookbook/approve-agent-tool-calls/)
- [Persist LLM chat history in C# and resume it](https://recursiveai.org/onellm/cookbook/save-and-resume-a-chat/)
- [Use IChatClient with Anthropic Claude and Gemini in C#](https://recursiveai.org/onellm/cookbook/ichatclient-for-claude-and-gemini/)
- [Generate a Git commit message with an LLM in C#](https://recursiveai.org/onellm/cookbook/commit-message-from-git-diff/)

---

## The short answer

Ask for a typed classification result, then validate its label before you use it to route work. OneLLM can derive a structured-output schema from source-generated JSON metadata and read the reply into a C# record. In this recipe the record contains `Team`, and host code accepts only `billing`, `shipping`, or `other`.

The download runs offline with canned replies, no API key needed. It exercises schema creation, response parsing and the routing branch with known replies. It does not measure classification accuracy or compare models. Replace the scripted provider with a configured model provider only after you have tests for your own tickets and categories.

## Can I use an LLM to classify text?

Yes: send the text and the set of labels, and request the label in a structured response. This is useful when tickets arrive in varied language but your application needs a small, stable set of routes. Here, “I was charged twice” belongs to billing, while a missing parcel belongs to shipping.

Make category definitions operational. Billing might mean payment, invoices and refunds; shipping might mean delivery dates, tracking and missing parcels. Decide which category wins when a ticket mentions both, and keep `other` for requests outside the categories. A label list alone does not define those boundary cases. Put those decisions into the prompt you use in production and test them with examples your support team agrees on.

The C# record supplies the response shape, not a guarantee that the model chose the correct team. A string property also does not itself enforce the three labels in this example. That is why the full program checks the returned string against an allowlist before routing it. Well-formed JSON and a correct business decision are different things.

For higher-stakes routing, keep the original ticket available to the reviewer and record which prompt and model produced the route. Avoid treating the model’s explanatory text as an instruction to perform unrelated account operations. This recipe returns a label only; it does not grant tools access to the customer’s account.

## How to do classification with LLM?

Start with representative tickets, agreed labels and a fallback. Use the full program as the smallest typed path: a ticket string enters the prompt, a `Route` comes back, and a checked label determines the destination. Add tests for empty tickets, mixed topics, unsupported labels and malformed replies. The [offline testing recipe](https://recursiveai.org/onellm/cookbook/test-llm-code-without-api-key/) shows how to inspect the requests a scripted provider receives.

Use a live, separately configured provider for quality evaluation; a scripted answer can prove that your fallback branch runs but cannot prove that a model understands a ticket. Do not infer probabilities from a successful parse. This sample has no confidence field and no calibrated score.

OneLLM also has a [decider API](https://recursiveai.org/onellm/docs/deciders/) for typed choice, score and yes/no questions. Its chat engine returns probabilities and derives choice, score and confidence from them; the docs say off-schema replies are retried up to three times, never repaired. That is a separate workflow from the direct structured-output call used here. Choose it when your application needs those question types and saved decision runs, rather than adding a made-up confidence number to a string-label response.
