Structured Extraction
StructuredAI.ExtractAsync reads typed values out of a text. A value is a span of the text, a number read or computed from it, a date, a list, or an estimate or forecast with its distribution. Every value comes back as the type the field asked for, or as null when the text does not determine it.
Where Structured Decisions chooses between options you name, an extraction returns a value. Use a decision for a label, a score or a yes/no; use an extraction for an amount, a due date, a reference number or a list of names.
There is no local extraction model. An extraction runs on a decision provider's extraction model. Today that is Cloudflare's Clairic (clairic, in preview), enabled on the Cloudflare Workers AI provider.
Extracting fields
using Mosaik.AI;
var options = StructuredAIOptions.Extraction(DecisionProviders.Cloudflare, userUID: CurrentUser);
var invoice = await StructuredAI.ExtractAsync(mail.Body, f => f
.Text("invoice_number")
.Date("due_date", "Issue date plus the net payment days")
.Number("total", "Amount due including shipping", unit: "USD", decimals: 2)
.List("contacts", ExtractionListItem.Text)
.Text("po_number", "Purchase order number")
.Estimate("days_to_pay", 0, 120, 1, "Days after issue until this invoice is paid"),
options, CancellationToken);
string number = invoice.TextOf("invoice_number"); // "INV-2291"
TextSpan where = invoice["invoice_number"].Span.Value; // character offsets into the text
double? total = invoice.NumberOf("total"); // 1080, computed from the line items
DateTime? due = invoice.DateOf("due_date"); // 2026-10-02
bool noPO = invoice["po_number"].IsNull; // true: the text has no PO number
var days = invoice["days_to_pay"].Estimate; // Median 30, P10 21, P90 38
The answers above are Clairic's for this text: "Invoice INV-2291 from Northwind Supply, issued 2026-09-02, net 30. 4 pallets at $250.00 each, shipping $80.00. Contact: Dana Ruiz, Priya Shah." It is the example on Cloudflare's model page.
The input is a string, or an object that is serialized to JSON. Text spans are offsets into that string. Give every field a descriptive name and instructions: the name, the type and the instructions are all the model is told.
ExtractManyAsync(inputs, fields, options) reads the same fields from many inputs, one request per input, up to MaxConcurrentRequests (4 by default) at a time. StructuredFields.Create(f => ...) builds a set of fields once, to reuse across calls.
Field types
| Builder | Answer |
|---|---|
.Text(name, instructions) |
A span copied from the input, with its Span (start and end offsets). |
.Number(name, instructions, unit, decimals, integer) |
A number read from the input or computed from it. decimals is 0 to 6. |
.Date(name, ...), .Time(name, ...), .DateTime(name, ...) |
An ISO date, time of day or date and time. Date, TimeOfDay read it. |
.Duration(name, instructions, unit) |
A length of time as a number of DurationUnit (seconds by default). Duration converts it to a TimeSpan, counting a month as 30 days and a year as 365. |
.List(name, items, instructions) |
Every matching text, number or date (ExtractionListItem) in input order, with a span per text item. An empty list when nothing matches. |
.Estimate(name, min, max, step, instructions, unit, scale) |
A value the input does not state but gives evidence for, as a distribution on the grid from min to max. EstimateScale.Log suits a value that spans orders of magnitude. |
.EstimateDate(name, min, max, step) |
The same, for a date, on a day, week, month or year grid. |
.Forecast(name, horizon, min, max, step, interval) |
One estimate per future step (horizon 1 to 64) of a series given in the input as text. |
A field name starts with a letter or _, followed by up to 63 letters, digits, _, . or -. The builder throws ArgumentException for an invalid or repeated name. It also throws for settings the API would refuse: bounds that do not increase, a step that is not positive, decimals on a list of texts.
Every scalar field also takes nullThreshold: the probability of "not in the input" above which the value comes back null. The provider's default is 0.5.
Reading the answer
StructuredExtraction holds one ExtractedValue per field, by name (invoice["total"]) or in the order the fields were added.
| Member | What it holds |
|---|---|
IsNull |
True when the input does not determine the value: a null scalar or estimate, an empty list, a forecast with no step. |
Text, Number, Date, TimeOfDay, Duration |
The value, read as the type that applies; the other readings are null. |
Guess |
What the model would have answered had it not judged the value absent. Equal to the value when there is one. |
PNull |
The probability that the input does not contain the value. |
Confidence |
How sure the model is of a single value or a list, from 0 to 1. |
Span, Spans |
Where a text value, or each text item of a list, sits in the input. TextSpan.Of(input) returns the text. |
Items |
The items of a list. |
Estimate |
An estimate's Median, P10 and P90 (the 80% interval), Mean, and PBelow / PAbove: the probability the value lies outside the bounds. |
Steps |
A forecast's estimates, the next step first. |
On the result itself, Model is the model that answered and InputTokens what the request was billed for. Truncated is true when the input was longer than the model's context and was cut to fit.
A typed answer is not a correct answer, and a confidence is measured, not guaranteed. Handle null in every field, and check a value that drives an irreversible action against another source or a person.
Options
| Option | What it does |
|---|---|
StructuredAIOptions.Extraction(provider, model, userUID) |
The provider, from the generated DecisionProviders helper. model names one of its enabled extraction models; omitted, the first one enabled answers. userUID attributes the usage to a user. |
ReferenceDate |
The date that anchors a date written without a year ("due 3 March") and relative ones ("next Friday"). Set it to the date the text was written. |
IncludeQuantiles |
Adds the 1st to 99th percentiles of every estimate (ExtractedEstimate.Quantiles). |
MaxConcurrentRequests |
How many requests ExtractManyAsync runs at a time. |
ReferenceDate and IncludeQuantiles are init-only, so set them on a constructed options object:
var options = new StructuredAIOptions(DecisionProviders.Cloudflare, "clairic")
{
UserUID = CurrentUser,
ReferenceDate = sentAt,
};
Limits of Clairic
Clairic is a preview. Cloudflare's announcement states these limits:
- One and two step arithmetic, such as a total or a day count, was reliable in Cloudflare's testing. Longer chains of dependent arithmetic, and sums over many table rows, are not. Extract the parts and do the arithmetic in code.
- An estimate needs evidence in the input. Its values always stay inside the field's bounds.
- A forecast follows the pattern of the series in the text. It does not know about events the series does not show.
Its context is 65,536 tokens. Fields that do not fit beside the input in one 8,192-token pass are split across parallel passes on Cloudflare's side. Each pass reads the whole input, and the answers are merged. Clairic is billed at $0.10 per million input tokens; output is not billed.
Images, which Clairic also reads, are not passed by ExtractAsync: its input is text or JSON.
Checking before calling
StructuredAI.IsExtractionAvailable(options) returns true when the provider is set up with an API key and an account ID. It also checks that an extraction model is enabled: the one named, if the options name one. Code that must degrade gracefully checks it first.
Errors
| Exception | When |
|---|---|
ArgumentException |
The options name no decision provider. There is no local extraction model. |
DecisionProviderException |
The provider is not set up, has no key or account ID, has no extraction model enabled, the model named is a decision model, or the vendor's API refused the request. StatusCode and ResponseBody hold the vendor's answer. |
StructuredAIPredictionException |
The answer could not be read, or a field is missing from it. RawOutput holds what the provider answered. |
Asking an extraction model a question with AskAsync throws DecisionProviderException the same way, naming the call to use instead.
Every request is recorded on Manage → Operate → LLM Usage, attributed to cloudflare.