
The common approach: prompt + JSON schema
The usual setup: list the fields and a JSON schema in the prompt, then parse what comes back. It's quick to build, and plenty of teams ship it.
What's left to you: checking that each value has the right type, checking that a value actually appears in the source, and deciding what to do when something is missing.
Invented values, and how Clairic handles them
A generative model is built to produce a plausible answer. Clairic instead returns pNull alongside scalar values. When pNull is above the null threshold (0.5 by default), value is null, though guess may still hold something. You can change the threshold for the whole request with options.nullThreshold, or per field with nullThreshold.
In the complaint-email example from the official blog, the email has no tracking number, so tracking_number comes back null instead of an invented one.

Types and spans make it auditable
Clairic has 9 field types: text, number, date, time, datetime, duration, list, estimate and forecast. A number field can set decimals. A text answer can include a span, the start and end character positions of the value in state, so a reviewer can see it highlighted in the source.
Lists can include spans too. In the official complaint-email example, each damage item, "cracked screen" and "dented corner", comes with its location in the source text.

This table compares interfaces and output shape only, not speed, accuracy or price.
| Prompted LLM | Clairic | |
|---|---|---|
| What comes back | Generated text you parse into JSON | One typed value per field |
| Missing values | Depends on your prompt and your checks | null when pNull is above the threshold |
| Confidence signal | Design it yourself | Scalar fields can carry confidence and pNull |
| Pointing back to the source | Build it yourself | Text can carry a span, lists can carry spans |
| Types | Validate them yourself | 9 built-in types, with TypeScript types in the SDK |
| Billing | Depends on the service you use | Once per request, by input size, not per field |
| Generates text? | Yes | No |
Here's the complaint-email task as code:
tsimport {
createSystemOne, defineFields, textField, numberField, dateField, durationField, listField,
} from '@system-one-ai/core';
import { systemOneAdapter } from '@system-one-ai/adapter-system-one';
import { createFetchTransport } from '@system-one-ai/transport-fetch';
const apiKey = process.env.SYSTEM_ONE_API_KEY;
if (!apiKey) throw new Error('Set SYSTEM_ONE_API_KEY.');
const client = createSystemOne({
adapter: systemOneAdapter,
transport: createFetchTransport(),
apiKey,
timeoutMs: 30_000,
maxRetries: 0,
});
const email = `(Fictional sample for illustration.)
Hi Example Shop team,
Order ORD-10482 was delivered on 2026-09-30. I'm writing on 2026-10-11.
The item was $649 plus $52.10 tax. The screen is cracked and the case is dented.
Please help. - Alex Sample`;
const fields = defineFields({
order_number: textField({ instructions: 'The order number in the email.' }),
delivery_date: dateField({ instructions: 'The date the order was delivered.' }),
total_paid: numberField({ decimals: 2, instructions: 'Item price plus tax actually paid.' }),
days_since_delivery: durationField({ unit: 'days', instructions: 'Days from delivery to the date of the email.' }),
damage: listField({ items: 'text' }),
tracking_number: textField({ instructions: 'The shipment tracking number, if the email gives one.' }),
});
const result = await client.extract({ model: 'clairic', state: email, fields });
const { tracking_number, damage } = result.answers;
console.log({
trackingNumber: tracking_number.value, // null when the email has no tracking number
trackingPNull: tracking_number.pNull,
damage: damage.value,
damageSpans: damage.spans,
});
Install with npm install @system-one-ai/core@0.8.0 @system-one-ai/adapter-system-one@0.7.0 @system-one-ai/transport-fetch@0.8.0, set SYSTEM_ONE_API_KEY in .env, and run with node --env-file=.env. Every run creates a new billable request.
Illustrative output, using only the values from the official example:
json{
"total_paid": 701.10,
"days_since_delivery": 11,
"damage": ["cracked screen", "dented corner"],
"tracking_number": null
}
Over HTTP, the endpoint is POST https://system-one.dev/v1/systemone, and camelCase SDK names become snake_case (for example referenceDate → reference_date).
Computed values
Some values aren't written in the input. In the official example, the email never states the total paid; Clairic adds 649 and 52.10 in tax to get 701.10. The 11 days from delivery to the email are worked out from two dates too.
There are limits. The official blog says the preview isn't yet reliable at long chains of calculations or multi-row aggregation, and well-formed output isn't necessarily correct. Test on your own samples before production, and set thresholds on confidence and pNull.
You can see null handling in the invoice extraction demo (sign-in required): when the total due says "to be confirmed", the amount comes back null and the invoice number is still extracted.
Cost model
Clairic is billed once per request based on input size, and the cost isn't multiplied by the number of fields. Rates are on the Clairic model page and the pricing page.
New accounts get 100 free credits on sign-up.
When an LLM is still the right tool
When you need written text, such as a reply or a summary, use an LLM. Clairic doesn't generate text.
The official three-step claim example: Clairic extracts values from the email, Clef checks them against the policy and decides between refund and repair, and an LLM writes the reply. The decision step sees structured values rather than the raw email, so it's less easily swayed by the email's wording. More in extract, then decide; if your input is an image, see image input.

Related reading
- What is Clairic? Extraction models explained
- Clairic vs Jev and Clef: extract, then decide
- Screenshot to JSON for AI agents
- Clairic model page
Last verified: 2026-10-22