Most of the calls inside a production .NET agent are not “write me a paragraph.” They are small, repeated judgments: which team owns this ticket, is this reply promising a refund, how severe is this incident, should the agent continue or hand off.
Those steps still get sent to a frontier chat model because that is the client everyone already wired up. It works. It is also slow, expensive, and a little awkward — you ask for JSON, then you parse prose that almost looks like JSON, then you invent a confidence score the model never actually returned.
Microsoft-Decision-1, now in public preview in the Foundry model catalog, is built for the other job. It does not draft an answer. It scores a fixed set of options and hands your code a typed decision plus probabilities.
This post is the C# version of that idea. The official quickstart is Python. The API is just HTTP, so a console app and HttpClient are enough.
Sources for the numbers below: the Foundry announcement (October 9, 2026), the Command Line deep dive, and the Foundry how-to.
What a decision model actually returns
A generative model produces text. Microsoft-Decision-1 produces a judgment about text or JSON you already have.
You send:
- a state — the thing being judged (a string, or a JSON object)
- one or more questions, each with a type and a closed set of options
You get back an answers object keyed by your question names. No rationale. No tool calls. No “here is my reasoning.” Your application is supposed to act on the numbers: route, gate, rank, or escalate.
Microsoft describes four places this shows up inside an app:
- Classification — intents, themes, document types
- Routing — which model, tool, team, or workflow
- Evaluation — does this draft meet the rubric
- Workflow control — continue, retry, stop, or send for review
The current model is post-trained from Qwen3.5-9B for single-pass decision scoring. Microsoft says later versions will be rebased on MAI and OpenAI models. Treat the weights as a preview, not a frozen contract.
Three question types, not a prompt template
The request is not a chat completion. It goes to the Microsoft provider route on your Foundry resource:
POST {resource-endpoint}/providers/microsoft/v1/systemone
The model field is your deployment name, not the catalog name. The response model field can still say microsoft-decision-1. That mismatch is expected.
| Question | JSON type | What you get |
|---|---|---|
| Is this true? | noul | A probability from 0 to 1 |
| Which option is it? | choice | One selected label, plus a probability for every label |
| How much? | score | A value on an ordered scale, plus a probability per level |
criteria is a dictionary for choice and noul (optional on noul). For score it is an array ordered from lowest to highest. The score is a probability-weighted average of the level indexes, so a four-level scale (0 through 3) can land between levels. Use it for ordering and thresholds, not as a calibrated star rating.
Questions that share the same state can ride in one request. That is the pattern worth copying: route the ticket, score severity, and check “is this a repeat?” without three round trips.
The numbers worth arguing about
Microsoft published these from internal evals. They are not your workload. They are the reason to run your own labeled set before you swap a GPT deployment out.
| Claim | Source figure |
|---|---|
| Latency vs GPT-6 Sol | About 35× faster (P50); also described as 35× quicker in the 36-benchmark timing run |
| Latency vs H2O-Lightning-4B v1.1 | 2.5× quicker than the runner-up |
| Held-out quality | Highest accuracy in a 36-benchmark comparison, nearly 150,000 questions kept blind from training |
| Robustness | Decision flips on 1.3% of perturbations on average; 0 flips when option text is paraphrased or options are reversed or shuffled |
| Safety screen | 5,250 requests across 11 benchmarks (harmful content, jailbreaks, prompt injection) |
| Xbox Research labeling | 10,000+ feedback items. Command Line: competitive with GPT-6 Sol, 14× faster, 200× cheaper. The Foundry blog states a different comparison: competitive with GPT-5, 80–100× faster. Do not collapse those into one number. |
| Copilot response grading | Competitive with GPT-5.6 Luna, about 100× faster |
| Microsoft Discovery replanning | 46× more consistent than the LLM score, 3× the scoring speed, nearly 4× end-to-end replanning speed |
| Price | $0.042 / 1M input tokens. Output tokens are not billed. US and EU Datazone are both listed at that input rate. |
Deployment types in docs are DataZoneStandard (inference stays in the data zone, where available) and GlobalStandard. Preview capacity and region list will move — check the catalog page before you promise a latency SLO.
Also available through OpenRouter. This post stays on the Foundry resource endpoint and Entra ID.
Where it fits in a .NET app
Use it when the next step is a closed choice:
- Route a support ticket before a human or a bigger model ever sees it
- Pick which tool or model an agent should call
- Grade a draft against a rubric and accept, revise, or escalate
- Gate a tool call (“does this touch production data?”)
- Label feedback, scan code against a checklist, rank search hits
Do not use it when the product needs the words. Summaries, rewrites, explanations, and open-ended plans still belong on a generative model. The useful architecture is both: Decision-1 decides, the chat model writes.
Two constraints from the docs that should be in the design review, not the footnote:
- It does not explain itself. If a regulator or a user asks “why,” you need a different model or a human.
- Do not let it be the only decision-maker for credit, employment, housing, healthcare, or legal outcomes. Microsoft’s own guidance is decision support plus review. It can also reflect bias from the base model, and calibration is strongest on task types it has seen.
Low top-probability is your abstain signal. Add an explicit cannot tell option when a forced label would be worse than a queue.
Calling it from C#
There is no dedicated .NET SDK method in the quickstart. Azure.Identity plus HttpClient matches the documented contract: bearer token for https://cognitiveservices.azure.com/.default, JSON in, JSON out.
|
1 2 3 4 5 6 7 8 |
dotnet new console -n Decision1Demo cd Decision1Demo dotnet add package Azure.Identity dotnet add package Microsoft.Extensions.Configuration.UserSecrets dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables dotnet user-secrets init dotnet user-secrets set "AZURE_AI_ENDPOINT" "https://your-resource.services.ai.azure.com" dotnet user-secrets set "AZURE_AI_DEPLOYMENT" "microsoft-decision-1" |
Deploy Microsoft-Decision-1 from the catalog first (GlobalStandard, or DataZoneStandard when you care which data zone runs the inference). The secret value must be the deployment name shown on the deployment page.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using Azure.Core; using Azure.Identity; using Microsoft.Extensions.Configuration; var config = new ConfigurationBuilder() .AddUserSecrets<Program>() .AddEnvironmentVariables() .Build(); var endpoint = (config["AZURE_AI_ENDPOINT"] ?? throw new InvalidOperationException("Set AZURE_AI_ENDPOINT.")) .TrimEnd('/'); var deployment = config["AZURE_AI_DEPLOYMENT"] ?? "microsoft-decision-1"; var credential = new DefaultAzureCredential(); using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) }; var decision = await DecideAsync( "Checkout is down again for all EU customers. This is my third request since 09:00."); Console.WriteLine($"Team: {decision.Team.Choice} (p={decision.Team.Probability:P0})"); Console.WriteLine($"Severity: {decision.Severity.Score:0.00} level={decision.Severity.Level}"); Console.WriteLine($"Repeat: {decision.Repeat.Probability:P0}"); if (decision.Team.Probability < 0.70) { Console.WriteLine("Top label is weak — send this ticket to a person."); } async Task<TicketDecision> DecideAsync(string state) { var token = await credential.GetTokenAsync( new TokenRequestContext(["https://cognitiveservices.azure.com/.default"]), CancellationToken.None); using var request = new HttpRequestMessage( HttpMethod.Post, $"{endpoint}/providers/microsoft/v1/systemone"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); var json = JsonSerializer.Serialize(new { model = deployment, state, questions = new { team = new { type = "choice", instructions = "Which team should handle this request? Pick the primary problem.", criteria = new Dictionary<string, string> { ["billing"] = "Charges, invoices, refunds, or subscription payments", ["engineering"] = "Bugs, errors, outages, or integration failures", ["support"] = "How-to questions, passwords, and account access" } }, severity = new { type = "score", instructions = "How severe is this incident?", criteria = new[] { "Cosmetic", "Minor", "Major", "Critical" } }, repeat_contact = new { type = "noul", instructions = "Has the customer contacted support about this before?", criteria = new Dictionary<string, string> { ["true"] = "The state says this is a repeat or follow-up", ["false"] = "Nothing in the state indicates an earlier contact" } } } }); // StringContent sets a fixed Content-Length; JsonContent uses chunked encoding. request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); using var response = await http.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { throw new InvalidOperationException( $"Decision API returned {(int)response.StatusCode}: {body}"); } using var doc = JsonDocument.Parse(body); var answers = doc.RootElement.GetProperty("answers"); return new TicketDecision( ReadChoice(answers.GetProperty("team")), ReadScore(answers.GetProperty("severity")), ReadNoul(answers.GetProperty("repeat_contact"))); } static ChoiceAnswer ReadChoice(JsonElement answer) { var probs = answer.GetProperty("probabilities"); var choice = answer.GetProperty("choice").GetString() ?? throw new InvalidOperationException("Missing choice."); return new ChoiceAnswer(choice, probs.GetProperty(choice).GetDouble(), probs); } static ScoreAnswer ReadScore(JsonElement answer) => new(answer.GetProperty("score").GetDouble(), answer.TryGetProperty("level", out var level) ? level.GetString() : null); static NoulAnswer ReadNoul(JsonElement answer) => new(GetRequiredDouble(answer, "noul")); // Fails with the raw answer JSON so the real response shape is visible. static double GetRequiredDouble(JsonElement answer, string name) { if (answer.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.Number) { return value.GetDouble(); } throw new InvalidOperationException( $"Answer has no numeric '{name}'. Raw answer: {answer.GetRawText()}"); } record ChoiceAnswer(string Choice, double Probability, JsonElement Probabilities); record ScoreAnswer(double Score, string? Level); record NoulAnswer(double Probability); record TicketDecision(ChoiceAnswer Team, ScoreAnswer Severity, NoulAnswer Repeat); |
Output
|
1 2 3 |
Team: engineering (p=100 %) Severity: 2.52 level= Repeat: 100 % |
What I would measure before production
The three-row sample in Microsoft’s quickstart is a smoke test, not an eval. Before this sits on a hot path:
- Build a labeled set from your own tickets, including the ambiguous ones.
- Report accuracy and the cases where the top probability was high and the label was wrong. Calibration matters more than the leaderboard once you threshold at 0.8.
- Time it from the same region as the app. Client plus network time is what users feel. Sequential calls do not tell you p99 under load.
- Shuffle option order on a copy of the set. Microsoft reports zero flips on shuffle for their suite; confirm that on yours.
- Put the abstain path in the same PR as the happy path. A classifier with no “not sure” bucket will invent confidence.
Cost is the easy part of the spreadsheet. At $0.042 per million input tokens and no output charge, the bill is dominated by how much state you stuff into every call. Keep the state to the fields the question needs. Do not paste the whole thread if the question is “which team.”
A practical split
If you already have a Foundry chat deployment for drafts and summaries, leave it there. Put Microsoft-Decision-1 in front of it for the closed questions: route, severity, policy gate, “is this draft good enough to send.”
That is the design the model is actually for. Not a smarter chatbot. A fast, cheap, typed branch in the application you already shipped.
Wrapping up
Most of what a .NET app does in a loop is not writing. It is choosing: which team, which tool, continue or stop, accept or revise. Microsoft-Decision-1 is built for those choices, and it returns numbers your code can act on.
I would not replace a chat model with it. I would put it in front of one. Run it on a labeled sample from your own workload first, look at the confident mistakes, and only then wire the threshold into production.
Resources
- Microsoft-Decision-1 C# Demo
- Microsoft-Decision-1 in the Foundry model catalog
- Introducing Microsoft-Decision-1 — Microsoft Foundry Blog
- Introducing Microsoft-Decision-1, our model for fast decision-making — Command Line
- Deploy and use Microsoft-Decision-1 — Microsoft Learn
- Microsoft-Decision-1 on OpenRouter
- My GPT-6 Astra C# guide
Building AI features in C#? I write about practical, no-hype prompt engineering and Azure AI patterns for .NET developers. Check out Prompt Engineering for .NET Developers — free, no Python required. You can also subscribe for more posts.



Leave A Comment