Luma public docs
Türkçe

Language

Type System

The type of a schema field does three jobs at once: it produces the JSON Schema sent to the gateway, it validates the response locally, and it becomes a TypeScript declaration through --generate-tool-types. This page lists every type and its JSON Schema counterpart.

Primitive types

TypeAliasAccepted valueJSON Schema
string—JS string{ "type": "string" }
intintegerA value that passes Number.isInteger{ "type": "integer" }
floatnumberA finite number — NaN and Infinity are rejected{ "type": "number" }
boolbooleantrue / false{ "type": "boolean" }
null—Only null{ "type": "null" }
any—Anything — no validation is performed{}
The int/float distinction is real

If 3.5 arrives in an int field you get X.y must be an integer.; 3.0 is valid as far as Number.isInteger is concerned. If NaN or Infinity arrives in a float field you get X.y must be a number. — which catches malformed model output early.

Composite types

enum

luma
status: enum["pending", "approved", "rejected"]

Accepts string values only and cannot be empty. The JSON Schema counterpart is { "type": "string", "enum": [...] }. A value outside the list produces X.y must be one of these values: ….

Array

luma
tags: string[]        // unbounded list of strings
items: LineItem[]      // list of nested schemas
grid: int[][]         // nested array

JSON Schema: { "type": "array", "items": { … } }. Each element is validated separately and the index appears in the error message: Invoice.items[2].amount missing.

An element is reached with [] — rows[0], rows[i].customer.name — and the same form on the left of an = writes one. Reading past the end is a named error, never a quiet null: xs[5] is out of bounds (length 2). Writing past the end is refused too, because a list does not grow by being indexed. The full table of boundary cases is on the language page.

map<T> / record<T>

luma
metrics: map<float>       // { "score": 0.95, "confidence": 0.8 }
labels: record<string>    // record is an alias for map

An object with dynamic keys. JSON Schema: { "type": "object", "additionalProperties": { … } }. Keys are free; the type of the values is validated.

A value is reached with m["key"], and writing to a key that is not there creates it. Reading one that is not there does not return null — a missing key is a named error: m["missing"] is not present. That is a statement about the type and not merely about the runtime: map<T> holds T, not T | null, so a lookup is not a place where null can enter the program. A key you are not sure about has to be checked before it is read:

import col from "std/collections"

agent Reader {
  fn read(labels: map<string>, key: string) -> Out {
    if contains(col.keys(labels), key) {
      return { value: labels[key] }
    }
    return { value: "unknown" }
  }
}

The full table of indexing boundary cases — lists as well as maps, reads as well as writes — is on the language page.

map<T> is risky with strict gateways

Strict structured-output mode (strict: true) makes most gateways expect every object to list all of its keys under properties and to set additionalProperties: false. A map<T> with dynamic keys cannot satisfy that, and the provider may reject the request.

If you need to carry dynamic metadata as LLM output, use a list of key/value pairs — that pattern is safe in strict mode:

schema MetaEntry {
  key: string
  value: string
}

// inside InvoiceExtract:
meta: MetaEntry[] @desc("Anything that does not fit the schema fields")

You can use map<T> freely in deterministic code (tool output, a value produced by return); the restriction only applies to schemas sent to the model.

json<T>

luma
import json from "std/json"

schema Envelope {
  raw: string           // JSON text
  data: any              // parsed, unvalidated value
  payload: json<Payload>    // parsed AND validated against Payload
}
json<T> is not a string

The json<T> type validates the value against T. If you want to hold JSON text, the type must be string. In JSON Schema generation json<T> unfolds directly into T, so as far as the gateway is concerned it is identical to T.

Tuple

luma
coordinate: [float, float]      // exactly two elements, positional

A fixed-length, positionally typed array. Validation checks the element count and the type at each position. It must contain at least one comma ([float] is not a tuple).

Element annotations do not apply to tuples

A tuple is emitted as prefixItems in JSON Schema and works fine as ask output. Numeric and string annotations cannot target tuple positions, though; the parser rejects them:

Error: @range can only be used on numeric fields or numeric arrays.

When you need a constraint per position, named fields are both more readable and more expressive:

schema Point {
  lat: float [-90..90]
  lon: float [-180..180]
}

Schema reference

luma
schema Address {
  city: string
}

schema Customer {
  address: Address      // nested object
  history: Address[]    // list of nested objects
}

Emitted as { "$ref": "#/$defs/Address" } in JSON Schema; the definition appears once under $defs.

No description is written onto a $ref field

If a field's type is a direct schema reference, the @desc on that field is not carried into JSON Schema. The reason is that some gateways reject a request when they see description at the same level as $ref. Put the description on the target schema itself (schema Address @desc("…")) or on the fields inside it.

Parametric schema — Name<T>

luma
schema LineItem {
  sku: string
  amount: float
}

schema Page<T> {
  items: T[]
  total: int
}

schema Report {
  page: Page<LineItem>
}

A schema may declare type parameters. Inside the body the parameter reads like any other type name, and every use site supplies a concrete argument: Page<LineItem>, Page<Customer>.

Page<LineItem> is expanded while the file is parsed. The parser copies the body of Page, puts LineItem where T stood, and registers the result as an ordinary schema named Page_LineItem. Nothing past that point knows a parameter was ever involved — JSON Schema generation, validation, $defs, where, when, @desc and std/schema reflection all run on the expanded schema exactly as they would on one typed out by hand, and no genericity reaches the provider.

The expanded name is the name you see. It is the $defs key, it is what schema.name(…) returns, and it is the prefix of a validation error raised against that schema:

Page_LineItem.items[0].amount missing.

Every position that accepts a schema name

PositionWritten as
Method return typefn list() -> Page<LineItem>
Schema fieldpage: Page<LineItem>
Agent method parameterfn take(page: Page<LineItem>) -> Report
pure fn / io fn parameterpure fn count(page: Page<LineItem>) -> int
Tool parametertool inspect(page: Page<LineItem>) -> Box<int>
Typed local variablelocal: Page<LineItem> = page

The carrying case is the one that matters: a producing method returns Page<LineItem> and a consuming method takes the same thing as a parameter, so the wrapper never has to be unpacked by hand at the boundary between them.

Several parameters, nesting, primitives

luma
schema LineItem {
  sku: string
}

schema Customer {
  name: string
}

schema Page<T> {
  items: T[]
}

schema Pair<A, B> {
  left: A
  right: B
}

schema Box<T> {
  value: T
}

schema Envelope<T> {
  payload: T
  trace_id: string
}

schema Deep<T> {
  inner: Box<T>
}

schema Catalogue {
  pair: Pair<LineItem, Customer>
  pages: Page<Envelope<LineItem>>
  count: Box<int>
  deep: Deep<LineItem>
}

A type argument may be a primitive (Box<int>), a schema, or another instantiation (Page<Envelope<LineItem>>), and a generic body may reference a second generic — Deep<T> above holds a Box<T>. Each distinct instantiation is expanded once, under a name assembled from its arguments: Pair_LineItem_Customer, Page_Envelope_LineItem, Box_int, Deep_LineItem. Give a generic the wrong number of arguments and the count is named back at you: Generic schema 'Pair' expects 2 type argument(s), got 1.

What a parameter may stand for, and what may be passed to it

Both sides of the substitution are wider than the examples above suggest. A parameter is not restricted to a bare field: it substitutes into every composite position, so one declaration covers the array, the map, the nullable, the tuple and the raw-JSON form of the same type.

luma
schema LineItem {
  sku: string
}

schema Multi<T> {
  one: T
  many: T[]
  lookup: map<T>
  maybe: T | null
  pair: [T, string]
  raw: json<T>
}

schema Catalogue {
  rows: Multi<LineItem>
}

And the argument does not have to be a schema name. Anything the type grammar can write may be passed, each one expanding under a name built from the argument's own written form:

Written asExpansion
Box<string[]>Box_string_array
Box<map<int>>Box_map_int
Box<string | null>Box_union_string_or_null
Box<enum["red", "green"]>Box_enum_red_green
Box<json<any>>Box_json_any
Box<Box<Box<LineItem>>>Box_Box_Box_LineItem

Type parameters are a schema construct only. Writing them on any other block does not give you a generic agent or a generic function.

Across a module boundary, since v0.9.0

Composition and generics used to be resolved before imports were merged, so both stopped at the file they were written in. A multi-module package could not share a base schema — schema Admin = User + Audit failed with Unknown schema composition part: User when User came from another file, and an imported Page<T> could not be instantiated in a field type. Resolution is now deferred until after the merge, so all three forms cross the boundary:

import "./base.luma"      // Taban, Ek, Zarf<T>

schema Birlesik = Taban + Ek        // composition
schema Kullanim = Zarf<Ic>          // generic alias
schema Sonuc { kutu: Zarf<Ic> }     // generic field type

The alias form is not just newly cross-module — it did not exist in the language at all. schema K = Zarf<Ic> was silently swallowed: the line parsed as nothing, K was never registered, and the failure only surfaced later as No schema named 'K' was found. A schema X = … line that matches no known form is now refused by name:

Error: Schema definition 'schema K = …' matches no known form: use 'A + B' composition, 'A | B tagged by field' union, or a 'Template<Args>' generic alias.
Four measured limits of the deferral

Deferral covers field types and aliases. It does not cover everything, and the edges were measured rather than assumed:

  • A generic in a parameter position — an agent method or pure fn parameter — is still resolved eagerly, so an imported template cannot be used there.
  • A pending generic alias cannot be a composition part (Unknown schema composition part). If you need that chain, instantiate the alias in its own module.
  • A template reached through a namespaced import (mod.Zarf<Ic>) is out of scope — namespaced schema naming is a separate surface and was not measured.
  • Two modules exporting a template under the same name are refused with Import generic schema conflict. A diamond — the same module reached twice — is a no-op, as it is for schemas.
One expansion per instantiation

Expansions are cached per instantiation, not per use site. Writing Page<LineItem> as a parameter and as the return type of the same method still produces a single Page_LineItem — the second use resolves to the schema the first one registered. That is what keeps $defs from growing with every mention of a wrapper.

Refused while parsing

Each of these stops the program before it runs:

Error: Unknown type argument schema 'Missing' for generic 'Page'.
Error: Generic schema cycle found: Tree<int> -> Tree<int>.
Error: Tagged unions cannot use generic schema members: Document.
Error: Tagged union 'Document' must contain at least two members.
  • A type argument that names nothing — Page<Missing>. Arguments are resolved, not taken on trust.
  • A generic that instantiates itself — a Tree<T> holding children: Tree<T>[]. Expansion has no stopping point there, so the cycle is reported with the chain that produced it rather than run forever.
  • A generic used as a tagged union member — schema Document = Page<Invoice> | Page<Receipt> tagged by kind. The tag carries a member's schema name, and which name an expansion should contribute was left undecided instead of guessed. The reverse direction is fine: a tagged union is a perfectly good type argument, and Envelope<Document> expands like any other.
  • And the rule underneath that one: a tagged union needs at least two members, so schema Document = Invoice tagged by kind is refused as well. A sum type with a single branch is a rename.
A generic name is a type, not a value

Reflection takes a value, and Page on its own is not one, so schema.toJsonSchema(Page<LineItem>) never runs:

Error: 'Page' variable is not defined yet.

Reach the expansion instead. Reflect over a schema that holds the instantiation on a field, or name the expanded schema directly — schema.toJsonSchema(Page_LineItem) — which exists as soon as some position in the program has instantiated it. With no instantiation anywhere, the same message comes back with the expanded name in it.

Union and nullable

luma
note: string | null
value: int | float | null
flexible: string | int

A union is written with | and produces anyOf in JSON Schema. During validation the members are tried in order; if none match, every sub-error is combined into one report:

Customer.tax_id does not match any union type: Customer.tax_id must be a string. | Customer.tax_id must be null.

Tagged union — tagged by

A | written inside a field type is an untagged union. A schema declared with = and tagged by is a different thing: a sum type. A value is one member or the other, and a tag field says which.

luma
schema Invoice {
  date: string
  total: float
  invoice_number?: string
}

schema Receipt {
  date: string
  total: float
  reference?: string
}

schema Document = Invoice | Receipt tagged by kind

The declaration is a single line with no body, and it needs at least two members that are already-defined schemas — an unknown member stops the parser with Tagged union 'Document' member schema 'Voucher' was not found. From there the two forms are resolved by entirely different rules:

Written asHow a value is matched
A | B — inside a field typeMembers are tried in order; the first one that validates wins.
schema D = A | B tagged by kOnly the k tag is read, and it names exactly one member. Nothing is tried.
Where members overlap, try-each reports a wrong answer as a correct one

Real schemas overlap. Invoice and Receipt above both carry date and total, and what separates them is optional — so a value holding only a date and a total validates cleanly against both. In an untagged doc: Invoice | Receipt field the first member wins, validation stops there and the run reports success. The value became an invoice because of the order the members were typed in.

Writing the same field as doc: Receipt | Invoice turns the very same value into a receipt. Nothing else in the program changes and no warning is produced. The tag removes the guess: under tagged by kind, a value carrying "kind": "Receipt" is a Receipt however well Invoice would have fitted.

The tag is injected, not declared

tagged by kind adds the kind field to every member, and its value is that member's schema name: "Invoice", "Receipt". You never write the field yourself, and the members are not altered — Invoice on its own, as a field type, a parameter or an ask output, still has exactly the fields it declares and no kind among them.

Because the tag is injected, a member that declares it too is a conflict, and the parser refuses the whole program:

Error: Tagged union 'Document' discriminator 'kind' conflicts with member schema 'Invoice'.

Reading the tag

The tag is read before anything else. It selects one member and the rest of the value is validated against that member alone, so the branch is deterministic even when every member would have fitted:

luma
agent Router {
  fn route(document: Document) -> Result {
    match document {
      case Invoice invoice { return { branch: "Invoice" } }
      case Receipt receipt { return { branch: "Receipt" } }
    }
  }
}

router = Router()
out = router.route({ kind: "Receipt", date: "2026-02-01", total: 42.0 })

A value whose tag is missing — or is not a string — never reaches a member, and neither does one whose tag names a schema outside the union:

Router.route.document.kind must identify a tagged union member.
Router.route.document.kind has unknown tagged union member 'Voucher'.

The prefix is the path of the value being validated; here, the document parameter of Router.route.

Optional / nullable matrix

This table is the distinction most often confused in Luma:

Written asField may be absent?Value may be null?In JSON Schema required
x: string✗ no✗ no✓ yes
x?: string✓ yes✗ no✗ no
x: string | null✗ no✓ yes✓ yes
x?: string | null✓ yes✓ yes✗ no
x: string @default("a")✓ (filled in when absent)✗ no✗ no
Which one for LLM output?

When extracting data from a model, T | null is usually better than ?: the field is always present, the model gets an explicit way to say "I did not find it", and because it stays in the required list under strict schema mode the model cannot skip it. ?, by contrast, lets the model quietly swallow the field.

// The preferred pattern in invoice extraction:
invoice_number: string | null @desc("Invoice number")
due_date: string | null @desc("Payment due date")
items: InvoiceItem[] @desc("All line items; empty list if none")

Arrays and null

Written asMeaning
items: Item[]Required array. An empty list [] is valid.
items: Item[] @minItems(1)At least one element required.
items: Item[] | nullnull or an array; the array may be empty.
items?: Item[]The field may be absent entirely.

Collection annotations such as @minItems and @unique applied to a nullable array only run when the value really is an array; they are skipped when it is null.

Size and range shorthands

Where a single space changes the meaning
Written asMeaning
float[0..1]An array of 0–1 floats (bracket attached to the type)
float [0..1]A single float between 0 and 1 (space between type and bracket)

These two behave differently in silence, and someone who writes confidence: float[0..1] has typically produced a schema that expects the model to return [0.82] instead of 0.82.

ShorthandMeaningEquivalent to
string[3]An array of exactly 3 stringsstring[] @minItems(3) @maxItems(3)
LineItem[1..100]Between 1 and 100 elementsLineItem[] @minItems(1) @maxItems(100)
float [0..1]0 ≤ value ≤ 1float @range(0, 1)
int [1..99]1 ≤ value ≤ 99int @range(1, 99)
float[1..3] [0..100]An array of 1–3 elements; each element between 0 and 100—
luma
schema Answer {
  title: string
  paragraphs: string[2]           // exactly two paragraphs
  people: Person[1..3]        // 1-3 people
  scores: float[1..3] [0..100]
  confidence: float [0..1]        // a single number between 0 and 1
}
The range shorthand cannot be combined with @range

Writing score: float [0..100] @range(0, 50) makes the parser fail with Field range cannot be specified with both legacy syntax and @range. Pick one.

JSON Schema mapping

The schema below and the real JSON Schema it produces, taken from schema.toJsonSchema(...):

luma
schema Address {
  city: string
}

schema Customer @desc("Invoice customer") {
  name: string @length(2, 40) @title("Full name") @example("Ada Byron")
  tax_id: string | null @length(10) @desc("Tax identification number")
  email?: string @fmt("email") @alias("e_mail")
  score: float [0..100]
  tags: string[] @minItems(1) @unique @contains("vip")
  kind: enum["individual", "company"]
  metrics: map<float>
  address: Address
  active: bool @default(true)

  where score >= 0
  when kind == "company" require [tax_id]
}
jsonschema.toJsonSchema(Customer)
{
  "type": "object",
  "description": "Invoice customer",
  "properties": {
    "name": { "type": "string", "minLength": 2, "maxLength": 40,
              "examples": ["Ada Byron"], "title": "Full name" },
    "tax_id": { "anyOf": [ { "type": "string", "minLength": 10, "maxLength": 10 },
                           { "type": "null" } ],
                "description": "Tax identification number" },
    "email": { "type": "string", "format": "email",
               "x-luma-aliases": ["e_mail"] },
    "score": { "type": "number", "minimum": 0, "maximum": 100 },
    "tags": { "type": "array", "items": { "type": "string" },
              "minItems": 1, "uniqueItems": true,
              "contains": { "const": "vip" } },
    "kind": { "type": "string", "enum": ["individual", "company"] },
    "metrics": { "type": "object", "additionalProperties": { "type": "number" } },
    "address": { "$ref": "#/$defs/Address" },
    "active": { "type": "boolean", "default": true }
  },
  "required": ["name", "tax_id", "score", "tags", "kind", "metrics", "address"],
  "additionalProperties": false,
  "allOf": [
    { "if":   { "properties": { "kind": { "const": "company" } } },
      "then": { "allOf": [ { "required": ["tax_id"] } ] } }
  ],
  "x-luma-where": [ { "expression": "score >= 0" } ],
  "x-luma-conditionals": [ … ]
}

Points worth noting

  • Given a single argument, @length(10) sets both minLength and maxLength to 10 — that is, exactly 10 characters.
  • On a union field, annotations are applied to the matching branch: the length constraint goes to the string branch and the null branch is left untouched.
  • @default and ? both keep the field out of required.
  • The root object always carries additionalProperties: false — the model cannot produce a field outside the schema.
  • where and conditional rules travel as x-luma-* extensions; gateways ignore them and Luma performs the validation locally.

A tagged union in JSON Schema

Each member is emitted in full inside anyOf, and the tag is pinned to a const in every one of them and joins that member's required list. That const is what does the discriminating: a document is a Receipt because its kind can only be "Receipt" there. This is the real dump for the Document union above:

jsonschema.toJsonSchema(Document)
{
  "anyOf": [
    { "type": "object",
      "properties": {
        "date": { "type": "string" },
        "total": { "type": "number" },
        "invoice_number": { "type": "string" },
        "kind": { "type": "string", "const": "Invoice" }
      },
      "required": ["date", "total", "kind"],
      "additionalProperties": false },
    { "type": "object",
      "properties": {
        "date": { "type": "string" },
        "total": { "type": "number" },
        "reference": { "type": "string" },
        "kind": { "type": "string", "const": "Receipt" }
      },
      "required": ["date", "total", "kind"],
      "additionalProperties": false }
  ]
}
No oneOf and no discriminator — that is deliberate

Until v0.5.0 this surface emitted oneOf with a sibling discriminator. Strict json_schema mode rejects both, so a tagged union — the headline feature of v0.2.0 — could not reach a strict provider at all. The wire form moved to anyOf with the tag as a const, which carries the same information in a shape every provider accepts, and schema.toJsonSchema reports exactly what goes on the wire. → strict mode

The untagged form carries none of that. A doc: Invoice | Receipt field becomes a plain list of references, and nothing in the schema tells the gateway — or a validator — which member was meant:

"doc": { "anyOf": [ { "$ref": "#/$defs/Invoice" }, { "$ref": "#/$defs/Receipt" } ] }

A parametric schema in JSON Schema

There is nothing special to see, and that is the whole point of expanding at parse time. Below is the real request body for an ask whose output type is Page<LineItem>; the model is handed a narrow object schema and never learns that a parameter was written:

jsonresponse_format.json_schema.schema
{
  "type": "object",
  "properties": {
    "items": { "type": "array", "items": { "$ref": "#/$defs/LineItem" } },
    "total": { "type": "integer" }
  },
  "required": ["items", "total"],
  "additionalProperties": false,
  "$defs": {
    "Page_LineItem": {
      "type": "object",
      "properties": {
        "items": { "type": "array", "items": { "$ref": "#/$defs/LineItem" } },
        "total": { "type": "integer" }
      },
      "required": ["items", "total"],
      "additionalProperties": false
    },
    "LineItem": {
      "type": "object",
      "properties": {
        "sku": { "type": "string" },
        "amount": { "type": "number" }
      },
      "required": ["sku", "amount"],
      "additionalProperties": false
    }
  }
}

$defs holds only what this root schema reaches: the expansion itself and the LineItem it refers to. A Page<Customer> declared elsewhere in the same program, on a path no request touches, is absent from the body — an expansion is not a request cost until something reachable points at it. That is why parametric schemas were held back until $defs pruning existed: without pruning, one wrapper multiplied by every payload type would be paid for on every single call.

Complete mapping table

Luma typeJSON Schema
string{"type":"string"}
int{"type":"integer"}
float{"type":"number"}
bool{"type":"boolean"}
null{"type":"null"}
any{} (no constraint)
enum[…]{"type":"string","enum":[…]}
T[]{"type":"array","items":T}
map<T>{"type":"object","additionalProperties":T}
json<T>Identical to T
A | B{"anyOf":[A, B]}
schema D = A | B tagged by k{"anyOf":[A, B]} — each member also gains k as a const, and that is what discriminates
[A, B] (tuple){"type":"array","prefixItems":[A,B],"minItems":2,"maxItems":2}
SchemaName{"$ref":"#/$defs/SchemaName"}
schema P<T> {…} used as P<X>{"$ref":"#/$defs/P_X"} — the expansion sits under $defs as an ordinary object schema

Which type when?

✓ Sturdy as model output
schema Record {
  id: string
  amount: float | null
  status: enum["open", "closed"]
  items: LineItem[]
  meta: MetaEntry[]
}
✗ Fragile as model output
schema Record {
  id: any             // no validation
  amount: float           // no "not found" path
  status: string          // free text
  items?: LineItem[]      // the model may skip it
  meta: map<any>        // may be rejected in strict mode
}
NeedChooseWhy
A fixed set of valuesenum[…]The model cannot produce free text; an enum violation is caught locally.
A value that may be absentT | nullThe field stays required and gives the model an explicit "none" path.
Extra information outside the schemaEntry[] (key/value)Compatible with strict gateways, unlike map<T>.
Carrying free-form JSONany or json<T>any does not validate; json<T> does.
A fixed-length pair of numbersfloat[2] or a two-field schemaTuples cannot carry per-position constraints.
A money or measurement valuefloat + whereTie the consistency invariant to where.
An identifier or codestring + @match / @fmtEnforce the shape at the schema level.

Choosing a type for a query row

Since v0.4.0 the same vocabulary also types data that no model produced. A query returns Schema[] and validates it row by row; a parallel over validates each element result against its declared element schema. Both name the position that failed — Orders.row[0].amount must be a number., classified[2].band must be one of these values: … — so the table above still applies, but the reasoning behind it changes.

A model output type is a negotiation: you are shaping what the model is allowed to say, and every choice trades strictness against the model's ability to comply. A row type is a description: the shape is already fixed by the table, nothing is negotiating with you, and the only question is whether the declaration matches what the bridge hands back. Nothing is coerced on the way in — the declared type is checked against the JS value the driver produced — so a column that arrives as a Date does not satisfy string, and the fix belongs in the SQL or in the bridge rather than in a looser type.

NeedChooseWhy
A NOT NULL columnTThe table already guarantees it. A row that violates it is schema drift, and you want that reported rather than absorbed.
A nullable columnT | nullSQL NULL arrives as null and a bare T rejects the row. x?: T does not help here — it permits an absent key, and a row that came out of a select carries the column as a key.
An aggregate (count, sum)int / float, returned as a bare SchemaA return type without [] asserts exactly one row: query 'Totals' expected exactly one row, got 3.
A column whose type you have not pinned downany, and only for nowany validates nothing, so the boundary stops naming the problem and the mismatch resurfaces later without a row index attached. Fine while exploring, a liability in a scheduled job.