Each customer's rules become another branch in the code.
When customers need different required fields and categories, the differences end up as conditions on the customer spread through the intake code. With IntentSharp each customer's rules live in its own intent file, chosen by name, and one call returns the same .NET type for every customer, checked against that customer's rules.
What it costs today
Software that serves many customers collects exceptions. One customer quotes a purchase order on every request, another charges everything to a cost center, a third sorts its work into categories nobody else uses. Each difference becomes a condition on the customer, in the form handling, the validation and the reporting, and every new customer adds another round of them.
The branches are hard to see in one place and easy to break when another customer changes. Every change waits for a release, even when a customer only asked for one field to become required.
How IntentSharp handles it
Northwind, Contoso and Fabrikam each have a file with their own required fields and categories. The one call, IntakeAsync, picks the file by the customer's name and returns an IntakeRequest. Contoso's file requires a cost center: the first answer leaves it out, the rule fails, and the second attempt carries the finding back.
name: Intake.Northwindversion: 3owner: northwind-accountintent: >- Fill in the intake form from this request from Northwind. It quotes its purchase order, like PO-4471.shape: clrType: IntakeRequestrules: required: - purchaseOrder properties: category: enum: - hardware - software - cloud purchaseOrder: pattern: '^PO-\d{4}$'name: Intake.Contosoversion: 1owner: contoso-accountintent: >- Fill in the intake form from this request from Contoso. It charges each request to a cost center, as CC-1234, and gives a due date.shape: clrType: IntakeRequestrules: required: - costCenter - neededBy properties: category: enum: - hardware - software costCenter: pattern: '^CC-\d{4}$'name: Intake.Fabrikamversion: 2owner: fabrikam-accountintent: >- Fill in the intake form from this request from Fabrikam. It sends field service work: a repair, an install or a training visit.shape: clrType: IntakeRequestrules: properties: category: enum: - repair - install - trainingusing var catalog = new FileIntentCatalog(intentsFolder); // IntakeRequest is the one result type the files may name; a file// cannot load a type of its own.var types = new IntentTypeRegistry().Add<IntakeRequest>();var intents = new IntentRuntime(client, catalog, types: types); // One intake call for every customer. Each customer's file holds// its required fields and its categories.Task<AIResult<IntakeRequest>> IntakeAsync(string customer, string text){ var intent = $"Intake.{customer}"; return text.RunIntentAsync<IntakeRequest>(intents, intent);}Northwind: Intake.Northwind version 3 Success: true, after 1 attempt Value: category hardware, purchaseOrder PO-4471Contoso: Intake.Contoso version 1 [Validator] attempt 1: value.costCenter is null; it is required. Success: true, after 2 attempts Value: category software, costCenter CC-2210, neededBy 2026-11-30Fabrikam: Intake.Fabrikam version 2 Success: true, after 1 attempt Value: category repairIntakeRequest is one record: Category, Summary, PurchaseOrder, CostCenter and NeededBy, the last three nullable; the output shows each value's category and the members that are not null. Each file is Intake.<customer>.intent.yaml. Compiled and run with every build of this site; the model's answers are scripted, and a pass means the answer met the checks, not that it is right.
What stays in your control
The files hold what differs between customers. The type, the call and the checks that need code stay in your code.
- One result type.
IntakeRequestis registered in code withIntentTypeRegistry. A file can name only a type your application registered, and nothing in a file can load code. A new type, or a new member on it, needs a release. - Each customer's rules.
required,enumandpatternare checked in your process on every answer, with no model call. A broken rule becomes a finding with its place in the result, such asvalue.costCenter, and the next attempt sends it to the model, within the attempts you allow. - A customer without a file. An unknown intent name throws
IntentNotFoundExceptionbefore any model call, and a file withenabled: falsethrowsIntentDisabledException. - What a failure looks like. When the attempts run out, the result has
Successfalse and every finding inIssues, and your code decides what happens to the request. - Checks that need code. A rule a file cannot express, such as a purchase order that must exist in your system, is a C# check your application registers by name and a file switches on under
validators. - A new customer. A new file, which the watched folder loads while the application runs. The call builds the intent's name from the customer, so it does not change.
- A pass is not a proof. A pass means the answer met that customer's rules, not that it read the request right: a category from the file's list can still be the wrong one.
Read next
Running intents from files
Register the checks and the .NET types a file may name, and what throws before any model call.
Intent file reference
The thirteen rules, what each one checks, and how a failure names its place.
Intent files
Why a file cannot run code, and what it may name.
More problems and their answers: Solutions.
Give one customer its own file.
dotnet add package IntentSharp --prereleasedotnet add package IntentSharp.Catalog --prerelease