Docs·Get started
Symbolic programs
For every question, XERJ Reader-1 writes a small program over your document, and an exact executor runs it. How to read a program, the symbols, the statuses and the confidence.
XERJ Reader-1 (xerj-reader-1, preview) does not write the answer. Its program says where the values are in your document, how to read them, and what to compute. The executor returns a status, a value and the evidence.
Every example on this page is a real run. Nothing is edited except the layout.
Why a program #
- Every value is traced to your document. A program starts from spans: exact characters of your document, by position. The executor reads numbers, amounts and dates only from those spans. The evidence lists each span the value came from.
- The arithmetic is not guessed. The model decides what to compute (“the later date minus the earlier one”). The executor does the computing, exactly, with decimal numbers and real calendars.
- It can be checked. You can read the program in one line and see each step. A wrong answer usually shows: the wrong span, the wrong operation, or a wrong constant.
- It can be run again. The program is data. Run the same program on the same document and you get the same value. Edit a program and run it again in the playground, or with
POST /v1/execute. - It says when it cannot answer. When the document does not say, or the values do not fit together, you get a status such as
ABSENTorUNIT_MISMATCHinstead of a value.
The model writes a literal value itself only for a Yes or No (YN), which must point at the sentence that decides it, and for a constant from your question, such as the 12 in “times 12”. Every literal is visible in the program.
How to read a program #
Each answer has the program in two forms. symbolic.program is JSON. symbolic.program_compact is one line for people:
SUB(DATE(“30.11.2026”, "auto"), DATE(“02.11.2026”, "auto")) = 28Read it from the inside out:
- Curly quotes
“…”are spans.“30.11.2026”is the exact text of your document at two character positions. The model chose the positions; the text is copied from your document. - Straight quotes
"…"are arguments the model wrote, such as a locale ("auto") or a unit ("hours"). - Functions read their arguments.
DATE(“30.11.2026”, "auto")reads the span as a date: 2026-11-30. - The outer function computes.
SUB(a, b)is a minus b. A date minus a date is a number of days. - After
=comes the value the executor computed. After→comes a status other thanANSWER, with the reason.
The same program as JSON:
["SUB", [["SPAN", "d1", 1458, 1468], ["DATE", "auto"]],
[["SPAN", "d1", 163, 173], ["DATE", "auto"]]]["SPAN", "d1", 1458, 1468]is characters 1458 to 1468 of your document (d1), end exclusive. Offsets count Unicode code points, like every offset in the API.- A list of steps, such as
[["SPAN", …], ["DATE", "auto"]], is a pipeline: each step gets the previous result as its first argument. It is the same as["DATE", ["SPAN", …], "auto"]. "_"stands for the current element when a symbol works through several values.
Worked examples #
Most examples ask questions about one email thread. Here it is in full, so you can check every offset:
Subject: Re: Re: Physio tariff 2027 / October claims
----- Message 1 -----
From: Ruth Ackermann <[email protected]>
To: Partner practices
Date: 02.11.2026 17:40
Dear practice partners,
Tariff update for physiotherapy (code PT-7301):
- session rate CHF 96.00 replaced by CHF 104.50 as of 01.01.2027
- home visits: no longer reimbursed as a separate item from 01.01.2027 (incl. travel surcharge CHF 18.00)
- group sessions: unchanged
- required on every claim: referral copy, diagnosis code, session date, therapist GLN
Ruth Ackermann
Claims Officer, Helvetia Claims Services
----- Message 2 -----
From: Dr. Hanne Solberg <[email protected]>
To: Ruth Ackermann
Date: 06.11.2026 08:05
Ruth,
- is CHF 96.00 still valid for December sessions?
- could you send our Sept/Oct statement? September was 27 claims if I remember right.
- Marco Keller, our billing clerk, asks whether home visits done in December are still billable.
Dr. Hanne Solberg
Practice Manager, Gesundheitszentrum Limmat, Zurich
----- Message 3 -----
From: Ruth Ackermann
To: Dr. Hanne Solberg
Cc: Marco Keller
Date: 09.11.2026 14:22
Hi Dr. Solberg,
- yes, CHF 96.00 applies to all sessions up to 31.12.2026
- December home visits are still billable, incl. the CHF 18.00 surcharge
- September: 27 claims, all approved
- October: 38 claims received, 35 approved, 3 returned (missing referral copy)
- please resend the 3 October claims with referral copies by 30.11.2026
Best,
Ruth
----- Message 4 -----
From: Marco Keller
To: Ruth Ackermann
Date: 10.11.2026 09:11
Thanks Ruth, will resend the 3 claims this week.1. A value read from one span #
Question: “new session rate from january” (type money)
MONEY(“CHF 104.50”, "auto") = 104.50 CHF- Point at
CHF 104.50(characters 290 to 300). - Read it as money.
"auto"means: work out the number format from the document.
Value: {"amount": "104.50", "currency": "CHF"}. Confidence 0.5782. The answer is right, but the model was unsure of one pointer, and the confidence shows it.
2. A number computed from two spans #
Question: “By how many CHF does the session rate go up in 2027?” (type number)
SUB(MONEY(“CHF 104.50”, "auto"), MONEY(“CHF 96.00”, "auto")) = 8.50- Read
CHF 104.50as money: the new rate. - Read
CHF 96.00as money: the old rate. - Subtract. Both are CHF, so the executor allows it.
Value: 8.50. Evidence: the two spans. Confidence 0.7017.
3. A date difference #
Question: “Counting from Ruth's first tariff email, how many days did the practice get to resend the returned claims?” (type number)
SUB(DATE(“30.11.2026”, "auto"), DATE(“02.11.2026”, "auto")) = 28- Read
30.11.2026, the resend deadline in message 3, as a date: 2026-11-30. - Read
02.11.2026, the date of message 1, as a date: 2026-11-02. - Subtract. A date minus a date is a number of days.
Value: 28. Confidence 0.9554. The question never names either date. The program shows which two the model chose.
4. Yes or No, with the sentence that decides it #
Question: “are group sessions changing in 2027” (type yesno)
YN(false, “group sessions: unchanged”) = no- Point at the line that decides the question:
group sessions: unchanged. - Judge it:
false, so the answer is No.
Value: false. Confidence 0.9358. A YN without a pointer is not allowed, so every Yes or No comes with its sentence.
5. A list #
Question: “what has to be on every claim” (type list)
LIST(“referral copy”, “diagnosis code”, “session date”, “therapist GLN”) = [referral copy; diagnosis code; session date; therapist GLN]Four spans, one per item. Confidence 0.9939.
6. Not in the document #
Question: “What is the rate per group session under code PT-7301?” (type text)
DECLINE("ABSENT") → ABSENT (declined by the program)The email says group sessions are unchanged, but never gives their rate. The program declines. Status ABSENT, no value. Confidence 0.9918: here the confidence says how sure the model was of declining.
7. A wrong answer, and how the program shows it #
Question: “what's the session rate for december sessions?” (type money)
MONEY(“CHF 104.50”, "auto") = 104.50 CHFThis is wrong. The document says CHF 96.00 applies to all sessions up to 31.12.2026, so the December rate is CHF 96.00. The model pointed at the 2027 rate. Confidence 0.8268, so a confidence bar alone would not catch it.
The program makes the mistake easy to see: the evidence is CHF 104.50, on the line that ends as of 01.01.2027. A reviewer who looks at the highlighted span sees it is the wrong line. This is why every value comes with its spans: check them when the answer matters.
Statuses #
status says what the executor could do with the program. Only ANSWER has a value.
| Status | What it means | What to do |
|---|---|---|
ANSWER |
The program ran and gave a value of the field's type. | Use the value. Show the evidence where a person checks the result. |
ABSENT |
The document does not say. The program declined. | Do not fill the field. Ask another document, or a person. |
UNKNOWN |
A value could not be read or computed. Examples: a number in an unexpected format, a date without a year and nothing to take it from, a division by zero, or a check on the value failed. | Read reason. Send ref_date for dates without a year, or ask more precisely. |
AMBIGUOUS |
Two candidates where one was needed: two numbers in one span, two currencies, or a tie in ARGMAX. The executor never picks one. |
Ask more precisely, or send the part of the document that holds one value. |
CONFLICT |
Different documents give different values. | Decide which source wins, or ask a person. |
INCOMPLETE |
Some elements of a list or table had no value, so a sum or count would be wrong. coverage says which. |
Read coverage. Fix or skip the missing rows yourself. |
UNIT_MISMATCH |
Values in different units or currencies were combined, such as EUR plus CHF. | Ask for one currency at a time. |
UNSUPPORTED |
The program returned the wrong kind of value for the field (a date for a number field), or could not run. |
Try another type, or rephrase the question. |
DECLINE |
Platform status, not from the executor: the program gave an ANSWER, but its confidence was not above your min_confidence. The value is in best_guess. |
Treat it as no answer. Show the best guess for review only. |
Real examples of the statuses other than ANSWER:
AGG("SUM", LIST(MONEY(“EUR 420”, "auto"), MONEY(“CHF 1,150”, "auto"), MONEY(“EUR 96”, "auto"))) → UNIT_MISMATCH (SUM over ['CHF', 'EUR'])
MONEY(“INV-20931 for 48,200 EUR”, "auto") → AMBIGUOUS (2 numbers in 'INV-20931 for 48,200 ')
MONEY(“48,200.00 EUR”, "nl-NL") → UNKNOWN ('48,200.00 ' is not a nl-NL number)
SUB(DATE(“1 Dec 2025”, "auto"), DUR(“9 yrs”)) → UNSUPPORTED (program returns Date, field is number)- The first question asked for the items billed in EUR. The model also took a CHF line. The executor refused to add EUR and CHF.
- The second span holds the invoice number and the amount. The executor does not guess which number is meant.
- The third read a dot-decimal amount with a Dutch number format. The executor returns no number rather than a wrong one.
- The fourth subtracted a duration from a date, which gives a date, but the question asked for a number of days.
Confidence #
The confidence comes from the model, not from the executor. It is computed from the program the model wrote:
confidence = (lowest pointer probability) × (mean token probability)- Pointers are the start and end positions of every span. For each one, the model gives a probability. The lowest one counts, so one doubtful position lowers the whole program.
- Tokens are the other parts of the program: the symbols, such as
SUBandDATE, and the literals, such as"auto". The mean of their probabilities counts. - A program without spans, such as
DECLINE("ABSENT"), uses 1 for the pointer part.
symbolic.confidence has the parts: ptr_min, ptr_mean, tok_min, tok_mean, the counts n_ptr and n_tok, and conf, the product. The answer's confidence is conf, rounded to 4 decimals.
What it does and does not tell you:
- It says how sure the model was of the program. It does not say whether the program could run: a program can have confidence 0.97 and still end
UNKNOWN. - A high confidence can still be wrong, as in example 7. Check the evidence when the answer matters.
- Set
min_confidenceto turn low-confidence answers intoDECLINE. Choose the bar on your own documents: see abstention and min_confidence.
Field types #
The program must return a value of the field's type. Send type with the question, or leave it out and the type is inferred from the wording (symbolic.type_inferred is then true).
type |
Value |
|---|---|
text |
A string, copied from the document. |
number |
A decimal string, such as "8.50". Also takes an amount of money or a duration and returns its number. |
money |
{"amount": "104.50", "currency": "CHF"}, with an ISO 4217 currency code. |
date |
"2026-11-30". |
time |
"07:40", or "07:58:12" with seconds. |
duration |
{"amount": "75", "unit": "minutes"}. |
pct |
A fraction: 19% is "0.19". |
yesno, bool |
true or false. |
list |
A list of values. |
choice |
One of the options you send. |
id |
{"kind": "iban", "value": "DE89370400440532013000", "checksum": true}, for IBAN, LEI, card, ISIN and GTIN, with the checksum verdict. |
A date without a year, such as 30 November, takes the year of the nearest full date before it in the document. If there is none that the executor can read, it takes the year of ref_date, or of today's date when you send none. Send ref_date (YYYY-MM-DD) when your documents have dates without a year: an example.
Symbol reference #
These are the symbols the reader writes today. Each entry gives the arguments, what the symbol does, and a short example with the executor's real output.
Address: where the values are #
SPAN(doc, start, end) → text. The characters start to end (end exclusive) of a document. Every value starts here.
“INV-2026-0091” = INV-2026-0091
Decode: how to read a span #
NUM(text, locale) → number. Reads exactly one number. "auto" works out the format from the document. Signs, brackets for negatives and number words count. Two numbers in the span is AMBIGUOUS; a wrong format is UNKNOWN, never a partial read.
NUM(“1'200 kg”, "auto") = 1200 and NUM(“(1,200)”, "auto") = -1200
MONEY(text, locale, currency) → money. Amount and currency. The currency comes from an ISO code or a symbol in the text, or from the third argument when the text has none.
MONEY(“EUR 3,324.86”, "auto") = 3324.86 EUR
PCT(text, locale) → percent. The value is a fraction.
PCT(“19%”, "auto") = 0.19
DATE(text, format) → date. Many formats and languages. A date without a year takes it from the document or ref_date.
DATE(“3 November 2026”, "auto") = 2026-11-03 and DATE(“Friday 20 November”, "auto") = 2026-11-20
TIME(text) → time. 10:55, 6.30pm, 18h00, noon. A bare number is not a time.
TIME(“07:40”) = 07:40
DUR(text, unit) → duration. Seconds to years, in several languages; parts are added (1 h 15 min is 75 minutes). No unit in the text and none given is UNKNOWN.
DUR(“1 h 15 min”) = 75 minutes and DUR(“Net 30”) = 30
Compute #
SUB(a, b) → a minus b. A date minus a date is days. A time minus a time is minutes. Add "business_days" to count Monday to Friday only.
SUB(TIME(“08:15”), TIME(“07:40”)) = 35
MUL(a, n) → same kind. A number, amount or duration times a number. Money times money is UNSUPPORTED.
MUL(DUR(“1 h 15 min”), LIT("Num", "4")) = 300
DIV(a, b) → a divided by b. Money divided by a number is money; money divided by money in one currency is a number. Division by zero is UNKNOWN.
DIV(MONEY(“EUR 384.00”, "auto"), NUM(“120”, "auto")) = 3.20 EUR
ROUND(x, places, mode) → same kind. Default half up. UP rounds away from zero, DOWN toward zero.
ROUND(DIV(LIT("Num", "800"), NUM(“250”, "auto")), 0, "UP") = 4
CONVERT(x, unit) → same kind. Time units, and mass, length, volume, area, energy and data units. Money needs the rate as the third argument; there is no built-in exchange rate.
CONVERT(MUL(DUR(“1 h 15 min”), LIT("Num", "4")), "hours") = 5 and CONVERT(NUM(“1'200 kg”, "auto"), "t") = 1.2
Several values #
LIST(a, b, …) → set. A set from separate places in the document.
LIST(“CHF 12”, “CHF 7”) = [CHF 12; CHF 7]
PAIR(label, value) → pair. A label with its value, so ARGMAX can return the label.
ARGMIN(LIST(PAIR(“standard”, MONEY(“CHF 12”, "auto")), PAIR(“reduced”, MONEY(“CHF 7”, "auto")))) = reduced
ARGMAX(set, value, result) → element. The element with the largest value, or what result reads from it. A tie is AMBIGUOUS.
ARGMAX(LIST(PAIR(“extract_orders”, DUR(“15 min”)), PAIR(“transform_orders”, DUR(“37 min”)))) = transform_orders
Answers #
YN(true or false, span, …) → yes/no. A Yes or No judged from the sentences it points at. At least one span is required.
YN(false, “Parking on site is no longer free for visitors”) = no and YN(true, “IBAN: DE89 3704 0044 0532 0130 00”) = yes
DECLINE(status, span, …) → no value. Declines with ABSENT, UNKNOWN, UNSUPPORTED, AMBIGUOUS or CONFLICT. Optional spans show where the model looked.
DECLINE("ABSENT") → ABSENT and DECLINE("ABSENT", “Parking on site is no longer free for visitors”) → ABSENT
The symbolic object #
Every answer carries a symbolic object: the full record of the run. The main keys:
| Key | What it holds |
|---|---|
program |
The program as JSON. |
program_compact |
The one-line form, with the value or the status. |
spans |
Every span in the program: {doc, start, end, quote}. |
status, value, value_text, reason |
The result. value_text is the value as one line of text; reason explains a status other than ANSWER. |
evidence |
The spans the value came from: {doc_id, char_start, char_end, quote, exact, …}. |
checks |
The result of each check in the field definition, if any. |
coverage, withheld_value, candidates |
Details for INCOMPLETE, for a value withheld by a failed check, and for AMBIGUOUS / CONFLICT. |
field, type, type_inferred, typecheck |
The field as it was asked, and the type the program returns. |
confidence |
The parts of the confidence, above. |
n_prompt_tokens, n_gen_tokens, finished, ms |
Size and time of the run. |
The Answers reference shows a complete one.