Skip to content

Criteria: a condition only the documents state

A brief often names something no field of your data holds: twenty years marketing baby formula in European markets. Search answers it anyway. It uses your fields to narrow the directory to the people who could qualify, reads each remaining person's own documents against the condition, and returns everyone who meets it with the sentences that say so — and, separately, who is ruled out, who nearly fits and what is missing, and who could not be read. Every judgment is stored, so asking again reads only what is new.

This page is the guide. Every field it names is in the generated verb reference; the ordinary side of a search — constraints, the partition, counts, gaps — is search.

What the examples run on. PEOPLE is a workspace of ten invented experts: a country each (one relation, based_in) and eleven profiles and call transcripts registered as documents about them. (Three sections use ON_RECORD, eight more invented experts whose roles are on record; three use IN_DOUBT, six more; one uses READ_EARLIER, two whose judgments an earlier reader stored.) In a deployment a model reads those documents. The examples run offline and spend nothing, so the model's replies are scripted stand-ins — the replies the product's own tests hold for these documents. Everything after the model's reply is the product's own code: checking each quote against the document, the vocabulary, the second check, the store and the response.

Sending one

Put the conditions a field can decide in constraints, and the rest — the WHOLE niche, as one text in plain words — in criteria:

r = client.post("/v1/public/search", json={
    "workspace_id": PEOPLE,
    "constraints": [{"relation": "based_in", "slot": "place", "op": "eq", "value": "Europe"}],
    "criteria": [{"text": "20+ years marketing baby formula in European markets"}],
}).json()
read = r["criteria"][0]
print("status:", read["status"])
print("read from:", read["candidates"]["entities"], "people,",
      read["candidates"]["documents"], "documents | budget:", read["budget"])
print("split by:", read["decomposed_by"],
      [(f["key"], f["kind"], f["text"]) for f in read["facets"]])
print("counts:", read["counts"])
print("matched:", r["entities"]["matched"], "| rows delivered:", r["provenance"])
status: complete
read from: 9 people, 10 documents | budget: {'max_reads': 100, 'max_usd': 0.5, 'max_seconds': 40.0, 'retry_failed': False, 'max_read_seconds': 900.0}
split by: model [('duration', 'duration', '20+ years'), ('function', 'condition', 'marketing'), ('product', 'condition', 'baby formula'), ('market', 'condition', 'in European markets')]
counts: {'matched': 1, 'none': 1, 'undetermined': 6, 'not_recorded': 1}
matched: ['greta_ostrander'] | rows delivered: {'declared': 1, 'extracted': 1}

What happened, in order:

  1. The constraint ran first. Ten experts are in the workspace; nine are based in Europe. Only those nine are candidates — the tenth was never read. With no constraint at all, every entity of the kind is a candidate.
  2. The text was split into conditions (facets): a length of time, and three things an experience must state. decomposed_by: model — one model call did it. You can state the split yourself (below).
  3. Each candidate's documents were read against those facets, inside the budget the response echoes (budget: the defaults, since the request sent none).
  4. The criterion joined the search as one more required condition. entities, counts and matches at the top of the response already include it, so entities.matched is the answer to the whole brief. Each match carries two rows: the declared one the export stated (where the person is based) and an extracted one the read wrote — the criterion's judgment, with a span receipt per condition.

One match is not the whole answer. Six people are undetermined, and several of them are there because the reader commits only what it can PROVE: a profile that writes twenty years in a sentence the reader does not take a length from is not matched — and is not dropped. Its sentence is handed to you, as a doubt, with the count it writes (how a length is counted). Read the four lists, never the first alone.

One search carries one criterion. Send the niche whole — never one criterion per condition: the conditions must hold in ONE experience of the person, and only a single criterion says that.

Four lists, never two

A person either meets the niche, is ruled out, or the documents do not settle it. The response keeps those apart, because "the profile does not say" and "no" are different facts. First, the matches and what each rests on:

from docs.examples.niche import EUROPE, NICHE, search

read = search(client, PEOPLE, constraints=EUROPE, criteria=[{"text": NICHE}])["criteria"][0]
for person in read["matched"]:
    print(person["entity_id"])
    for quote in person["evidence"]:
        print("   ", quote["facet"] + ":", repr(quote["text"]),
              "| checked by", quote.get("checked_by", "rule"))
greta_ostrander
    function: 'led marketing for baby formula' | checked by vocabulary+second_call
    product: 'baby formula' | checked by second_call
    market: 'across Germany and Poland' | checked by vocabulary+second_call
    duration: 'for more than 20 years' | checked by rule

A match means: every document of the person was read; one of them states every condition of one experience, each with a quote that was found in the document; and none of them holds a sentence the reading model listed as a denial that the reader could not positively set aside (when a document denies the niche — a denial the model does NOT list is not seen, and that section says so). checked_by says who confirmed a condition beyond the first read — second_call (one more, narrower model call was asked whether that quote states the condition of the person's own work, and said yes), vocabulary+second_call (the workspace vocabulary placed the stated value under the condition's term — Germany and Poland are in Europe — and the second call confirmed whose work it describes), or vocabulary alone (the value fits; nobody was asked whose work the sentence describes — read the quote before relying on it). One more value appears on real profiles: typing_call+second_call (or vocabulary+typing_call+second_call). Where the words first offered for a typed condition are ones the vocabulary cannot place — a bare job title — one more model call is shown that experience's own sentences and the vocabulary's terms, and NOT your criterion, and names the term the sentences state; the evidence row then quotes the words it pointed at and carries the term it named ("ran the brand team" → Marketing), and the second call was asked whose work those words describe and said the person's own. The term is always the one that call named: where the vocabulary reads ANOTHER term in the words it pointed at ("General Management", at overseeing marketing, sales and finance), the two disagree and nothing is established — the person is listed undetermined with typer_and_vocabulary_disagree, and their typing entry shows the words. One more appears only when you ask for it: accepted_name+second_call — the stated words write a name you accepted for the condition (Saying what else counts). A duration carries no checked_by: no model confirms it. The reader counts it itself, from the quote, by rule — and because nothing checks that count, it takes a length from ONE form of sentence only: "Greta has done this work for more than 20 years." How a length is counted says what that form is, and what becomes of a length written any other way. (A length is never checked_by: record: the step that reads one from your record's dates is switched off.)

Then everyone who is not a match — three different facts:

from docs.examples.niche import EUROPE, NICHE, search

read = search(client, PEOPLE, constraints=EUROPE, criteria=[{"text": NICHE}])["criteria"][0]
for person in read["none"]:
    print("NONE", person["entity_id"], [quote["text"] for quote in person["evidence"]])
for person in read["undetermined"]:
    print("UNDETERMINED", person["entity_id"], "-", person["reason"],
          "| has:", person["established"], "| missing:", person["missing"])
    print("    because:", person.get("missing_because"),
          "| states instead:", [quote["text"] for quote in person.get("different", [])])
for person in read["not_recorded"]:
    print("NOT RECORDED", person["entity_id"], "-", person["reason"])
NONE dario_benavente ['has never worked on baby formula']
UNDETERMINED cornelia_vantongeren - conflict | has: ['function', 'product', 'market'] | missing: ['duration']
    because: {'duration': 'length_not_proved'} | states instead: []
UNDETERMINED margaux_delacroix - silent | has: ['function', 'product', 'market'] | missing: ['duration']
    because: {'duration': 'length_not_proved'} | states instead: []
UNDETERMINED friederike_hollerer - silent | has: ['function', 'product'] | missing: ['duration', 'market']
    because: {'duration': 'length_not_proved', 'market': 'quote_not_in_document'} | states instead: []
UNDETERMINED kasper_vandenberg - silent | has: ['product', 'market'] | missing: ['duration', 'function']
    because: {'duration': 'length_not_proved', 'function': 'quote_outside_experience'} | states instead: []
UNDETERMINED nikolai_wierzbicki - silent | has: ['function', 'product'] | missing: ['duration', 'market']
    because: {'duration': 'length_not_proved', 'market': 'different'} | states instead: ['across Southeast Asia']
UNDETERMINED silvio_calderoni - silent | has: [] | missing: ['duration', 'function', 'product', 'market']
    because: {'duration': 'duration_unknown', 'function': 'different', 'product': 'silent', 'market': 'silent'} | states instead: ['is a structural engineer']
NOT RECORDED xenia_bergstrom - no_documents

  • none — a document states, in words that were found in it, that the person did NOT meet a condition; the denial is PROVED the person's own (their name leads it: "Dario has never worked on baby formula."); and no document of the person states what it denies. It is a quoted denial, never an inference from silence — and never a sentence the reader is in doubt about (when a document denies the niche). Its quote in evidence says so: denies: true.
  • undetermined — the documents do not settle it. silent: they do not state every condition; established and missing say how far each person got, and the list comes nearest first, so the people one condition short open it. missing_because gives the reader's reason per condition, and it is worth reading: one profile states another value (different — the quote is in different: the marketing was across Southeast Asia); for another the model offered a quote that is not in the document (quote_not_in_document — refused, so the condition is not established); for a third the quote comes from a sentence about a later role (quote_outside_experience — the conditions must hold in one experience). Five of them say length_not_proved: the document WRITES a length for the experience, in a sentence the reader does not take a length from, and the entry hands that sentence on under lengths_not_proved — a doubt for you to judge, not a "no" (how a length is counted). duration_unknown means what it says: no document states a length for an experience that establishes one of your conditions, and no sentence is handed on. conflict: the documents say both — here a profile states the product and the person's own call note denies it.
  • not_recorded — nothing could be read: the person has no document (no_documents), or a document is still unread (unread, with its causes).

Never report the last two as "no". They are the people a human should look at, with the reason attached. A match is delivered only on what the reader could check: every quote is the document's own words, a condition is confirmed by the check its checked_by names, and a length is granted only by a sentence that passes every one of a closed list of checks (how a length is counted). That is a short list of things checked, not a promise that no match is wrong — each section that describes a check ends with What it cannot see, the ways a wrong match can still be made.

Checking a quote yourself

Every quote carries the document it came from and its span. GET /v1/public/region re-reads those bytes from the stored document, so a quote is checked against the source rather than taken on the response's word:

from docs.examples.niche import EUROPE, NICHE, search

read = search(client, PEOPLE, constraints=EUROPE, criteria=[{"text": NICHE}])["criteria"][0]
quote = read["matched"][0]["evidence"][0]
stored = client.get("/v1/public/region", params={
    "workspace_id": PEOPLE, "source_id": quote["source_id"],
    "start": quote["char_start"], "end": quote["char_end"]}).json()
print(repr(stored["text"]), "| the same bytes:", stored["text"] == quote["text"])
'led marketing for baby formula' | the same bytes: True

What it costs, and how to cap it

Model calls are the only part that costs, and a niche makes up to four kinds of them:

  • one call to split the text into conditions — only for a text you did not split yourself, made before the plan is priced and outside the budget;
  • one call per candidate document — the read;
  • for a document that would otherwise be a match, a second, narrower call that checks what the read reported;
  • and before that second call, where a typed condition rests on words the vocabulary cannot place (a bare job title), a typing call that is shown the experience's own sentences and not your criterion.

So the price of a niche is the number of people your constraints leave — the more a brief says in fields, the less there is to read. The plan is priced BEFORE anything is read, against criteria_budget (max_reads, max_usd): a plan above it reads nothing and says what it would have cost. A plan the budget allows is read WHOLE, in the background, under that budget — max_seconds is how long the call waits for it, max_read_seconds how long the read itself may go on.

from docs.examples.niche import EUROPE, FACETS, NICHE, search

mine = {"text": NICHE, "facets": FACETS}
plan = search(client, PEOPLE, constraints=EUROPE, criteria=[mine],
              criteria_budget={"max_usd": 0})["criteria"][0]
print(plan["status"], "-", plan["status_reason"], "| needed:", plan["reads"]["needed"],
      "| read by this call:", plan["reads"]["done"], "| matched so far:", plan["counts"]["matched"])
print("estimate:", plan["estimate"]["cost_basis"], "| costs something:",
      plan["estimate"]["cost_usd"] > 0, "| a role constraint would narrow it:",
      "held_role" in plan["unconstrained_relations"])
print("calls at most:", plan["estimate"]["reads"], "reads +",
      plan["estimate"]["adjudication"]["calls_at_most"], "second calls +",
      plan["estimate"]["typing"]["calls_at_most"], "typing calls")

two_countries = [{"relation": "based_in", "slot": "place", "op": "in",
                  "value": ["Germany", "France"]}]
narrowed = search(client, PEOPLE, constraints=two_countries,
                  criteria=[{"address": plan["address"]}],
                  criteria_budget={"max_reads": 3})
print(narrowed["criteria"][0]["status"], "| needed:", narrowed["criteria"][0]["reads"]["needed"],
      "| matched:", narrowed["entities"]["matched"])
over_budget - max_usd | needed: 10 | read by this call: 0 | matched so far: 0
estimate: priced | costs something: True | a role constraint would narrow it: True
calls at most: 10 reads + 10 second calls + 10 typing calls
complete | needed: 3 | matched: ['greta_ostrander']

  • To price a brief without reading it, send "max_usd": 0. over_budget is a refusal of the whole plan — never half a read — and estimate holds the reads, tokens and dollars. It is an upper bound: it prices a second call and a typing call for EVERY document (estimate.adjudication, estimate.typing), and only would-be matches get either. What a read really made is in usage (every call of this search, the split included) and reads (done, adjudicated — the documents that got a second call — and typed, the ones that got a typing call). unconstrained_relations names the relations the brief left open — what could narrow it.
  • Narrow, and it fits. Two countries leave three documents; the same criterion — sent back by its address — is read for those and the match among them is returned.
  • A request returns inside the deployment's wall (max_seconds, 40 s here) — and the read does not stop when it does. The call that starts a read hands the whole plan to a background reader, which reads it to the end with no further call. A large read therefore comes back partial with background.reading: true and retry_after_s (about how long the rest takes): call again with the same constraints and the address to WAIT for it — that call returns the moment the read is done, and starts nothing a reader already holds. estimate.calls_to_complete says how many such calls a plan needs.

Stating the split yourself

The example above sent facets: the caller's own split of the text, so no model call split it. Each facet is the text's exact words for one condition — a condition, a duration with min_years, or a recency (current, or within_years). The response echoes every facet with what the reader made of it, and that echo is worth a look before you pay for a read:

from docs.examples.niche import EUROPE, FACETS, NICHE, search

def priced(facets):
    return search(client, PEOPLE, constraints=EUROPE, criteria_budget={"max_usd": 0},
                  criteria=[{"text": NICHE, "facets": facets}])["criteria"][0]

clean = priced(FACETS)
print("split by:", clean["decomposed_by"], "| typed:",
      [(f["key"], f.get("value")) for f in clean["facets"] if f["kind"] == "condition"])
merged = priced([FACETS[0], FACETS[3],
                 {"key": "work", "kind": "condition", "text": "marketing baby formula"}])
print("two conditions in one facet:",
      [(f["typed"], f["typed_reason"]) for f in merged["facets"] if f["key"] == "work"])
left_out = priced([facet for facet in FACETS if facet["key"] != "function"])
print("a word no facet took:", left_out["unchecked_words"])
split by: caller | typed: [('function', 'Marketing'), ('product', None), ('market', 'Europe')]
two conditions in one facet: [(False, 'term_is_not_the_head')]
a word no facet took: ['marketing']

  • A typed condition is held to the vocabulary. value names the ONE vocabulary term a condition means ("in European markets" → Europe), and the reader types a condition whose own words name a term by itself (marketing → Marketing). Typed, a quote that names another term — "across Southeast Asia" — is refused by the vocabulary; untyped (product here: the vocabulary has no such term), that judgment rests on the model and its second check.
  • One condition per facet. "marketing baby formula" as a single facet is judged as one statement: the word marketing inside it is no longer held to the vocabulary, and the echo says so (typed: false, term_is_not_the_head). Split it into the function and the product.
  • A word left out is a word nobody checks. Your facets are a statement: nothing is added to them. A word of the text that no facet took is listed in unchecked_words, on the block and on every match — a match is a match for the facets only.
  • A duration is a minimum, in years. min_years is the LEAST number of years, and the facet's own words must state that number as a count of years and as a minimum: "20+ years", "more than five years", "a decade". A length that bounds from above, gives a range or counts another unit — "less than 5 years", "up to five years", "between 5 and 10 years", "18 months" — is refused: your own facet with a 422 that names the fault (below), a model's split as incomplete. No field states a maximum.
  • A caller's split and a model's split of one text are two criteria, each read separately.

When a model splits the text

A text you send without facets is split by one model call, and the reader then checks that split against your words before anything is read. The response says what it made of it — decomposition (accepted, with the actions it took) and, on each facet of the echo, the words the reader's own rules compare with:

from docs.examples.niche import EUROPE, search

read = search(client, PEOPLE, constraints=EUROPE, criteria=[{
    "text": "Experts who led marketing for baby formula in European markets for 20+ years",
}])["criteria"][0]
print(read["decomposition"]["status"], "|", read["decomposition"]["actions"])
for facet in read["facets"]:
    print(facet["key"] + ":", repr(facet["text"]), "| core:", facet.get("core"),
          "| carried:", facet.get("carried"))
print("unchecked words:", read["unchecked_words"], "| matched:", read["counts"]["matched"])
print("model calls:", read["usage"]["calls"], "= 1 split +", read["reads"]["done"], "reads +",
      read["reads"]["adjudicated"], "second calls +", read["reads"]["typed"], "typing calls")
accepted | [{'action': 'carried', 'key': 'function', 'words': 'Experts who led'}, {'action': 'lead_in', 'key': 'market', 'core': 'European markets', 'words': 'in'}]
duration: '20+ years' | core: None | carried: None
function: 'marketing' | core: None | carried: ['Experts who led']
product: 'baby formula' | core: None | carried: None
market: 'in European markets' | core: European markets | carried: None
unchecked words: [] | matched: 1
model calls: 12 = 1 split + 10 reads + 1 second calls + 0 typing calls

  • core is a facet without the lead-in it opens with ("in European markets" → European markets): what the vocabulary and the reader's rules are held to. The model is still shown the whole of the facet's text.
  • carried are words of your text that stand before a facet and lead into it — Experts who led. They are nobody's condition: nothing checks that a person LED the marketing rather than worked in it, and they are NOT listed in unchecked_words. Only a short closed list of plain verbs of doing is carried this way (who sold, who worked in, who led). If the verb matters to you, state it as a condition of its own.
  • completed_by: reader marks a condition the reader added itself, from words the model left out that the vocabulary types (a country, a function).

Anything else the split leaves out — or joins — is NOT read around. The search reads nothing (not_run, decomposition_incomplete), mints nothing, and hands the pieces back: facets (what holds), decomposition.incomplete (each remaining piece and why), and suggested — the reader's proposal for a piece, as a facet you can send:

from docs.examples.niche import EUROPE, NICHE, search

text = NICHE + " through pharmacies"
stopped = search(client, PEOPLE, constraints=EUROPE, criteria=[{"text": text}])["criteria"][0]
print(stopped["status"], "-", stopped["status_reason"],
      "| documents read:", stopped["reads"]["done"])
print("left out:", stopped["decomposition"]["incomplete"])
print("suggested:", [(facet["kind"], facet["text"]) for facet in stopped["suggested"]])

mine = stopped["facets"] + stopped["suggested"]     # ONE reading of every piece: yours now
read = search(client, PEOPLE, constraints=EUROPE,
              criteria=[{"text": text, "facets": mine}])["criteria"][0]
print(read["status"], "| split by:", read["decomposed_by"], "|",
      [facet["text"] for facet in read["facets"]])
print("counts:", read["counts"])
not_run - decomposition_incomplete | documents read: 0
left out: [{'words': 'through pharmacies', 'why': 'no_facet_states_it'}]
suggested: [('condition', 'through pharmacies')]
complete | split by: caller | ['20+ years', 'marketing', 'baby formula', 'in European markets', 'through pharmacies']
counts: {'matched': 0, 'none': 1, 'undetermined': 7, 'not_recorded': 1}

The split the model returned had no facet for through pharmacies, so a read under it would have delivered marketers who never sold through a pharmacy as confirmed matches. Sent back with the suggested facet, the text is read as five conditions — and nobody's profile states the fifth. Three things to know:

  • Send ONE reading of each piece. Where a piece can be read two ways — "marketing automation software": one product, or the function marketing and a product — it is suggested both ways, each entry marked reading (split or whole). Both share a key: facets plus suggested sent back unchanged is then a 422. Choose.
  • Some pieces get no suggestion (nothing_suggested: true), because no facet could state them: a denial or an exception ("not in Asia", "other than infant formula", "experts who have never worked in sales"), a preference ("ideally in Europe"), an alternative ("marketing or sales"), a length that is no minimum in years, words that name nothing. A facet states something a person HAS done, and every facet is required. Reword the text:

from docs.examples.niche import EUROPE, NICHE, search

stopped = search(client, PEOPLE, constraints=EUROPE,
                 criteria=[{"text": NICHE + ", not in Asia"}])["criteria"][0]
print(stopped["status_reason"], "|", stopped["decomposition"]["incomplete"],
      "| suggested:", stopped["suggested"])
decomposition_incomplete | [{'words': 'not in Asia', 'why': 'negates_or_bounds_a_condition', 'nothing_suggested': True}] | suggested: []

  • Nothing of a refused split is kept. The same text sent again is split again (and that one call is paid again); your own facets are a statement and are read as sent.

Saying what else counts

A profile often names the thing you asked for in other words — espresso machines under a brief for kitchen appliances, infant milks under one for baby formula. Whether those words count is knowledge of a trade, and a reading model holds it unevenly: measured on real replies, most of the people one brief still missed were missed for exactly this. So the search does not ask the model to know. It shows you the words, and you say.

What the near-misses state instead. After a read, stated_instead lists — per condition — the words that candidates ONE CONDITION SHORT state for the condition they lack, with how many people state them. These examples run on ON_RECORD, a second small workspace: eight invented experts whose roles are on record. Two of them sold the product under another name; another sold something else that holds that name.

from docs.examples.niche import FACETS, NICHE, search

read = search(client, ON_RECORD, criteria=[{"text": NICHE, "facets": FACETS}])["criteria"][0]
print("matched:", [person["entity_id"] for person in read["matched"]])
for facet, listed in read["stated_instead"].items():
    print(facet, "|", listed["distinct"], "distinct |",
          [(value["states"], value["people"]) for value in listed["values"]])
matched: []
product | 2 distinct | [('infant milks', 2), ('packaging for infant milks', 1)]

Three people are short of the product alone: two profiles say infant milks, the third packaging for infant milks. The list is words and counts, never a judgment: it does not say which of them count — that is yours to say, value by value. It offers only words you could send back — not a value the vocabulary places outside a typed condition (a profile that says Southeast Asia is short of "in European markets", and no name mends that), not a denial ("everything except infant milks"), not a whole sentence, not a rank or a country under another kind of condition, not a name the criterion already holds, and not a condition whose words were read in another role (second_call:role_different). Those people are still listed under undetermined with the reason and their own words. values is cut at list_limit like the other lists; distinct is the whole count.

Accepting a name. Where a listed value IS the thing your condition asks for — a kind of it, or the trade's own word for it — send the criterion again with that value in the facet's also:

from docs.examples.niche import FACETS, NICHE, search

named = [dict(facet, also=["infant milks"]) if facet["key"] == "product" else facet
         for facet in FACETS]
read = search(client, ON_RECORD, criteria=[{"text": NICHE, "facets": named}])["criteria"][0]
for person in read["matched"]:
    product = next(quote for quote in person["evidence"] if quote["facet"] == "product")
    print(person["entity_id"], "|", repr(product["text"]), "| the name:",
          product.get("accepted_name"), "| checked by", product["checked_by"])
print("still offered:", [(value["states"], value["people"])
                         for value in read["stated_instead"]["product"]["values"]])
print("not matched by the name:", [(person["entity_id"], person["missing_because"])
                                   for person in read["undetermined"]
                                   if "product" in person["missing_because"]])
ottoline_brandvold | 'her portfolio centred on infant milks' | the name: infant milks | checked by accepted_name+second_call
ragnhild_storvik | 'launched infant milks in Poland' | the name: None | checked by second_call
still offered: [('packaging for infant milks', 1)]
not matched by the name: [('benedikte_ravn', {'product': 'different'})]

Both profiles that state infant milks now match — in two different ways, and each match says which. The first rests on YOUR statement: its evidence carries checked_by: accepted_name+second_call and accepted_name, the name it rests on. The second rests on the second call's reading of the sentence: checked_by: second_call, and no accepted_name. The profile that states packaging for infant milks is not matched by infant milks — and is still offered, for you to accept or leave. What a name does and does not do:

  • It is read only in what the profile STATES for the condition — the first thing the reader reported for it — never anywhere else in a sentence. "Sold yoghurt to teams that also bought infant milks" states yoghurt.
  • A name is matched whole, as a profile states it. The stated words must BE the name: a determiner or a preposition before them aside ("the infant milks", "across the Alpine cluster"), its last word's plural aside (infant milk for infant milks). Nothing is taken off YOUR name: accepting specialised nutrition does not accept a profile that states nutrition, and accepting nutrition does not accept managed nutrition. A narrower thing, a longer phrase or a list is another name — "organic infant milks", "veterinary infant milks", "infant milks packaging", "packaging for infant milks", "infant milks and yoghurt" — and accepting infant milks accepts none of them. To accept such words, send them as they are listed: the reader cannot tell a kind of your product from another product, and a one-word name ("formula") would otherwise match "a shampoo formula".
  • In its own words. silicone is not silicon, wood pellets are not wood pallets, and custom software is not customs software: a name is never read up to a spelling, and only its last word's plural is set aside.
  • A name SETTLES the condition only in a sentence the reader can read. There are three things a stated name can become, and which one is decided by the name's own sentence, never by the reading model's choice of words:
    • Settled — the sentence states it as the person's own work in one of a closed list of forms: the person, then a verb of doing ("she sold infant milks", "has led …", "ran …"); the same with the work named as a thing and a preposition that says what the work is of ("took charge of sales for …", "was responsible for …", "ran the marketing of …", "led the launch of …", "spent ten years on …"); or their own work as the subject ("her portfolio centred on …", "their remit covered …", "The range in question was …"). The thing between the verb and the name must be one the reader knows to be the person's work: a word of work (charge, responsibility, launch, portfolio, experience …), a length of time, or what ANOTHER condition of your criterion is about — that condition's own words, the words the profile states for it, or a term of the vocabulary axis it is read on ("sales" under a criterion that asks for a function). And nothing stands behind the name, to the end of its sentence, but more of a list ("… and yoghurt" — "and" only: "… or tried to" is a vote), a title, or a place or a time the reader can place ("in Poland", "since 2019"). A MEANS behind "through" or "via" is placed only by what the reader KNOWS: every word of it is a term of a vocabulary, a number or a date — or the means is, word for word, ANOTHER condition of your criterion as you wrote it. So under a criterion that asks for "… through distributors" as a condition of its own, "sold infant milks through distributors" settles the name, and that condition judges the channel. A LONGER channel is another channel — "through distributors of veterinary supplies", "via veterinary distributors", "through distributors and pet shops" — and is a vote, as "through pharmacies" is under a criterion that asks for no channel: "infant milks through pet shops" is another trade, and the reader cannot tell the two apart. What the reading model SAYS the profile states for the other condition places nothing. Nobody is then asked what the words mean; the second call is asked only whose work they describe. The match says accepted_name+second_call.
    • A vote — any other sentence. The name still overrules a reading model that does not know the trade's word, but it settles nothing: the second call is asked the ordinary question — do these words state the condition of the person's own work? — and decides. This is what becomes of a verb the reader does not list ("launched infant milks in Poland", as printed above), of a word between the verb and the name ("marketed veterinary infant milks"), of a noun there that names somebody else, a unit, a place or a thing ("served customers of infant milks", "worked with the team for …", "worked for a company in …", "ran the business in …", "sold products for …"), of a preposition that states no work on what follows ("worked towards …", "sold into …"), of a phrase behind the name the reader cannot place ("infant milks for calves", "… in name only", "… to pharmacies"), of a channel it cannot place ("… through pharmacies", "… via distributors", "… via veterinary practices" — the first two true, the third another trade), of a sentence somebody else leads ("her rival ran …", "the company also sells …") and of every way of taking the name out that the reader has no word for ("she turned down …", "her training skipped …"). Such a match says checked_by: second_call and carries no accepted_name: it rests on the second call's reading, not on your statement. Read the quote.
    • Refused, with nobody asked — the few forms the reader KNOWS take the name out: a denial in the name's own sentence ("has never worked on infant milks" — a denial of the name anywhere else in the person's documents blocks too: when a document denies the niche), an exclusion or a contrast right before it ("her remit excluded …", "every dairy line with the exception of …", "…, unlike …", "lost … to a rival division"), and the name as the complement of another thing ("packaging for infant milks", "suppliers of infant milks" — unless the word before the preposition is a word of work, an activity or a rank your vocabulary holds, or what the profile states for another condition: "sales for infant milks"). The person stays one condition short, by the first reading's own answer. This list is a short cut, not the guard: a way of taking a name out that it does not hold is left to the closed forms above, and those hold no word of denial.
  • A settled name settles what the words NAME, nothing else. Whose work the sentence describes is still asked of the second call — about the name's own words — and its answer is heard: a person the second call reads as only beside the work, or as naming it in another role, is listed undetermined with that reason. That also means a name the reading model disbelieves may still be removed by it; the entry then says so.
  • Only a caller states one. A model's split of a text never carries also; the response's facets echo it only where you sent it.
  • Other names are another criterion. also is part of the criterion's identity: a criterion with names has its own address, and its candidates are read again — price it first ("max_usd": 0). Up to twelve names per condition, six words each.
  • A name the reader can tell is wrong is a 422 (criterion_facets_invalid): the condition's own words; a denial or a preference; a number or a length of time; words that name nothing ("team", "the same work"); a name another condition of the criterion already states; and, on a typed condition, a name the vocabulary places OUTSIDE its value or broader than it — to ask for another value, state it as the facet's value:

from docs.examples.niche import FACETS, NICHE

def refused(key, name):
    facets = [dict(facet, also=[name]) if facet["key"] == key else facet for facet in FACETS]
    r = client.post("/v1/public/search", json={
        "workspace_id": ON_RECORD, "criteria": [{"text": NICHE, "facets": facets}]})
    return r.status_code, r.json()["detail"]["faults"]

print(refused("market", "Southeast Asia"))
print(refused("product", "not baby formula"))
print(refused("product", "marketing"))
(422, ['market:also_name_outside_the_value'])
(422, ['product:also_name_negates'])
(422, ['product:also_name_is_another_condition'])

Asking again

Judgments are stored per criterion and per document, so the second question is free — and a wider question reads only the documents the narrower one did not:

from docs.examples.niche import EUROPE, NICHE, search

first = search(client, PEOPLE, constraints=EUROPE, criteria=[{"text": NICHE}])["criteria"][0]
again = search(client, PEOPLE, constraints=EUROPE,
               criteria=[{"address": first["address"]}])["criteria"][0]
print("again:", again["status"], "| needed:", again["reads"]["needed"],
      "| reused:", again["reads"]["reused"], "| model calls:", again["usage"]["calls"])
spaced = search(client, PEOPLE, constraints=EUROPE, criteria_budget={"max_reads": 0},
                criteria=[{"text": NICHE.upper().replace(" ", "  ")}])["criteria"][0]
stopped = search(client, PEOPLE, constraints=EUROPE, criteria_budget={"max_reads": 0},
                 criteria=[{"text": NICHE + "."}])["criteria"][0]
print("other case and spacing: the same criterion:", spaced["address"] == first["address"])
print("a final full stop:", stopped["status"], "-", stopped["status_reason"])
again: complete | needed: 10 | reused: 10 | model calls: 0
other case and spacing: the same criterion: True
a final full stop: not_run - reuse_only_criterion_unknown

  • Reuse by address. Every response returns it. A text's identity ignores case and spacing and nothing else: the same words with a final full stop are another criterion, read — and paid for — again. Two phrasings of one niche are never merged.
  • "max_reads": 0 reuses only. It answers from stored judgments and never reads; a criterion it has not seen is not_run (reuse_only_criterion_unknown). A read-only key may send it; asking for reads with a read-only key is a 403.
  • A new document about a candidate is the only thing read the next time.
  • A judgment is reused only by the reader that could have written it. A stored judgment is one reader's: its version, and the setting of every one of its switches — one digest, which the block returns as reader_identity. After an upgrade of the deployment, or a switch an operator sets otherwise, the reader is another one, and what the earlier reader stored is NOT served: it would hand you that reader's matches under this reader's name.

READ_EARLIER holds two people whose profiles were read by an earlier reader — one that took a length from "She has done this work for more than 20 years." on its word. Both were matches then. The same criterion, asked of the reader this deployment runs:

from docs.examples.niche import FACETS, NICHE, search

mine = {"text": NICHE, "facets": FACETS}
reuse = search(client, READ_EARLIER, criteria=[mine],
               criteria_budget={"max_reads": 0})["criteria"][0]
print("reuse only:", reuse["status"], "| stored, not served:", reuse["reads"]["stored_not_served"])
print("   ", reuse["counts"], "|", [(p["entity_id"], p["reason"], p["causes"])
                                    for p in reuse["not_recorded"]])
read = search(client, READ_EARLIER, criteria=[mine])["criteria"][0]
print("with a budget:", read["status"], "| read again:", read["reads"]["done"],
      "| reused:", read["reads"]["reused"], "|", read["counts"])
print("   ", [(p["entity_id"], p["missing_because"]) for p in read["undetermined"]])
again = search(client, READ_EARLIER, criteria=[mine])["criteria"][0]
print("and again: read:", again["reads"]["done"], "| reused:", again["reads"]["reused"],
      "| stored, not served:", again["reads"]["stored_not_served"])
reuse only: over_budget | stored, not served: {'read_by_another_reader': 2}
    {'matched': 0, 'none': 0, 'undetermined': 0, 'not_recorded': 2} | [('borghild_aasen', 'unread', ['read_by_another_reader']), ('eldrid_nesheim', 'unread', ['read_by_another_reader'])]
with a budget: complete | read again: 2 | reused: 0 | {'matched': 1, 'none': 0, 'undetermined': 1, 'not_recorded': 0}
    [('borghild_aasen', {'duration': 'length_not_proved'})]
and again: read: 0 | reused: 2 | stored, not served: {}

  • Reuse-only says what it did not serve. reads.stored_not_served counts the documents by cause (read_by_another_reader), and each person is not_recorded, unread, with that cause — never the earlier reader's verdict. The count is also in counts_detail.not_recorded.unread_causes. The status is over_budget: two reads are needed and the request allowed none.
  • A search that may read reads each such document again, once, and stores its own judgment: here one of the two is still a match, and the other is handed her sentence — this reader does not take a length from behind "She". From then on its own judgments are reused.
  • THE PRICE: one read of every stored judgment at the first search after an upgrade. Price it before you pay it ("max_usd": 0 plans and reads nothing), and budget for it where a workspace holds many read criteria.
  • A new VERSION of the reader is a new criterion. The version is part of a criterion's identity: after such an upgrade the same text has another address and is read in full, and the address an earlier version minted is refused — a 422, criterion_stale_reader_version: send the text again.

When a read needs more than one call, the loop is short — resume by address, wait when there is nothing left to ask for, and stop after a few tries (a document that failed twice will fail again):

import time
from docs.examples.niche import EUROPE, NICHE, search

criterion, calls = {"text": NICHE}, 0
while True:
    response = search(client, PEOPLE, constraints=EUROPE, criteria=[criterion], list_limit=0)
    read, calls = response["criteria"][0], calls + 1
    if read["status"] != "partial" or calls == 8:
        break
    criterion = {"address": read["address"]}   # the SAME criterion, where it stopped
    if not read["background"]["reading"] and read["reads"]["in_flight_at_return"]:
        time.sleep(20)   # nothing is read behind this reply but an earlier call's last reads
print(read["status"], "after", calls, "call |", read["counts"])
complete after 1 call | {'matched': 1, 'none': 1, 'undetermined': 6, 'not_recorded': 1}

A call returns by its wall with profiles still being read (reads.in_flight_at_return) and, usually, more of them queued (reads.not_submitted): the block's background says that a reader is at work on them (reading), how many it still holds (queued, in_flight) and about how long it needs (retry_after_s). The read stays partial until the last has landed, and the loop above needs no pause: a call made while the reader works waits for it, up to its own max_seconds. Each reply's usage is of the reads that landed and that no earlier reply reported, so the replies of one read add up to what it cost.

A deployment that runs several API processes turns the background reader off (LIMMA_CRITERIA_BACKGROUND=0): there a criterion is read only while a call is open, background.reading is never true, a call that found nothing left to submit learns nothing by being repeated at once — hence the pause above — and max_seconds must exceed the deployment's read time. A worked loop with a spend ceiling and every way of stopping is demo/expert_network/pre_read.py.

Bounding the reply

At scale the lists of non-matches are long and an agent rarely needs them whole. list_limit (0–200) bounds every list of NON-matches and never cuts the matches below their own fixed cap (a criterion's matched list and entities.matched hold at most 200 people each; counts.matched is always the whole number, and matches is paged by limit). The counts stay whole, and counts_detail says, over every candidate, how many are one condition short, which condition is open for how many, and why the unread are unread:

from docs.examples.niche import EUROPE, NICHE, search

read = search(client, PEOPLE, constraints=EUROPE, criteria=[{"text": NICHE}],
              list_limit=0)["criteria"][0]
print("listed:", {name: len(read[name]) for name in
                  ("matched", "none", "undetermined", "not_recorded")})
print("counted:", read["counts"])
short = read["counts_detail"]["undetermined"]
print("one condition short:", short["one_condition_short"], "| conflict:", short["conflict"])
print("open for how many:", short["missing"])
print("and why:", short["missing_because"])
print("people handed a length to judge:", short["lengths_not_proved"])
print("not recorded:", read["counts_detail"]["not_recorded"])
listed: {'matched': 1, 'none': 0, 'undetermined': 0, 'not_recorded': 0}
counted: {'matched': 1, 'none': 1, 'undetermined': 6, 'not_recorded': 1}
one condition short: 1 | conflict: 1
open for how many: {'duration': 5, 'function': 2, 'market': 3, 'product': 1}
and why: {'duration': {'duration_unknown': 1, 'length_not_proved': 4}, 'function': {'different': 1, 'quote_outside_experience': 1}, 'market': {'different': 1, 'quote_not_in_document': 1, 'silent': 1}, 'product': {'silent': 1}}
people handed a length to judge: 5
not recorded: {'no_documents': 1, 'unread_causes': {}}

Send 10 to show the nearest near-misses, 0 to report numbers only. Take numbers from counts and counts_detail, never from counting the entries you were sent.

"Open" is the reader's own account, not the length of missing. An entry lists a length under missing whenever any condition is — the store keeps a length with the conditions it belongs to — also when the profile states that length in the form the reader reads. missing_because names what is really open, and counts_detail is counted from it: one person here is one condition short (the length her transcript writes, which the reader did not take), three more are short of a condition AND of a proved length, and one of everything. Count an entry as one condition short when missing_because names exactly one facet; one_condition_short and counts_detail.undetermined.missing are counted that way. A person in conflict is counted there and nowhere else.

missing_because in counts_detail is the same count split by the reader's reason, so a reply that lists nobody still tells a profile that STATES a length the reader did not take (length_not_proved: four here) from one that states none (duration_unknown: one). lengths_not_proved is the number of undetermined people — a conflict included, which is why it is five — who carry at least one handed-on sentence: what a reader of those sentences has in front of it.

The compact reply (search: detail: "compact", the MCP tool's default) carries none of these lists. It names the matches with one line per condition, and under close.matches the candidates worth a call: exactly one condition open by the reader's account, and that condition open in one of two ways. because: "not_stated" — the documents say NOTHING for it (missing_because is silent, or duration_unknown for a length). because: "stated_not_verified" — a document STATES the length and the reader did not take it as proved (missing_because is length_not_proved): the entry then carries quote, the one sentence the reader handed on (the first of the entry's lengths_not_proved for that condition, at most 160 characters) — read it, it is never a match. lacks names the open condition, why the ones the documents do establish. It lists at most ten, nearest first, and close.total counts them all. Everybody else who is undetermined is counted in others.cannot_tell and not listed: a profile that states ANOTHER value for the open condition (different, second_call:different — it is one condition short and is not a missing fact), a condition the second call did not confirm, a conflict, two or more open conditions. A length under the threshold (duration_below_threshold) is among them: the reply carries the reason and not the years. reading appears while the read is unfinished (done of of, status, because; with background: true the rest is being read without a further call, for about retry_after_s seconds) — call again with criterion, the address, to wait for it. For one person's quotes, reasons and receipts, send the search again with entity_ids, detail: "full", the address and criteria_budget: {"max_reads": 0}: the four lists then hold those people's entries only, from the stored judgments, and nothing is read.

When a document denies the niche

A match takes proof. A denial takes much less: the reader KEEPS one on doubt. Those are the two halves of one rule — what the reader does not positively know should never move a person toward a match. This section and the next say what the product DOES to hold each half, form by form, and where it still falls short of the rule each says so under "What it cannot see".

A reading model lists the sentences it takes for a denial of a condition. The reader makes each of them one of three things, and the reply says which:

  • Firm — PROVED the person's own outright denial: the whole sentence is the person, the denial and the condition, and the person is NAMED — by their given name or their full name, exactly as a length must be (who leads it) — ("Dario has never worked on baby formula.") or speaks in the first person in a turn that is theirs ("Cornelia: I have never worked on baby formula myself."). A pronoun proves nobody: "He has never worked on baby formula." is firm in no profile. Neither does a family name alone, or a name behind a relative's. In evidence a firm denial is marked denies: true.
  • A doubt — everything else on the model's list that the reader cannot positively set aside. Kept, quoted whole, and marked kept_as: "doubt".
  • Set aside — in ONE form only, the mirror of the firm one: the whole sentence is ANOTHER person's full name, the denial and the condition. "Greta Holm has never worked on baby formula." in somebody else's call note is set aside; so is a quote whose words are not in the document. Nothing else is — the reader keeps by default. "Greta Holm confirmed that Hallgerd has never worked on baby formula.", "Greta Holm said she never touched baby formula.", "Her mentor has never worked on baby formula." and "Tom Brandt and Greta Holm never sold baby formula." are each a doubt: a sentence somebody else leads may still REPORT the person's own denial, and no list of the nouns and verbs such a sentence can hold is ever finished.

What each does to the person:

  • A firm denial with nothing against it is a none — the reader's one confident "no".
  • A firm denial beside ANY document of the person that states what it denies is a conflict, never a none. The other document need not be a match: a profile that states the product and is short of a proved length, beside a call note that denies the product, is a conflict — that is the one conflict of the lists above.
  • A doubt beside support is a conflict, never a match. A doubt alone leaves the person undetermined, never none.

IN_DOUBT holds such a person — a profile that states the whole niche, and a note of a screening call that says "The role is broader than the profile says. She has never worked on baby formula herself.":

from docs.examples.niche import FACETS, NICHE, search

read = search(client, IN_DOUBT, criteria=[{"text": NICHE, "facets": FACETS}])["criteria"][0]
person = next(p for p in read["undetermined"] if p["entity_id"] == "ingrid_valen")
print(person["entity_id"], "-", person["reason"], "| matched:", [p["entity_id"] for p in read["matched"]])
for quote in person["evidence"]:
    print("   ", quote["facet"] + ":", repr(quote["text"]), "|", quote.get("kept_as", "supports"))
print("denials:", [(denial["facet"], denial["text"]) for denial in person["denials"]])
ingrid_valen - conflict | matched: []
    function: 'led marketing for baby formula' | supports
    product: 'baby formula' | supports
    market: 'across Norway and Sweden' | supports
    duration: 'for more than 20 years' | supports
    product: 'She has never worked on baby formula herself.' | doubt
denials: [('product', 'She has never worked on baby formula herself.')]

Whose "she" is that? The reader does not decide: a "she" proves nobody, whatever stands before it. So the sentence is no "no" — and it is not dropped either: her profile alone would be a match, and she is not delivered as one while her own call note may deny the product. Each doubt is in evidence (kept_as: "doubt") when the person is a conflict, and under denials on whichever list they are. What is kept as a doubt:

  • A sentence a pronoun leads, whoever it may mean ("She has never worked on …", "He has never …").
  • A sentence somebody else leads, whatever it holds — a named third party's report ("Greta Holm confirmed that Hallgerd has never …", "… said …", "… called back to say …"), a possessive and its noun ("Her mentor has never …"), a firm's name. The one exception is the form set aside above.
  • A denial the reading model files under a key your criterion does not have — the condition's words in place of its key, a capital, a plural, nothing at all. It is held against the key it matches when case is ignored, else against the condition the key or the sentence names, else against the first — and kept as a doubt, with its sentence.
  • A sentence a thing leads, a fragment, a heading ("The role has never included baby formula.", "Never worked on baby formula.", "Baby formula is outside her experience.").
  • A denial in words the reader holds no rule for ("She has yet to work on baby formula.", "She has only ever worked on pet food."). The reader does not try to recognise a denial among the sentences a model lists: a list of the ways a sentence can deny a thing is never finished.
  • The person's own named sentence where the denial is not outright — it states a limit ("At another employer, Ingrid never worked on …"), or denies the very words one of their documents states ("has never worked in Germany or France" in a call note, beside a profile that says "across Germany and France").
  • An answer, with its question. "Have you ever worked on baby formula yourself?" / "No, never." is kept as the two sentences together — either half alone says nothing — and so is a question the interviewer asks, with the answer behind it. The quote's span then covers both.
  • Quoted words that are the document's and run across two sentences ("No. I ran the cereals side.").

A third person of IN_DOUBT has a profile that states the whole niche, her twenty years in the form the reader reads, and a note of a reference call that says "Greta Holm confirmed that Hallgerd has never worked on baby formula.":

from docs.examples.niche import FACETS, NICHE, search

read = search(client, IN_DOUBT, criteria=[{"text": NICHE, "facets": FACETS}])["criteria"][0]
person = next(p for p in read["undetermined"] if p["entity_id"] == "hallgerd_skarstein")
print(person["entity_id"], "-", person["reason"])
for quote in person["evidence"]:
    if quote["facet"] == "product":
        print("   ", repr(quote["text"]), "|",
              "denies" if quote.get("denies") else quote.get("kept_as", "supports"))
hallgerd_skarstein - conflict
    'baby formula' | supports
    'Greta Holm confirmed that Hallgerd has never worked on baby formula.' | doubt

Her profile alone is a match. The report is not her own sentence, and it is not nothing: a named person says she never did the work. The reader cannot check that, so it neither makes her a "no" nor delivers her as a match — the two documents are handed to you together.

A denial of a name you accepted blocks like a denial of the condition's own words. Where you say what else counts (above), the names you accept are part of what a denial is held against. A third person of IN_DOUBT has a profile that says "she sold infant milks" and a call note that says "Oddrun has never worked on infant milks.":

from docs.examples.niche import FACETS, NICHE, search

named = [dict(facet, also=["infant milks"]) if facet["key"] == "product" else facet
         for facet in FACETS]
read = search(client, IN_DOUBT, criteria=[{"text": NICHE, "facets": named}])["criteria"][0]
person = next(p for p in read["undetermined"] if p["entity_id"] == "oddrun_aarvik")
print(person["entity_id"], "-", person["reason"], "| matched:", [p["entity_id"] for p in read["matched"]])
for quote in person["evidence"]:
    if quote["facet"] == "product":
        print("   ", repr(quote["text"]), "| the name:", quote.get("accepted_name"),
              "| checked by", quote.get("checked_by"), "| denies:", quote.get("denies", False))
oddrun_aarvik - conflict | matched: []
    'sold infant milks' | the name: infant milks | checked by accepted_name+second_call | denies: False
    'Oddrun has never worked on infant milks.' | the name: None | checked by None | denies: True

With infant milks accepted her profile states the whole niche — and her own call note denies, by her name, the very name the match would rest on: a firm denial beside a document that states what it denies. She is a conflict, and both quotes are in evidence. Three kinds of item stand in a conflict's evidence, and each says which it is: a supporting quote carries checked_by or states; a FIRM denial carries denies: true; a doubt carries kept_as: "doubt". (Sent without the name, the same note is a doubt about the product and no more — the reader does not know that infant milks is baby formula — and the sentence is under her denials.)

What it costs. A reading model lists as "exclusions" many sentences that deny nothing: one that states another value ("At another employer I was mostly on footwear"), a harmless "never" ("She has never missed a baby formula launch."), a denial with a limit its own sentence states ("Before joining she had never worked on baby formula."), a sentence about the firm ("The firm has never sold baby formula."). Each is kept, and a person who would match beside one is a conflict where a careful reader sees none. That is the price of never delivering a match beside a denial the reader dropped by mistake. Read the quotes before you rule the person in or out — they are handed to you for that.

What it cannot see. A denial the reading model does not list. The reader keeps what it is shown and judges the sentences that are quoted to it; it does not sweep a document for denials of its own — a profile that states the niche and, three paragraphs on, takes it back in a sentence the model passed over is a match. And inside the one form it sets aside, the reader takes a name on its word: "Nordic Dairy has never worked on baby formula." is read as another PERSON's denial wherever the reply names no organisation of that name, and is set aside.

How a length is counted

A length is the one condition no model confirms: the reader counts it from the profile's own words, and nothing after it checks the count. A rule that reads "whose years are these?" out of free prose has no last form, and each form it gets wrong is a confident wrong match. So the granting side is a WHITELIST: a length is granted only by ONE form of sentence that passes every one of six checks — each says what lets a sentence THROUGH — and every other length the reader is shown it hands on to you, as a doubt, with the sentence and the name of the check it failed.

In one paragraph — the words the response field, the MCP tool and the SDK say it in (the rest of this section and the one before are that paragraph at length):

A LENGTH IS GRANTED only by one form of sentence: the person's given name (or full name; "I" only where the text is proved to be their own voice), then "has done this work for N years" (or "since ", "from to ") and nothing more, in plain running text, directly behind a sentence of that experience, and no longer than the experience's stated start allows. EVERY OTHER sentence the reading model reports as a length of an experience that establishes at least one condition — and every other sentence that points at such an experience's work ("this role", "that work") and writes a time — is HANDED ON, never read: missing_because says length_not_proved, and the entry lists it under lengths_not_proved — the whole sentence with its span, years (the count its words write; absent where nothing bounds it), bound, kept_as: "doubt" and why, closed words: not_in_the_closed_form, more_than_the_closed_form, behind_a_pronoun, name_not_proved, first_person_not_proved, subject_not_proved, subject_not_named, pointer_not_proved, not_running_text, contradicts_its_start, outside_the_experience, count_not_bounded, dates_not_usable, the_careers, of_an_earlier_time, organisation_not_proved. Quote it and say the length is stated but unverified — never "matched", never "no length". duration_unknown means what it says: no such sentence was reported or found (one that another person's full name leads, or that is about another organisation, is not handed on as theirs). In evidence, an item with denies: true is the person's own outright denial; one with kept_as: "doubt" may deny the condition and proves nothing; every other item supports.

The one form read. The whole sentence is

<the person>  [has | have | had]  done | did | held | been in | been doing |
              been holding | was in | worked in | served in
<this>  [kind of]  work | role | job | position | post | capacity | remit
              [at | with | for <the experience's own employer>]
for <ONE count of years>  |  since <a date>  |  from <a date> to <a date>

and nothing else — no second clause, no aside, no question. "Greta has done this work for more than 20 years." is read; so are "Anneke has held this role at Halden Foods AS for more than 20 years.", "Anneke did this work from March 2001 to May 2023." and, in a dated document, "Anneke has held this role since March 2001." The six checks, each with the word a sentence that fails it is handed on with:

  1. Who leads it (name_not_proved, behind_a_pronoun, subject_not_proved). The person's GIVEN name — alone, or followed by the rest of the name as the document writes it ("Anneke", "Anneke Voss", "Anne-Marie", "Siobhan O'Brien", "Oscar P. Harper") — or the FULL name behind a title ("Dr. Anneke Voss"). Letters are compared with their accents folded: "Soren Odegard" is Søren Ødegård. Never the family name alone ("Voss has done this work …"), never a title before the family name alone ("Dr. Voss …"), never an initial in the place of the given name ("A. Voss …", "A.P. Voss …"), and never a part of the name that the same document also writes in a name that is not the person's — "Anneke has done this work …" in a profile that names an Anneke Brandt, "Voss …" beside a Henrik Voss. A "she", a "he" or a "they" is never proof of the person, whatever stands before it. Somebody else's full name is somebody else: "Greta Holm has done this work for more than 20 years." gives the person of the profile nothing.
  2. Whose "I" (first_person_not_proved). The first person is the person's own only in a turn labelled with their name ("Margaux: I have …"; or, in a transcript whose turns are stated, the one label nobody stated to be somebody else's) — or, in a document without turns, where the sentence that opens the experience is itself written in the first person. Never by a count of pronouns: "I held this role for 22 years." in a reference about the person is the WRITER's.
  3. What it points at (pointer_not_proved). "This work", "this role", "this position", "this job" — one piece of work, the one at hand: never "that …", "these …", "those …", "the same …". The sentence stands among the sentences of ONE experience, no second experience opens in the same sentence, and the sentence right before it is that experience's own: the one that opens it, one a condition of yours is quoted from, or one that names its employer.
  4. Where it stands (not_running_text). Plain running text: not behind a quote mark ("> …"), not behind a bracketed mark that is no time stamp ("[REJECTED] …", "[ ] …"), not an item under a line that ends in a colon ("Claims the reference check rejected:"), and not a sentence the next one points back at ("That line was an error.").
  5. What it says (more_than_the_closed_form). The form above and nothing more — and no negator inside the count ("for not nearly 20 years").
  6. When the work began (contradicts_its_start). Where the reading model reports a start for the experience, written in one of the experience's own sentences ("joined … in 2021"), a length longer than the time from then to the document's own date (today, where it carries none) — or to the end your record gives the role, with a record_check — is not that experience's: twenty years of something begun three years ago.

And the count must bound the length from below: "for more than 20 years", "for 22 years", "since 2001" against the document's own date. A count that states no minimum bounds nothing ("nearly 20 years", "a couple of years", "18 months"); neither does an open period in a document with no date, or "had held this role since 1999" (open until when?).

Every other sentence that states a length is handed on — with a closed word for why. The duration's reason in missing_because is then length_not_proved, and the entry carries lengths_not_proved: one item per sentence, with the closed words for why the reader did not take it, the count the sentence writes, and the sentence whole. Three people of PEOPLE, one for each way a true length is commonly written:

from docs.examples.niche import EUROPE, NICHE, search

read = search(client, PEOPLE, constraints=EUROPE, criteria=[{"text": NICHE}])["criteria"][0]
for who in ("kasper_vandenberg", "cornelia_vantongeren", "margaux_delacroix"):
    person = next(p for p in read["undetermined"] if p["entity_id"] == who)
    print(who, "| short of:", person["missing_because"]["duration"])
    for length in person["lengths_not_proved"]:
        print("   ", length["why"], "|", length["bound"], length["years"], "|", length["kept_as"])
        print("   ", repr(length["text"]))
kasper_vandenberg | short of: length_not_proved
    ['not_in_the_closed_form'] | at_least 20.0 | doubt
    'Kasper Vandenberg sold baby formula for Brightwell Nutrition across Belgium and France for more than 20 years.'
cornelia_vantongeren | short of: length_not_proved
    ['behind_a_pronoun'] | at_least 20.0 | doubt
    'She has done this work for more than 20 years.'
margaux_delacroix | short of: length_not_proved
    ['not_in_the_closed_form'] | period 22.25 | doubt
    '[00:00:12] Margaux: I have led marketing for baby formula at Verdane Nutrition across France and Belgium since 2001.'

And two people of IN_DOUBT whose sentence IS the form above and fails the first two checks — a profile that names her father, where the family name alone leads the sentence; and a reference somebody else wrote about her, in the first person:

from docs.examples.niche import FACETS, NICHE, search

read = search(client, IN_DOUBT, criteria=[{"text": NICHE, "facets": FACETS}])["criteria"][0]
for who in ("gunnhild_rosvoll", "aslaug_tveiten"):
    person = next(p for p in read["undetermined"] if p["entity_id"] == who)
    (length,) = person["lengths_not_proved"]
    print(who, "| holds:", person["established"], "| short of:", person["missing_because"])
    print("   ", length["why"], "|", length["bound"], length["years"], "|", repr(length["text"]))
print("matched:", [p["entity_id"] for p in read["matched"]])
gunnhild_rosvoll | holds: ['function', 'product', 'market'] | short of: {'duration': 'length_not_proved'}
    ['name_not_proved'] | at_least 20.0 | 'Rosvoll has done this work for more than 20 years.'
aslaug_tveiten | holds: ['function', 'product', 'market'] | short of: {'duration': 'length_not_proved'}
    ['first_person_not_proved'] | exactly 22.0 | 'I held this role for 22 years.'
matched: []

"Her father Henrik Rosvoll ran the dairy until 2021. … Rosvoll has done this work for more than 20 years." — whose twenty years? "I held this role for 22 years." in a reference "written by her former manager" — whose twenty-two? The reader does not guess. Each of the two holds every condition and is one proved length short, and each is handed the sentence.

The closed words of why — a sentence carries every one that applies:

why The sentence … For example
not_in_the_closed_form states a length in another wording: a count beside the work's own words, or dates. Most true lengths a person writes are this — and so are the lengths that are NOT the experience's; the reader does not try to tell the two apart. "… sold baby formula … for more than 20 years", "… has spent 22 years leading …", "Since 2001, …"
more_than_the_closed_form points at the work and says more than the form allows: a second clause, an aside, another unit, a label, a negator inside the count. "… has done this work for 22 years and holds an MBA.", "Years in this role: 22.", "… for 264 months", "… came to this role after 20 years in sales."
behind_a_pronoun is the form but for its subject: a "she", a "he", a "they" that may be the person. The reader finds these itself, whether or not the model reported them. "She has done this work for more than 20 years."
name_not_proved is led by a name that holds a part of the person's and does not prove them (check 1). "Rosvoll has done this work …", "Dr. Rosvoll …", "G. Rosvoll …"
first_person_not_proved is led by "I" in a text not proved to be the person's own voice (check 2). a reference's "I held this role for 22 years."
subject_not_proved is led by nobody the reader knows: no subject at all, a thing, a possessive and its noun. "• Led marketing for baby formula … for 22 years.", "Tenure: 22 years.", "Her predecessor has done this work …"
subject_not_named stands in a document whose person is kept under no name the reader can use (below): said in place of the word above. any sentence a name leads, in such a document
pointer_not_proved does not prove WHICH work "this work" is (check 3). "… did that work for …", "… has held these roles for …", the form behind two lines that are not the role's own
not_running_text does not stand as plain running text (check 4). "> Anneke has done this work …", "[REJECTED] Anneke has done this work …"
contradicts_its_start writes a length longer than the time since the experience began (check 6). "… has done this work for more than 20 years." of a role the reply dates from 2021
outside_the_experience was given by the reading model to an experience it neither stands among the sentences of nor names the employer of. the form seven sentences below the role, behind other matters
count_not_bounded writes a count the reader cannot bound below in years. "for nearly 23 years", "for 18 months", "a couple of years"
dates_not_usable writes dates the reader would read and cannot use. "has held this role since 2019" in a document with no date
the_careers states a career's total, which no one experience is given. "has 22 years of experience in consumer goods"
of_an_earlier_time states years of what came before the experience. "moved there after 22 years in banking"
organisation_not_proved gives the time to a shorter or a longer name of the experience's employer — an organisation the reader cannot prove to be that employer and does not know to be another. "has spent 22 years at Qvistmark" of work at "Qvistmark Foods AS"

(One more exists and is never said by this deployment: not_where_the_work_is_stated, the word of a rule that reads a count in free prose, which is switched off.)

  • years and bound are the count the sentence writes, as the reader would have counted it had it taken the sentence: at_least ("more than 20 years"), exactly ("22 years"), about ("about 22 years": two fewer), or period — the months between the dates it writes, an open end counted to the document's own date. years is absent where nothing bounds the count ("nearly 23 years", "18 months"). They are a reading aid, never evidence: no row of the judgment holds them as a length.
  • kept_as: "doubt", like a denial the reader could not attribute. It never makes a match and never a "no". Report such a person as states a length the reader could not prove to be this experience's, with the sentence — not as "under twenty years", and not as "unknown".

What is NOT handed on. duration_unknown means what it says: no document states a length for an experience that establishes one of your conditions. Four things a document may write are therefore not handed on as the person's:

  • a sentence ANOTHER person's full name leads ("Greta Holm has done this work for more than 20 years.") — the reader positively knows whose years those are;
  • years at ANOTHER organisation ("…, having spent 22 years at another employer", "at a pet food company") — another experience's employer, an organisation the sentence leaves unnamed, a name that is no form of this employer's;
  • a length stated for an experience that establishes NONE of your conditions — the twenty years of a logistics career in a profile that never mentions the product, the market or the function you asked for is no near-miss of your niche;
  • an interviewer's sentence, and one that writes no time at all.

Two profiles of ON_RECORD show the difference:

from docs.examples.niche import FACETS, NICHE, search

read = search(client, ON_RECORD, criteria=[{"text": NICHE, "facets": FACETS}])["criteria"][0]
for who in ("solveig_aaberg", "torunn_holmen"):
    person = next(p for p in read["undetermined"] if p["entity_id"] == who)
    print(who, "| holds:", person["established"], "| short of:", person["missing_because"])
    print("    handed on:", [(length["why"], length["text"])
                             for length in person.get("lengths_not_proved", [])])
solveig_aaberg | holds: ['function', 'product', 'market'] | short of: {'duration': 'duration_unknown'}
    handed on: []
torunn_holmen | holds: ['function', 'product', 'market'] | short of: {'duration': 'length_not_proved'}
    handed on: [(['behind_a_pronoun', 'pointer_not_proved'], 'She did this work for more than 20 years.')]

The first profile says she led the work at one employer "…, having spent 22 years at" another: the reader knows whose years those are, and they are not this experience's. The second says "The position was previously held by someone else. She did this work for more than 20 years." — the closed form behind a "she" that may be her predecessor, and behind a sentence that is not the role's own. The reader does not guess; it hands the sentence on with both words.

A person kept under no name. Who "the person" is comes from the name a load recorded for them — a connect of a document-store export records each person's display name, and that name then serves every document of the person, whichever verb registered it — or, where none was recorded, from what the reading model quotes as the document's subject, and only where that quote IS a name: a few capitalised words and particles, no digit, no sentence punctuation, none of the words the reply states for your conditions. A whole sentence, a header line or a label is no name. In such a document nothing is granted by name — the reader cannot say whose name leads a sentence — and every length it writes is handed on, with subject_not_named where a name leads it. A document that opens with the person's name, or a load that records it, and this does not arise.

What it costs. Every true length written outside the one form is withheld — on profiles written by people that is most of them: "… led marketing for baby formula across Germany and France for 22 years.", "For 22 years, she led …", "… has spent 22 years leading …", "since 2001" in a sentence that states the work, and her own "She has done this work for more than 20 years." in a profile that names her once. So is the form itself where a check fails on a true length: her own sentence two lines below what she does ("She manages a budget of five million. She holds an MBA from INSEAD. Anneke has done this work for more than 20 years."), and her own sentence led by her family name or by a title before it — ten of thirty-six ways a document writes a person's own name are not read. None of these is lost: each is handed on, with its word. But none is a match, and a search that needs them matched needs a reader of the handed-on sentences after it — you, or your agent.

What it cannot see. The checks hold what the reader can check, and four things are still taken on the reading model's word or on the sentence's own:

  • An experience the reading model does not list. "This work" is the experience whose sentences the pointing sentence follows — as the model listed them. Where one sentence opens two careers ("… leads marketing for baby formula at Halden Foods AS, having started there in 2021 after a career as a lab technician. Anneke did this job for more than 20 years.") and the model lists one experience, there is one for "this job" to be.
  • A start nobody reported. Check 6 reads a start only from a period the reading model reports for the experience; it reads no prose. "In 2021 she moved to Halden Foods AS to lead … . Anneke has done this work for more than 20 years." in a reply that lists no period is granted.
  • Other work at the same employer. A sentence that names the employer and describes something else ("At Halden Foods AS, Anneke also coaches the staff football team. Anneke has done this work for 25 years.") passes for the experience's own, and the 25 years go to the role.
  • A given name the document writes for nobody else. "Anneke" is the person wherever no other name in the document holds it. A namesake the document mentions by given name alone is the person, to the reader.

The last of these holds for a firm "no" too, which is held to the first of the checks. A profile that tells two careers in one breath, or that dates a role in prose, is worth reading yourself.

And one thing is not seen in the OTHER direction: a length the reading model does not report, written in a sentence that does not point at the work ("… and has run the brand for 22 years" in a paragraph the model passed over). Such a profile reads duration_unknown, exactly like one that states no length — duration_unknown means that no length was reported or found, which is what the reader can know.

A length from the record

A profile often states what a person did at an employer and not for how long — while the role's dates sit in the record you connected. The reader has a step that reads such a length from those dates, and it is switched off. A role's dates are a fact about the role; whether they are the length of the work your niche describes is a judgment, and the step was found to make it wrongly in ways nothing in the reply showed — a short length the profile states in words the reader does not know was overruled by a long role, and a row was counted by a title the person held at another time. A confident wrong match is the one error this verb must not make, so the step stays off until it passes verification switched on. No request can turn it on.

Sending record_check is still accepted — it is never a 422 for that — and the reply says what the check was read for:

from docs.examples.niche import FACETS, NICHE, search

check = {"relation": "held_role", "party_slot": "company", "label_slot": "title"}
read = search(client, ON_RECORD, criteria=[
    {"text": NICHE, "facets": FACETS, "record_check": check}])["criteria"][0]
print("status:", read["status"], "| matched:", [person["entity_id"] for person in read["matched"]])
print("read for a length:", read["record_check"]["length"])
print("read for an open period:", read["record_check"]["open_period"])
print("one condition short:", [(person["entity_id"], person["missing_because"])
                               for person in read["undetermined"] if "duration" in person["missing_because"]])
without = search(client, ON_RECORD, criteria=[{"text": NICHE, "facets": FACETS}])["criteria"][0]
print("without the check:", without["record_check"], "|",
      [person["entity_id"] for person in without["undetermined"] if "duration" in person["missing_because"]])
status: complete | matched: []
read for a length: {'consulted': False, 'why': 'switched_off'}
read for an open period: {'consulted': True, 'why': None}
one condition short: [('henriette_lindqvist', {'duration': 'duration_unknown'}), ('leontine_aubrecht', {'duration': 'duration_unknown'}), ('marianne_solvik', {'duration': 'length_not_proved'}), ('solveig_aaberg', {'duration': 'duration_unknown'}), ('torunn_holmen', {'duration': 'length_not_proved'})]
without the check: None | ['henriette_lindqvist', 'leontine_aubrecht', 'marianne_solvik', 'solveig_aaberg', 'torunn_holmen']

The first three of these people have roles of twenty years and more on record at the employer their profile names (the other two are the pair of the section before). None of the profiles states a length the reader takes, so each is one condition short — with the check and without it, and for the profile's own reason: duration_unknown where it states none, or length_not_proved for the two whose sentence is handed on (one of the three says "… did this work for 18 months before moving to finance.": a length, in words that say more than the form — and a 25-year role on record does not overrule it). What you are told, and what the check still does:

  • record_check on the block says what was read. The check as you stated it, and two entries: length — consulted: false with why: "switched_off" (or "no_duration_facet" when your criterion asks for no length) — and open_period, consulted: true. It is null when the criterion states no check. On a not_run reply it echoes the check you sent, consulted for nothing (open_period: consulted: false, why: "not_run").
  • Nobody is matched on a record's dates. No entry carries rests_on_record, and no evidence says checked_by: record or prints years_recorded. A judgment stored by a reader that had the step on is ANOTHER READER's judgment — the step is one of the switches a reader's identity is made of — so none of them is served: each counts as unread (read_by_another_reader) and is read again, once, by the next search that may read (asking again).
  • What the check is read for: an open period. "Currently", "since 2019" and "to present" are the profile's claim. With record_check, an open period at an employer whose every recorded role has ENDED makes nothing current, and is counted no further than that end. The record only ever shortens what the profile dates: a profile that carries no date dates nothing, and the record's end is not lent to it.
  • A person the check withholds is told so. Where the closure is what leaves a condition open — the period would have held as the profile writes it — the entry's missing_because says record_says_ended for that condition, and closed_by_record names the record row and the month it ended. That is what a "currently" the record closed says, and a length read from a closed pointing sentence that writes "since". A "since 2001" in any other sentence gives no length with or without the record (how a length is counted); it is handed on under lengths_not_proved, and the closure is said THERE: the item carries closed_by_record, and its years stop at the record's end (below).
  • "The record" is what you connected. Only rows an export DECLARED are read. A row a model extracted from a document is never the record.
  • A record with a known hole is no witness. Where the loader withheld a row of the relation for the person, the rows that are held close nothing.
  • label_slot is accepted and not used. It names the slot that tells several roles at one employer apart, which only the switched-off step reads. It is validated, and it is part of the criterion's identity when you state it — so the same niche sent with it and without it is two addresses, each read in full, for the same verdicts. Leave it out.
  • To ask about a length today, the profile has to state it — in the one form the reader takes a length from ("Greta has done this work for more than 20 years."), or in a sentence that is handed on for you to judge. What the record holds about a role's dates is still yours to ask for as what it is: a constraint on the relation (search), which reads the role's dates and claims nothing about the niche.

What a person the record withholds is told

One profile of IN_DOUBT says "Since 2001, Maren has led marketing for baby formula …"; her record says the role at that employer ended in May 2015. Asked whether she does the work CURRENTLY (as of June 2024), and asked for twenty years of it:

from docs.examples.niche import FACETS, NICHE, search

check = {"relation": "held_role", "party_slot": "company"}
current = {"text": "currently marketing baby formula in European markets", "facets": [
    {"key": "recency", "kind": "recency", "text": "currently", "current": True}] + FACETS[1:]}

def maren(criterion, **more):
    read = search(client, IN_DOUBT, criteria=[criterion], **more)["criteria"][0]
    return next((p for p in read["undetermined"] if p["entity_id"] == "maren_hauge"), None)

person = maren({**current, "record_check": check}, as_of="2024-06")
print("currently | short of:", person["missing_because"],
      "| closed by the record at:", person["closed_by_record"]["end"],
      "| names the row:", "row" in person["closed_by_record"])
print("currently, without the check | a match:", maren(current, as_of="2024-06") is None)
person = maren({"text": NICHE, "facets": FACETS, "record_check": check})
(length,) = person["lengths_not_proved"]
print("20+ years | short of:", person["missing_because"], "|", length["why"], "|",
      length["years"], "years, counted to the record's end:", length["closed_by_record"]["end"])
(without,) = maren({"text": NICHE, "facets": FACETS})["lengths_not_proved"]
print("20+ years, without the check |", without["years"], "years | closed by the record:",
      "closed_by_record" in without)
currently | short of: {'recency': 'record_says_ended'} | closed by the record at: 2015-05 | names the row: True
currently, without the check | a match: True
20+ years | short of: {'duration': 'length_not_proved'} | ['not_in_the_closed_form'] | 13.42 years, counted to the record's end: 2015-05
20+ years, without the check | 22.25 years | closed by the record: False

Without the check her "since 2001" makes the role current, and she matches the first brief. With it, the entry says the record closed it: record_says_ended, and closed_by_record with the row and the month. Her LENGTH is another matter: "Since 2001, Maren has led …" is not the form the reader takes a length from, so the second brief never matched her — and the sentence is handed on either way. What the check changes there is what the handed-on item says: thirteen years to the record's end, with the row, instead of the twenty-two the sentence seems to write.

A closure means one of two things, and the reply cannot tell you which: the profile is stale, or the RECORD IS INCOMPLETE — a record that holds only an old internship at that employer closes a true current role in exactly this way. Report such a person as withheld by the record, with the row and its end; never as "does not qualify". Where the closure changes nothing — the profile's own dates fall short with or without it — the reason is the profile's own (duration_below_threshold) and no row is named. And when the record is connected again and the closing row is gone, or a role has been added, the judgment is read again by the next search that may read.

What each status means

status What happened What to do
complete Every candidate document holds a judgment, or a recorded failure. Use it.
partial The read is not finished; status_reason says why (time_budget: the call returned first, and background.reading says whether the rest is being read without it; spend_cap, model_unavailable, read_failures). Call again with the same constraints and the address: while background.reading is true the call waits for the read and starts nothing.
over_budget The plan was above the budget: NOTHING was read. estimate prices it. Add a constraint, or raise criteria_budget.
over_budget_during_read The estimate proved low: reading stopped at the read that crossed max_usd. What was read stands; raise the budget to continue.
not_run Nothing was read; status_reason says why. See below.

The not_run reasons an agent meets:

status_reason What it means What to do
reuse_only_criterion_unknown Reuse ("max_reads": 0) was asked of a criterion never read. Send it with a budget that allows reads.
decomposition_incomplete The reader could not show that a model's split covers the whole text and keeps its conditions apart. Nothing was read. Send facets plus ONE reading of each suggested piece, or reword (above).
decomposition_failed The split's call failed, or its reply was not a split a criterion can be built from. Send the text again, or state the facets yourself.
decomposition_timeout The split did not return inside the call's time. The same.
ordinary_constraint_not_bound A constraint did not run, so who to read is unknown. Fix what gaps names.
no_candidates_in_relation_space No constraint was sent and no relation knows the people the documents are about. Connect a record of them, or constrain.
provider_refused The model refused every read of this call with one error. What earlier calls read still stands in counts; say so and do not loop.
model_unavailable No model is available to the reader in this deployment. The same; an operator must configure one.

Whatever the status, counts describes the candidates as they stand now, and coverage.narrowed_by at the top of the response names what keeps the answer from being complete: criterion_silent:<address> and criterion_conflict:<address> (people the documents do not settle), criterion_not_read:<address> (documents still unread), criterion_partially_decomposed:<address> (your own facets leave words of the text to nobody — unchecked_words), and criterion_documents_outside_universe:<address> (documents are about people no relation knows, so they were never candidates).

What a match licenses, and what it does not

  • It licenses: one document registered as this person's states every condition of the niche in the quoted words, among the sentences of one experience — which you can re-read from the stored document — and states its length in the one sentence the reader grants a length from, passing every check of how a length is counted.
  • It does not license "nothing in their documents says otherwise". A denial is seen only where the reading model lists it, and "this work" is the experience as the model listed it. Both sections end with what the reader cannot see.
  • It does not license "the others do not qualify". Someone whose documents state a condition in words the reader does not recognise is undetermined, with the reason; someone with no document is not_recorded. Only none is a no, and it takes a quoted denial proved the person's own that none of their documents opposes.
  • The matched list is the PROVED part of the answer, not the whole of it. A length is proved in one form of sentence only (how a length is counted), so a person whose profile writes "… for 22 years" any other way — or behind "she" — is undetermined with length_not_proved, and the sentence under lengths_not_proved. On profiles written by people that is most true lengths. A complete answer to a brief with a length in it is the matches PLUS the handed-on sentences, read.
  • It does not check words the facets leave out. unchecked_words — on the block and on every match — lists the words of your text that no facet took and that are more than framing. Two kinds of word are NOT listed there and are not checked either: framing ("Experts with", "people who have"), and a lead-in a facet carries (carried on the facet: who led). A match is a match for the facets, as echoed.
  • It does not know a profile is out of date. "Currently" in a biography is the biography's claim. To hold it against the role history you loaded, send record_check naming the relation that records the role (what the check is read for); without it the block says recency_unchecked_against_record.
  • It does not take a length from your record. A person whose profile states no length stays one condition short, whatever dates their roles carry: that step is switched off (a length from the record). Two more things to know before you send one. The criterion's text is workspace metadata, visible to every key of the workspace: write the condition, never who is asking. And the refusals are 422s that read nothing — two criteria in one search, a malformed criterion, an address the workspace does not hold, a budget above the deployment's limits, a facet that does not hold against the text (criterion_facets_invalid, each fault named <key>:<what> — here a length that is no minimum, and one that is not counted in years):

from docs.examples.niche import EUROPE, NICHE

def refusal(**body):
    r = client.post("/v1/public/search", json={"workspace_id": PEOPLE, "constraints": EUROPE, **body})
    return r.status_code, r.json()["detail"]["type"]

print(refusal(criteria=[{"text": NICHE}, {"text": "ten years of harbour pilotage"}]))
print(refusal(criteria=[{"address": "crit_0000000000000000"}]))
print(refusal(criteria=[{"text": NICHE}], criteria_budget={"max_reads": 1_000_000}))

def length_fault(words, years):
    r = client.post("/v1/public/search", json={"workspace_id": PEOPLE, "criteria": [{
        "text": words + " marketing baby formula", "facets": [
            {"key": "duration", "kind": "duration", "text": words, "min_years": years},
            {"key": "function", "kind": "condition", "text": "marketing"},
            {"key": "product", "kind": "condition", "text": "baby formula"}]}]})
    return r.status_code, r.json()["detail"]["type"], r.json()["detail"]["faults"]

print(length_fault("less than 5 years", 5))
print(length_fault("18 months", 18))
(422, 'too_many_criteria')
(422, 'criterion_unknown')
(422, 'criteria_budget_invalid')
(422, 'criterion_facets_invalid', ['duration:min_years_is_no_minimum'])
(422, 'criterion_facets_invalid', ['duration:min_years_not_in_years'])

Where the documents come from

A criterion reads documents that are registered as being ABOUT an entity: POST /v1/public/ingest with entity_name and a document_kind the workspace's pack declares (document kinds), or a connect of a document-store export whose mapping lists the prose fields (documents[] — a biography, each call's text; that load registers the text and calls no model — see operating the connect path). A document is read for a criterion when a search first needs it, and once per criterion.

Over MCP the tool is limma_search with the same fields (criteria, criteria_budget, list_limit); the Python SDK's LimmaClient.search takes them as keyword arguments.