Decisions API in Azure OpenAI: A C# Walkthrough
Decisions API in Azure OpenAI: A C# Walkthrough Recently I wrote about Microsoft-Decision-1, a model that scores a fixed set of options instead of writing text. That post used a Microsoft provider route. This week the
Decisions API in Azure OpenAI: A C# Walkthrough
Recently I wrote about Microsoft-Decision-1, a model that scores a fixed set of options instead of writing text. That post used a Microsoft provider route. This week the Azure OpenAI Decisions API went generally available in Microsoft Foundry, and it solves the same problem through the Azure OpenAI resource you may already have.
Every agent makes small judgments: which team owns this ticket, does the customer want a refund, how severe is this issue, should the workflow continue or stop. Those steps still get sent to a chat model, which means you ask for JSON, parse text that almost looks like JSON, and trust a confidence number the model never really returned.
The Decisions API is built for those judgments. It evaluates text or images and returns a typed answer: a probability, a choice from your list, or a score against your rubric.
Sources for the numbers and endpoint details below: the Foundry announcement (October 22, 2026) and the Decisions API how-to. The official examples are in Python, JavaScript, and REST. This post gives you the same REST call in C#.
What the API returns
The announcement describes three outcomes, one per question type:
- Probability: whether a condition is true, with a value from 0 to 1
- Choice: one value from a list you define
- Score: a position on an ordered rubric
You send one shared input and a questions array. Each question has a unique name, so you can match each answer back to its question. You get back an answers array in the same order, plus token usage.
The endpoint is:
POST https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/decisions
The model field takes your deployment name. The docs use gpt-6-luna, which is currently the only supported model. It works with either an API key in the api-key header or a Microsoft Entra ID bearer token.
Two things to remember from the docs:
- A refusal is a separate answer type. It is not a low probability, so handle it as its own case.
- Each request supports up to 400 questions. Questions in one request all see the same input.
How this differs from Microsoft-Decision-1
| Microsoft-Decision-1 (previous post) | Azure OpenAI Decisions API (this post) | |
|---|---|---|
| Endpoint | {resource}/providers/microsoft/v1/systemone |
{resource}.openai.azure.com/openai/v1/decisions |
| Model field | Your Decision-1 deployment | Your gpt-6-luna deployment |
| Question types |
noul, choice, score
|
predicate, choice, score
|
| Auth | Entra ID | API key or Entra ID |
| SDK examples | None in the quickstart, C# calls HttpClient
|
Python, JavaScript, REST |
Both return typed answers. The sample in this post uses the Azure OpenAI route, so the code and the JSON match the official docs.
Calling it from C
The official docs don't show a C# SDK for this endpoint, so the sample calls it with HttpClient. It uses the same pattern as the Decision-1 demo: Azure.Identity for Entra ID, with an API key as a shortcut when one is set.
The complete project is on GitHub at taswar/OpenAI-DecisionApi-CSharp-Sample. Clone it if you want to run the code without copying it from this post.
Create the project on .NET 10, then add the packages. The packages let the app read settings from user secrets, so the endpoint and key stay out of your shell and out of the code:
dotnet new console -n OpenAI-DecisionApi-CSharp-Sample
cd OpenAI-DecisionApi-CSharp-Sample
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 init adds a UserSecretsId to the project file. Keep it as it is.
Store your settings with user secrets from the project folder. Use the base URL only, with no path:
dotnet user-secrets set "AZURE_OPENAI_ENDPOINT" "https://YOUR-RESOURCE-NAME.openai.azure.com"
dotnet user-secrets set "AZURE_OPENAI_DEPLOYMENT" "gpt-6-luna"
dotnet user-secrets set "AZURE_OPENAI_API_KEY" "your-key-here"
The deployment name is optional, and the app defaults to gpt-6-luna. If you leave out AZURE_OPENAI_API_KEY, the app uses DefaultAzureCredential instead, so az login works.
To check what the app will read, run:
dotnet user-secrets list
The project file is OpenAI-DecisionApi-CSharp-Sample.csproj. The UserSecretsId value is the one dotnet user-secrets init writes in your copy:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>91102972-0612-452c-90b4-a2dfa5578168</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.12" />
</ItemGroup>
</Project>
Then Program.cs. The ticket and the three questions are the same ones the official Python example uses, so the output can be compared directly:
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
var config = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.AddEnvironmentVariables()
.Build();
var endpoint = (config["AZURE_OPENAI_ENDPOINT"]
?? throw new InvalidOperationException(
"Set AZURE_OPENAI_ENDPOINT with dotnet user-secrets, for example https://YOUR-RESOURCE-NAME.openai.azure.com"))
.TrimEnd('/');
var deployment = config["AZURE_OPENAI_DEPLOYMENT"] ?? "gpt-6-luna";
var apiKey = config["AZURE_OPENAI_API_KEY"];
var ticket = "I was charged twice for my order. Please return the extra charge.";
var body = new JsonObject
{
["model"] = deployment,
["input"] = ticket,
["questions"] = new JsonArray
{
new JsonObject
{
["type"] = "predicate",
["name"] = "refund_requested",
["instructions"] = "Does the customer ask for a refund?"
},
new JsonObject
{
["type"] = "choice",
["name"] = "department",
["instructions"] = "Which department should handle this complaint?",
["choices"] = new JsonArray
{
new JsonObject { ["value"] = "billing", ["description"] = "Payments and refunds." },
new JsonObject { ["value"] = "technical", ["description"] = "Product problems." },
new JsonObject { ["value"] = "shipping", ["description"] = "Delivery and tracking." },
new JsonObject { ["value"] = "other", ["description"] = "All other requests." }
}
},
new JsonObject
{
["type"] = "score",
["name"] = "severity",
["instructions"] = "How severe is this issue?",
["levels"] = new JsonArray
{
new JsonObject { ["label"] = "Cosmetic", ["description"] = "No lost functionality." },
new JsonObject { ["label"] = "Workaround available", ["description"] = "A task fails, but another way works." },
new JsonObject { ["label"] = "Fully blocked", ["description"] = "A task fails with no workaround." }
}
}
}
};
using var decision = await PostDecisionAsync(endpoint, apiKey, body);
foreach (var answer in decision.RootElement.GetProperty("answers").EnumerateArray())
{
PrintAnswer(answer);
}
static async Task<JsonDocument> PostDecisionAsync(string endpoint, string? apiKey, JsonObject body)
{
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };
using var request = new HttpRequestMessage(HttpMethod.Post, $"{endpoint}/openai/v1/decisions");
if (!string.IsNullOrWhiteSpace(apiKey))
{
request.Headers.Add("api-key", apiKey);
}
else
{
var credential = new DefaultAzureCredential();
var token = await credential.GetTokenAsync(
new TokenRequestContext(["https://cognitiveservices.azure.com/.default"]),
CancellationToken.None);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
}
// StringContent sets a fixed Content-Length; JsonContent uses chunked encoding.
request.Content = new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json");
using var response = await http.SendAsync(request);
var text = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(
$"Decisions API returned {(int)response.StatusCode}: {text}");
}
return JsonDocument.Parse(text);
}
static void PrintAnswer(JsonElement answer)
{
var type = answer.GetProperty("type").GetString();
var name = answer.TryGetProperty("name", out var nameValue) ? nameValue.GetString() : null;
switch (type)
{
case "predicate":
var probability = answer.GetProperty("probability").GetDouble();
Console.WriteLine($"{name}: {probability:P0} true");
break;
case "choice":
var choice = answer.GetProperty("choice").GetString();
var choiceConfidence = answer.GetProperty("confidence").GetDouble();
Console.WriteLine($"{name}: {choice} (confidence {choiceConfidence:P0})");
break;
case "score":
var score = answer.GetProperty("score").GetDouble();
var scoreConfidence = answer.GetProperty("confidence").GetDouble();
Console.WriteLine($"{name}: {score:0.00} (confidence {scoreConfidence:P0})");
break;
case "refusal":
// A refusal is not a low probability. Send this one to a person.
Console.WriteLine($"{name ?? "(unnamed)"}: refused, send to a person");
break;
default:
Console.WriteLine($"Unexpected answer: {answer.GetRawText()}");
break;
}
}
Run it:
dotnet run
The output should look like this. Your numbers will differ:
refund_requested: 100% true
department: billing (confidence 100%)
severity: 0.98 (confidence 13%)
What changed in the code
The first thing to notice is that all three questions go out in one request. The ticket is evaluated once, and the answers come back together. That matters when a workflow makes many small decisions.
The score answer is a weighted average of the level positions. A score of 0.98 sits close to "Workaround available" (position 1), but the confidence is only 13%. That low confidence is the signal to act on. Send this ticket to a person rather than trust the score. Use the score for thresholds and ordering, not as an exact label.
The refusal branch is there on purpose. The docs are clear that a refusal is different from a predicate answer with a low probability, so the code gives it its own case.
I kept the code as one file so it reads top to bottom. In a real service you would move the request into its own class and add a retry policy.
Where it fits
Use it when the next step is a closed choice:
- Route a support ticket before a person or a bigger model sees it
- Decide whether an agent should continue, retry, or hand off
- Check a draft against a rubric before it goes out
- Gate a tool call, such as one that touches production data
Keep a chat model for anything that needs words, such as summaries, rewrites, or explanations. The decision model decides, and the chat model writes.
Set your own thresholds with a labeled set of tickets from your application. The announcement and the docs both describe these probabilities as estimates, so do not treat 0.95 as a guarantee.
Final thoughts
The Decisions API is the same idea as Microsoft-Decision-1, but it sits on the Azure OpenAI resource with gpt-6-luna and the three question types from the docs. If your team already has an Azure OpenAI resource, this is the easier starting point.
Try it against your own tickets before you change any routing. If your team already has a classifier, put this next to it and compare the answers on the same data. 🙂
Resources
- Azure OpenAI Decisions API: Now generally available in Microsoft Foundry
- Use the Azure OpenAI Decisions API (Microsoft Learn)
- OpenAI-DecisionApi-CSharp-Sample on GitHub
- Microsoft-Decision-1 C# demo (previous post's repo)
- Microsoft-Decision-1 for C# Developers (previous post)
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. Also subscribe to my mailing list for the latest blogs, tips and tricks I share.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.