Skip to content

Search: a brief to a closed set

"Experts who held a VP-level Sales role in DACH and passed a compliance screening" is not a ranking question. It names a closed set: every entity for which the store holds rows satisfying each condition. POST /v1/public/search (over MCP, limma_search) takes that request — as a plain-language brief, or as the constraints it compiles to — runs it server-side over the relations, and returns the set with the denominator that makes these are all of them sayable, or names why it is not.

Five stages, each visible in the response:

  1. compile — a brief becomes constraints, each carrying the words of the brief it came from (one schema-constrained model call; skipped when you send constraints);
  2. expansion — each constraint through the workspace vocabulary;
  3. enumeration — every relation the constraints name, read over the same scoped readers as limma_facts, intersected per entity;
  4. ranking — deterministic, over slots, no model;
  5. the warrant — matches with their rows and receipts, counts per constraint, None, Not recorded, the gaps, the printed expansion, and the completeness licence.

A brief may also name a condition no relation can express — twenty years marketing baby formula in European markets. That part goes in criteria and is read from each candidate's own documents, with the sentences that say so: criteria.

A question about an idea rather than a thing — where do we discuss pricing risk? — goes in idea, and is answered over the parts of the documents: ideas.

The examples run against roster(client): five people, six held roles from a warehouse export, and three compliance screenings. Every name is invented.

Sending constraints

A constraint is {relation, slot, op, value, brief_span, required} — one condition on one slot of one relation, in the relation grammar. This needs no model:

from docs.examples.roster import roster
ws = roster(client).workspace_id
BRIEF = [
    {"relation": "held_role", "slot": "seniority", "op": "gte", "value": "VP"},
    {"relation": "held_role", "slot": "function", "op": "eq", "value": "Sales"},
    {"relation": "held_role", "slot": "geography", "op": "eq", "value": "DACH"},
    {"relation": "screening", "slot": "kind", "op": "eq", "value": "compliance"},
    {"relation": "screening", "slot": "outcome", "op": "eq", "value": "passed"},
]
r = client.post("/v1/public/search", json={"workspace_id": ws, "constraints": BRIEF})
body = r.json()
print("bound:", body["bound"], "| ranking:", body["ranking"])
print("matched:", body["entities"]["matched"])
counts = {k: v for k, v in body["counts"].items() if k != "per_constraint"}
print("counts:", counts)
print("entities:", {k: v for k, v in body["entities"].items() if k != "matched"})
print("coverage:", body["coverage"])
bound: True | ranking: deterministic: optional-groups, threshold-distance, entity_id
matched: ['halvard_quist']
counts: {'universe': 5, 'matched': 1, 'none': 3, 'not_recorded': 1, 'undetermined': 0, 'bound': 4}
entities: {'none': ['isolde_marbrook', 'odile_fenwright', 'teodor_brask'], 'not_recorded': ['yusuf_alderane'], 'undetermined': []}
coverage: {'completeness_licensed': False, 'withheld_because': 'narrowed', 'narrowed_by': ['entities_not_recorded']}

One entity satisfies every condition. The other four are not a single "no": three are none and one is not recorded, and the difference is the whole point of the verb.

Reading the warrant

The partition

Every entity the relation space knows (counts.universe, entities of the relations' subject kind in your scope) is in exactly one of four sets:

set meaning licenses
matched a row satisfies every required condition this entity matches, with the rows to show it
none some required condition's relation was looked at for the entity and no row satisfies it no recorded row satisfies this
not_recorded some required relation was never looked at for the entity (and none of the above) NOTHING — never "does not match", never "clean"
undetermined its only candidate rows cannot be judged (see as_of, an unrecorded slot and corrections) nothing either way

counts.bound is matched + none: the entities whose membership is determined.

Conditions on the same relation are grouped, and a group is one row's conditions: "VP-level", "Sales" and "DACH" must hold on ONE held role, not on three different roles. groups shows each group as it ran; none / not_recorded / undetermined list the entity ids per group:

from docs.examples.roster import roster
ws = roster(client).workspace_id
BRIEF = [
    {"relation": "held_role", "slot": "seniority", "op": "gte", "value": "VP"},
    {"relation": "held_role", "slot": "function", "op": "eq", "value": "Sales"},
    {"relation": "held_role", "slot": "geography", "op": "eq", "value": "DACH"},
    {"relation": "screening", "slot": "kind", "op": "eq", "value": "compliance"},
    {"relation": "screening", "slot": "outcome", "op": "eq", "value": "passed"},
]
body = client.post("/v1/public/search",
                   json={"workspace_id": ws, "constraints": BRIEF}).json()
for group in body["groups"]:
    print("group", group["index"], group["relation"], "constraints", group["constraints"],
          "| covered:", group["covered"], "| matched:", group["matched"],
          "| none:", group["none"], "| not_recorded:", group["not_recorded"])
for listing in ("none", "not_recorded"):
    for entry in body[listing]:
        print(listing, "| group", entry["group"], entry["relation"], "->", entry["entity_ids"])
group 0 held_role constraints [0, 1, 2] | covered: 5 | matched: 3 | none: 2 | not_recorded: 0
group 1 screening constraints [3, 4] | covered: 4 | matched: 2 | none: 2 | not_recorded: 1
none | group 0 held_role -> ['odile_fenwright', 'teodor_brask']
none | group 1 screening -> ['isolde_marbrook', 'teodor_brask']
not_recorded | group 0 held_role -> []
not_recorded | group 1 screening -> ['yusuf_alderane']

Read one person at a time. Isolde holds a VP-level Sales role in Switzerland but her compliance screening failed: the screening group looked at her and found no passing row, so she is none. Teodor is none on both groups — a C-level Operations role in the United States, and (the roles export says it checked) no screening at all. Yusuf holds a VP Sales role in Austria, and nothing ever looked at his screenings: he is not recorded, and the licence is withheld (narrowed_by: entities_not_recorded) because he may well have passed one.

Absence inside a group

Each group carries the relation's per-entity absence verdict — the same block limma_facts returns (None versus Not recorded). absence.none names the entities holding NO row of the relation at all, each with the coverage row that says a load looked; it is stronger than the group's none, which also counts entities whose rows exist and do not satisfy the condition:

from docs.examples.roster import roster
ws = roster(client).workspace_id
body = client.post("/v1/public/search", json={"workspace_id": ws, "constraints": [
    {"relation": "screening", "slot": "outcome", "op": "eq", "value": "passed"}]}).json()
group = body["groups"][0]
print("group none:", body["none"][0]["entity_ids"])
absence = group["absence"]
print("split:", absence["split"])
for verdict in absence["none"] + absence["not_recorded"]:
    print(verdict["entity_id"], "| kind:", verdict["kind"], "| denominator:",
          verdict["denominator"], "| declared_by:",
          [c["declared_by"] for c in verdict["coverage"]])
group none: ['isolde_marbrook', 'teodor_brask']
split: {'none': 1, 'not_recorded': 1, 'recorded': 3}
teodor_brask | kind: none | denominator: 0 | declared_by: ['source']
yusuf_alderane | kind: not_recorded | denominator: None | declared_by: []

Isolde is in the group's none (her one screening failed) but not in absence.none (a screening row exists). Teodor is in both.

Counts per constraint

counts.per_constraint[i] counts constraint i ALONE, over the same readers — which condition is doing the narrowing:

from docs.examples.roster import roster
ws = roster(client).workspace_id
BRIEF = [
    {"relation": "held_role", "slot": "seniority", "op": "gte", "value": "VP"},
    {"relation": "held_role", "slot": "function", "op": "eq", "value": "Sales"},
    {"relation": "held_role", "slot": "geography", "op": "eq", "value": "DACH"},
    {"relation": "screening", "slot": "kind", "op": "eq", "value": "compliance"},
    {"relation": "screening", "slot": "outcome", "op": "eq", "value": "passed"},
]
body = client.post("/v1/public/search",
                   json={"workspace_id": ws, "constraints": BRIEF}).json()
for entry in body["counts"]["per_constraint"]:
    c = entry["constraint"]
    print(entry["index"], c["slot"], c["op"], c["value"], "|", entry["status"],
          "| matched", entry["matched"], "| rows", entry["rows"], "| none", entry["none"],
          "| not_recorded", entry["not_recorded"])
print("expansion:")
for line in body["expansion"]:
    print("  ", line)
0 seniority gte VP | bound | matched 4 | rows 4 | none 1 | not_recorded 0
1 function eq Sales | bound | matched 3 | rows 4 | none 2 | not_recorded 0
2 geography eq DACH | bound | matched 4 | rows 5 | none 1 | not_recorded 0
3 kind eq compliance | bound | matched 3 | rows 3 | none 1 | not_recorded 1
4 outcome eq passed | bound | matched 2 | rows 2 | none 2 | not_recorded 1
expansion:
   seniority: ≥ VP → {VP, SVP, C-level} via order
   function: Sales is a term of axis function
   geography: DACH → {DACH, Germany, Austria, Switzerland} via hierarchy
   kind: compliance is a term of axis kind
   outcome: passed is a term of axis outcome

rows is the number of current rows satisfying that one condition; matched the entities holding one. An unbound constraint (below) carries nulls.

Bounding the lists

The lists of people who did NOT match grow with the directory, and an agent that reports numbers rarely needs them whole. list_limit (0–200) bounds every list of non-matches — the none / not_recorded / undetermined ids, per group and overall, and a group's absence entries. It never touches the matches, and the counts stay whole:

from docs.examples.roster import roster
ws = roster(client).workspace_id
passed = [{"relation": "screening", "slot": "outcome", "op": "eq", "value": "passed"}]
whole = client.post("/v1/public/search", json={"workspace_id": ws, "constraints": passed}).json()
bounded = client.post("/v1/public/search", json={"workspace_id": ws, "constraints": passed,
                                                 "list_limit": 0}).json()
print("unbounded:", whole["entities"]["none"], whole["entities"]["not_recorded"],
      "| list_limit in the response:", "list_limit" in whole)
print("bounded:  ", bounded["entities"]["none"], bounded["entities"]["not_recorded"],
      "| list_limit:", bounded["list_limit"])
print("matches, both:", whole["entities"]["matched"] == bounded["entities"]["matched"],
      "| counts, both:", whole["counts"] == bounded["counts"],
      "| none, counted:", bounded["none"][0]["total"])
unbounded: ['isolde_marbrook', 'teodor_brask'] ['yusuf_alderane'] | list_limit in the response: False
bounded:   [] [] | list_limit: 0
matches, both: True | counts, both: True | none, counted: 2

Unset, each such list carries up to 200 ids. Send 10 to show a few, 0 to report numbers only — and take the numbers from counts and each list's total, never from the length of a list you asked to be cut. The same bound applies to a criterion's lists (criteria).

The compact reply

The warrant above is what an integrator's code reads. An agent holds the reply in its context and re-reads it on every turn, and most of the warrant is evidence it will not quote: every row's sources and receipts, up to 200 ids per list of non-matches. detail: "compact" answers the same search as the matches, one line per condition on why each is in the set, and counts of everything else — a projection of the full reply, sized by the matches returned and never by the candidates examined. The HTTP verb's default stays "full"; the MCP tool limma_search defaults to "compact". No key of it names a domain: a workspace may hold people, companies, contracts or products, and an entry is a match (matches, close.matches) whatever it holds.

from docs.examples.roster import roster
ws = roster(client).workspace_id
passed = [{"relation": "screening", "slot": "outcome", "op": "eq", "value": "passed"}]
reply = client.post("/v1/public/search", json={"workspace_id": ws, "constraints": passed,
                                               "detail": "compact", "limit": 1}).json()
print(sorted(reply))
print("matched:", reply["matched"], "| complete:", reply["complete"],
      reply.get("incomplete_because"))
for match in reply["matches"]:
    print(match["entity_id"], match["name"], match["why"])
    print("  about:", match.get("about"))
print("others:", reply["others"])
page_2 = client.post("/v1/public/search", json={
    "workspace_id": ws, "constraints": passed, "detail": "compact", "limit": 1,
    "offset": reply["matched"]["next_offset"]}).json()
print("next page:", [p["entity_id"] for p in page_2["matches"]], page_2["matched"])
['close', 'complete', 'detail', 'incomplete_because', 'map_version', 'matched', 'matches', 'more', 'others', 'request_id']
matched: {'total': 2, 'showing': [1, 1], 'next_offset': 1} | complete: False ['entities_not_recorded: 1']
halvard_quist None [{'condition': 'screening.outcome = passed', 'value': 'passed', 'from': 'record'}]
  about: ['Austria, Director, Sales, Lumen Ridge (2019–now)', 'compliance, passed, as of 2024-02', 'Germany, VP, Vice President, Sales, Kestrel Pumps AG (2016–2018)']
others: {'ruled_out': 2, 'cannot_tell': 0, 'not_read': 1, 'no_documents': 0}
next page: ['odile_fenwright'] {'total': 2, 'showing': [2, 2], 'next_offset': None}

  • matched.total is counts.matched — the whole set, whatever the page. Page with offset (next_offset; null on the last page); limit defaults to 25 here.
  • complete is coverage.completeness_licensed, and incomplete_because the licence's own reasons, at most five, each with the count the full reply holds for it (here one person no load ever screened: nobody can say she did not pass). A page of a set nothing else narrows says truncated, with the number of matches not in this reply.
  • matches[].why is one line per condition a delivered row establishes: condition (the constraint as relation.slot op value, or a criterion's own words for a facet), value (what the row or the document states), from — record for a row loaded as stated, text for one read from a document (with the supporting quote, at most 160 characters, where it says more than value), computed for a length taken from a record's dates — and check, present only when the source no longer reads the value.
  • name is the display name the workspace recorded for the entity — what a document load's name_path read for their record, whether or not the load registered a document about them — and null where it holds none (this roster was loaded from SQL tables, which record none): it is never derived from an id. The full reply's matches[].name is the same field.
  • about says who a listed match is, whatever was asked: at most three lines (120 characters each) of what the workspace's own relations hold for it now — the row of each relation it holds most recently: a role first (whether or not it states its years), then a standing fact such as a place, then a role that ended — one line a row, in the row's own values; a term its vocabulary holds is said by the vocabulary's own label (a record that writes "Deutschland" reads "Germany", as the search matched it). It is read in one batch for the entities the reply lists, from the relations the packs declare and in the caller's document scope; an entity the workspace holds nothing for has no about. The entries of close.matches carry it too. why answers "why is this one in the set"; about answers "who is this".
  • others are counts only: ruled_out (counts.none — the one "no"), cannot_tell (counts.undetermined, less the close matches), no_documents and not_read (together counts.not_recorded). close.matches lists a criterion's candidates with exactly one condition open — nothing stated for it (because: "not_stated"), or stated in a sentence the reader did not take as proved (because: "stated_not_verified", with that sentence as quote) — whose own documents establish a condition of SUBSTANCE: a condition no vocabulary of places types (the block's facet says which axis types it). Somebody who shares only a place, a length or a time with the question is counted under cannot_tell, never listed: criteria.
  • candidates (with a criterion): how many profiles its niche is read for — everybody the ordinary conditions leave. It is what a question with a niche CHECKED; others partitions the whole directory.
  • list_limit is not read: a compact reply lists no non-match.
  • spent is present only when the call itself made model calls for a criterion — {calls, reads, cost_usd, cost_basis}, the criterion's own usage for this call (cost_usd null means it could not be priced, never that it was free). A call served from stored judgments carries none.
  • reading is present only while a criterion's read is unfinished. A criterion is read only while a call is open: the reads still running when a call returns finish behind it, and nothing new starts until the next call — so call again with the criterion address at once; waiting reads nothing.
  • map_version, last, is the workspace map's version as you may see it — the number facts carries in labels.map_version, and the full reply's map_version: it moves when a name your documents carry is added, re-spelled or decided, never because of a document you may not read. A version you hold that is still the reply's says the names you cached are still the map's. An idea search carries it too.

What it leaves out is fetched when it is wanted. more names the calls: more.entity, the same search restricted to entities with entity_ids and detail: "full", returns those entities' rows and receipts — and nothing about any other, with every count still of the whole set (more.rows is one entity's rows of a relation through facts; more.receipt a receipt through region). (A search with a criterion names one more, criterion: the same call with entity_ids: [] delivers NOBODY's rows — the full reply's criteria block with empty lists, which is the criterion's own working: its facets, its decomposition and estimate, what the people one condition short state instead.)

from docs.examples.roster import roster
ws = roster(client).workspace_id
passed = [{"relation": "screening", "slot": "outcome", "op": "eq", "value": "passed"}]
reply = client.post("/v1/public/search", json={"workspace_id": ws, "constraints": passed,
                                               "detail": "compact"}).json()
print(sorted(reply["more"]))
person = reply["matches"][-1]["entity_id"]
detail = client.post("/v1/public/search", json={
    "workspace_id": ws, "constraints": passed, "entity_ids": [person],
    "detail": "full"}).json()
print([m["entity_id"] for m in detail["matches"]] == [person],
      "| rank:", detail["matches"][0]["rank"], "| counts.matched:", detail["counts"]["matched"])
row = detail["matches"][0]["rows"][0]
print(row["relation"], row["slots"]["outcome"], "| receipt:",
      {k: row["slot_receipts"]["outcome"][k] for k in ("db_table", "db_column", "byte_verified")})
['entity', 'full', 'receipt', 'rows']
True | rank: 2 | counts.matched: 2
screening passed | receipt: {'db_table': 'screenings', 'db_column': 'outcome', 'byte_verified': True}

request_id is the call's id: its audit event (GET /v1/audit/events?request_id=…) records the facts and spans the answer rests on, though the reply did not carry them.

The rows and their receipts

matches[] is {entity_id, name, rank, rank_key, rows} in rank order, and rows is every row that satisfied a group for that entity, each marked with the groups it satisfied:

from docs.examples.roster import roster
ws = roster(client).workspace_id
BRIEF = [
    {"relation": "held_role", "slot": "seniority", "op": "gte", "value": "VP"},
    {"relation": "held_role", "slot": "function", "op": "eq", "value": "Sales"},
    {"relation": "held_role", "slot": "geography", "op": "eq", "value": "DACH"},
    {"relation": "screening", "slot": "kind", "op": "eq", "value": "compliance"},
    {"relation": "screening", "slot": "outcome", "op": "eq", "value": "passed"},
]
body = client.post("/v1/public/search",
                   json={"workspace_id": ws, "constraints": BRIEF}).json()
match = body["matches"][0]
print(match["entity_id"], "| name:", match["name"], "| rank:", match["rank"])
for row in match["rows"]:
    print(" ", row["relation"], row["slots"], "| satisfies groups", row["satisfies_groups"])
    print("    provenance_kind:", row["provenance_kind"], "| receipt:", row["receipt"],
          "| slot receipts:", sorted((s, v["db_table"], v["db_column"], v["byte_verified"])
                                     for s, v in row["slot_receipts"].items()))
print("provenance split over delivered rows:", body["provenance"])
halvard_quist | name: None | rank: 1
  held_role {'company': 'Kestrel Pumps AG', 'function': 'Sales', 'geography': 'Germany', 'seniority': 'VP', 'title': 'Vice President, Sales'} | satisfies groups [0]
    provenance_kind: declared | receipt: None | slot receipts: [('company', 'roles', 'company', True), ('function', 'roles', 'function', True), ('geography', 'roles', 'geography', True), ('seniority', 'roles', 'seniority', True), ('title', 'roles', 'title', True)]
  screening {'as_of': '2024-02', 'kind': 'compliance', 'outcome': 'passed'} | satisfies groups [1]
    provenance_kind: declared | receipt: None | slot receipts: [('as_of', 'screenings', 'screened_on', True), ('kind', 'screenings', 'kind', True), ('outcome', 'screenings', 'outcome', True)]
provenance split over delivered rows: {'declared': 2, 'extracted': 0}

  • A row loaded from a database has receipt: null and a structured receipt per slot, re-read from its cell on delivery; a row stated by a document has a span receipt that limma_region re-reads (relations).
  • provenance is {declared, extracted} over the rows delivered in matches: a declared row came from a structured load, an extracted one was read from a document — a relation row a reader recovered at ingest, or the judgment a criterion wrote when it read a profile. This roster was loaded from a database export, so every row here is declared.
  • name is null: the store holds no display name for a relation entity and never derives one from the id.

Gaps: what did not run

A constraint the workspace cannot express is unbound: it is listed with its reason, it does not run, and the search runs over the rest and withholds the licence naming it:

from docs.examples.roster import roster
ws = roster(client).workspace_id
body = client.post("/v1/public/search", json={"workspace_id": ws, "constraints": [
    {"relation": "held_role", "slot": "function", "op": "eq", "value": "Sales"},
    {"relation": "engagement", "slot": "topic", "op": "eq", "value": "pricing"},
    {"relation": "board_seat", "slot": "company", "op": "eq", "value": "Sable Point"},
]}).json()
for gap in body["gaps"]["unbound"]:
    print("unbound", gap["index"], "->", gap["reason"])
print("dropped:", body["gaps"]["dropped"], "| uncompiled:", body["gaps"]["uncompiled"])
print("matched:", body["entities"]["matched"])
print("coverage:", body["coverage"])
unbound 1 -> vocabulary_unbound:topic
unbound 2 -> relation_unknown:board_seat
dropped: [] | uncompiled: []
matched: ['halvard_quist', 'isolde_marbrook', 'yusuf_alderane']
coverage: {'completeness_licensed': False, 'withheld_because': 'narrowed', 'narrowed_by': ['constraint_unbound:1', 'constraint_unbound:2']}

vocabulary_unbound:<slot> is a term slot with no vocabulary (engagement topics are open-ended in this pack); relation_unknown:<name> a relation no pack here declares; slot_unknown:<name> a slot the relation does not declare. The three matches satisfy the one condition that ran — not the request. Tell the user each gap: that part of the request was not searched.

gaps.dropped and gaps.uncompiled are the compile's gaps and are empty on the constraints path (a brief, below). A constraint that is malformed as a request is a 422 on this path, never a gap — the request never made sense, and answering part of it would read as an answer to the whole:

from docs.examples.roster import roster
ws = roster(client).workspace_id
for constraint in (
        {"relation": "held_role", "slot": "seniority", "op": "in", "value": "VP"},
        {"relation": "engagement", "slot": "hourly_rate_usd", "op": "gte", "value": "1,5"},
        {"relation": "held_role", "slot": "company", "op": "gte", "value": "Kestrel"}):
    r = client.post("/v1/public/search",
                    json={"workspace_id": ws, "constraints": [constraint]})
    detail = r.json()["detail"]
    print(r.status_code, detail["type"], "|", detail["title"])
r = client.post("/v1/public/search", json={"workspace_id": ws, "constraints": []})
print(r.status_code, r.json()["detail"]["title"])
422 search_constraint_invalid | constraints[0] is malformed
422 search_constraint_invalid | a constraint asks its slot something its type cannot answer
422 search_constraint_invalid | a constraint asks its slot something its type cannot answer
422 `constraints` is empty

Ranking

The order is deterministic: optional-groups, threshold-distance, entity_id — no model ranks anything:

  1. optional groups satisfied, most first. required: false makes a condition a PREFERENCE: it never removes an entity from the set and never withholds the licence; it moves the entities that satisfy it up.
  2. distance from each gte / lte threshold, nearest first — one key per such constraint in request order, so months and dollars are never summed; on an ordered term axis the distance is in rungs.
  3. entity_id, ascending.

matches[].rank_key shows the key each entity was ranked by:

from docs.examples.roster import roster
ws = roster(client).workspace_id
preference = client.post("/v1/public/search", json={"workspace_id": ws, "constraints": [
    {"relation": "held_role", "slot": "function", "op": "eq", "value": "Sales"},
    {"relation": "screening", "slot": "outcome", "op": "eq", "value": "passed",
     "required": False},
]}).json()
print("preference:", [(m["rank"], m["entity_id"], m["rank_key"]["optional_groups_satisfied"])
                      for m in preference["matches"]],
      "| licensed:", preference["coverage"]["completeness_licensed"])
threshold = client.post("/v1/public/search", json={"workspace_id": ws, "constraints": [
    {"relation": "held_role", "slot": "seniority", "op": "gte", "value": "Director"}]}).json()
print("threshold:", [(m["rank"], m["entity_id"], m["rank_key"]["threshold_distance"])
                     for m in threshold["matches"]])
preference: [(1, 'halvard_quist', 1), (2, 'isolde_marbrook', 0), (3, 'yusuf_alderane', 0)] | licensed: True
threshold: [(1, 'halvard_quist', [0.0]), (2, 'odile_fenwright', [0.0]), (3, 'yusuf_alderane', [1.0]), (4, 'isolde_marbrook', [2.0]), (5, 'teodor_brask', [3.0])]

Halvard ranks first under the preference because his screening passed; Isolde's failed and Yusuf's was never recorded, but neither leaves the set, and the licence holds — a preference is not a condition. Under ≥ Director, Halvard and Odile hold a Director role (distance 0) and tie, broken by id; Teodor's C-level role is three rungs above.

limit (default 50, at most 500) caps matches only. The counts and entities.matched (ids in rank order, up to 200) always describe the whole set, and a page is not a complete answer:

from docs.examples.roster import roster
ws = roster(client).workspace_id
body = client.post("/v1/public/search", json={"workspace_id": ws, "limit": 1, "constraints": [
    {"relation": "held_role", "slot": "seniority", "op": "eq", "value": "VP"}]}).json()
print("matches on the page:", [m["entity_id"] for m in body["matches"]],
      "| truncated:", body["truncated"])
print("every match:", body["entities"]["matched"], "| counts.matched:", body["counts"]["matched"])
print(body["coverage"])
matches on the page: ['halvard_quist'] | truncated: True
every match: ['halvard_quist', 'yusuf_alderane'] | counts.matched: 2
{'completeness_licensed': False, 'withheld_because': 'truncated'}

as_of

as_of (YYYY-MM) asks the question at a month. A dated row counts when its half-open period contains the month: period_from ≤ as_of and (period_to open or period_to > as_of). Without as_of, every current row counts whatever its period.

from docs.examples.roster import roster
ws = roster(client).workspace_id
VP_IN_DACH = [
    {"relation": "held_role", "slot": "seniority", "op": "gte", "value": "VP"},
    {"relation": "held_role", "slot": "geography", "op": "eq", "value": "DACH"},
]
for month in (None, "2018-01", "2020-01"):
    body = client.post("/v1/public/search", json={
        "workspace_id": ws, "constraints": VP_IN_DACH,
        **({"as_of": month} if month else {})}).json()
    group = body["groups"][0]
    print(month, "->", body["entities"]["matched"], "| active rows:", group["active_rows"],
          "| inactive rows:", group["inactive_rows"], "| as_of_source:", body["as_of_source"])
print(body["groups"][0]["time_test"]["rule"])
None -> ['halvard_quist', 'yusuf_alderane', 'isolde_marbrook'] | active rows: 3 | inactive rows: 0 | as_of_source: None
2018-01 -> ['halvard_quist'] | active rows: 1 | inactive rows: 2 | as_of_source: request
2020-01 -> ['isolde_marbrook'] | active rows: 1 | inactive rows: 2 | as_of_source: request
dated rows: period_from ≤ 2020-01 and (period_to open or period_to > 2020-01); undated rows cannot be placed at 2020-01: the relation declares no date slot — unanswerable

In January 2018 Halvard held his VP role at Kestrel; Isolde's SVP role began in June. By January 2020 Halvard's VP role had ended (he is none — his open role is a Director one), Isolde's was held, and Yusuf's began in April.

An undated row (a screening, dated by its own as_of slot) makes no tenure claim. It counts at as_of when it is current and the relation's ONE date slot names a date whose whole span ends by the end of the month. The rule is stated per group in time_test:

from docs.examples.roster import roster
ws = roster(client).workspace_id
body = client.post("/v1/public/search", json={"workspace_id": ws, "as_of": "2024-01",
    "constraints": [{"relation": "screening", "slot": "outcome", "op": "eq",
                     "value": "passed"}]}).json()
group = body["groups"][0]
print("matched:", body["entities"]["matched"], "| none:", body["entities"]["none"])
print("undated rows:", group["undated_rows"], "| active:", group["active_rows"],
      "| inactive:", group["inactive_rows"], "| date slot:", group["time_test"]["date_slot"])
print(group["time_test"]["rule"])
matched: ['odile_fenwright'] | none: ['halvard_quist', 'isolde_marbrook', 'teodor_brask']
undated rows: 2 | active: 1 | inactive: 1 | date slot: as_of
dated rows: period_from ≤ 2024-01 and (period_to open or period_to > 2024-01); undated rows: current, and the whole of as_of ≤ 2024-01 (a as_of whose span straddles 2024-01, such as a bare year, is unanswerable)

Odile's screening (November 2023) had happened by January 2024; Halvard's (February 2024) had not, so at that month he is none. A row that cannot be placed — its relation declares no date slot or two, the value is missing, or it names a span that straddles the month (a bare year "2024" at 2024-01) — is unanswerable: an entity whose only candidate rows are unanswerable is undetermined, never none, and the licence is withheld with undated_rows_unanswerable:<relation>. The same holds for a period_from / period_to constraint over a row whose column is empty (open_period_rows_unanswerable:<relation>).

An unrecorded slot

A pack can declare a slot no load wrote: a held_role.industry over roles whose export carried no industry column. Such a role is neither in Technology nor out of it, so a row that holds NO value for a slot a constraint names — and fails no other condition of its group — cannot be judged: its entity is undetermined unless another of its rows matches, groups[].unrecorded_slots names the slot, and the licence is withheld with slot_unrecorded_rows_unanswerable:<relation>. industry eq Technology and industry ne Technology over such rows are therefore never both licensed empty. A row another condition fails on the record (an Operations role under function eq Sales) is still a non-match. The facts verb reads the same rows the same way (relations.slot_unrecorded, narrowed_by: slot_unrecorded_rows).

as_of on the search verb is a month the ROWS are tested at. It does not select a document generation: every count is over the current generation of each source (see supersession).

A brief

brief is the request in plain language, compiled server-side by ONE schema-constrained model call. Send exactly one of brief and constraints (both, or neither, is a 422). A deployment with no model answers a brief with 503 and says what to do instead:

from docs.examples.roster import roster
ws = roster(client).workspace_id
r = client.post("/v1/public/search", json={
    "workspace_id": ws, "brief": "VP-level Sales leaders in DACH."})
print(r.status_code, r.json())
r = client.post("/v1/public/search", json={
    "workspace_id": ws, "brief": "VP-level Sales leaders in DACH.",
    "constraints": [{"relation": "held_role", "slot": "seniority", "op": "gte",
                     "value": "VP"}]})
print(r.status_code, r.json()["detail"]["title"])
503 {'type': 'compile_unavailable', 'reason': 'compile requires a model; pass constraints'}
422 send exactly one of `brief` and `constraints`

Over MCP the same refusal reaches the agent as a tool error (isError: true) whose text is the verb's own body as JSON — {status, path, detail}, with detail exactly as the route wrote it — after the MCP library's Error executing tool limma_search: prefix. Match on detail.type, not on the wording. The call below goes through an MCP client session to the real tool (call_tool):

import json
from docs.examples.over_mcp import call_tool
from docs.examples.roster import roster
ws = roster(client).workspace_id
result = call_tool(client, "limma_search",
                   {"workspace_id": ws, "brief": "VP-level Sales leaders in DACH."})
prefix, _, body = result.content[0].text.partition(": ")
refusal = json.loads(body)
print("isError:", result.isError, "|", prefix)
print(refusal["status"], refusal["path"], refusal["detail"])
isError: True | Error executing tool limma_search
503 /v1/public/search {'type': 'compile_unavailable', 'reason': 'compile requires a model; pass constraints'}

There is no rule-based fallback: a guessed compile would be an answer to a different question.

What the compile does

The model is forced to call one tool whose input schema IS the workspace's address space: one variant per (relation, slot), the ops that slot's type supports (gte / lte on a term slot only where its axis is ordered), the axis's terms in the slot description, and brief_span + quote required on every constraint:

from epistem.bundles.catalogue_loader import parse_relation_sections
from epistem.bundles.registry import load_registry_bundle
from epistem.substrate.search import compile_tool

pack = parse_relation_sections(load_registry_bundle("bundle_expert_network"))
tool = compile_tool(pack.schema, pack.vocabulary)
variants = tool["input_schema"]["properties"]["constraints"]["items"]["anyOf"]
print(tool["name"], "|", len(variants), "relation.slot variants")
for variant in variants:
    p = variant["properties"]
    if p["slot"]["enum"][0] in ("seniority", "title"):
        print(p["op"]["enum"], "|", p["slot"]["description"])
print("required:", variants[0]["required"])
compile_search_brief | 13 relation.slot variants
['eq', 'ne', 'in', 'gte', 'lte'] | held_role.seniority (term): axis seniority, ordered low to high: IC < Manager < Director < VP < SVP < C-level. Write the term, or the brief's own word when it is a synonym — synonyms and groups are resolved server-side
['eq', 'ne', 'in'] | held_role.title (term): axis title has NO vocabulary in this workspace; a constraint here is reported unbound and does not run
required: ['relation', 'slot', 'op', 'value', 'brief_span', 'quote', 'required']

What the model returns is then checked against the brief, deterministically, before anything runs. The example below hands that checker — read_compile_output, the function the route runs on the model's tool call — a compile with two faults: one constraint whose offsets are mis-counted but whose quote is text of the brief, and one whose quote the brief does not contain. The third sentence of the brief states nothing the pack can express:

from epistem.bundles.catalogue_loader import parse_relation_sections
from epistem.bundles.registry import load_registry_bundle
from epistem.substrate.search import read_compile_output

pack = parse_relation_sections(load_registry_bundle("bundle_expert_network"))
brief = ("Find VP-level Sales leaders in DACH. They must have passed a compliance "
         "screening. Fluent in German.")

def at(words):
    start = brief.index(words)
    return [start, start + len(words)]

model_said = {"constraints": [
    {"relation": "held_role", "slot": "seniority", "op": "gte", "value": "VP",
     "quote": "VP-level", "brief_span": at("VP-level"), "required": True},
    {"relation": "held_role", "slot": "function", "op": "eq", "value": "Sales",
     "quote": "Sales leaders", "brief_span": [0, 3], "required": True},
    {"relation": "held_role", "slot": "geography", "op": "eq", "value": "DACH",
     "quote": "DACH", "brief_span": at("DACH"), "required": True},
    {"relation": "screening", "slot": "outcome", "op": "eq", "value": "passed",
     "quote": "passed", "brief_span": at("passed"), "required": True},
    {"relation": "screening", "slot": "kind", "op": "eq", "value": "compliance",
     "quote": "a compliance check", "brief_span": [0, 5], "required": True},
]}
compiled = read_compile_output(brief, pack.schema, pack.vocabulary, model_said)
for c in compiled.constraints:
    moved = f" (relocated from {c.span_relocated_from})" if c.span_relocated_from else ""
    print("kept", c.slot, c.op, c.value, c.brief_span, repr(c.quote) + moved)
for d in compiled.dropped:
    print("dropped", d["constraint"]["slot"], "->", d["reason"])
print("uncompiled:", compiled.uncompiled)
kept seniority gte VP (5, 13) 'VP-level'
kept function eq Sales (14, 27) 'Sales leaders' (relocated from (0, 3))
kept geography eq DACH (31, 35) 'DACH'
kept outcome eq passed (52, 58) 'passed'
dropped kind -> quote_not_in_brief
uncompiled: [{'span': [83, 100], 'text': 'Fluent in German.'}]

The rules, from epistem/substrate/search.py:

  • a constraint must be attributable to whole words of the brief. A span that does not re-slice to its quote is re-located when the quote occurs exactly once as whole words (span_relocated_from names the offsets the model gave); a quote that occurs more than once, with a span on none of the occurrences, is dropped as span_ambiguous; one found only inside longer words is quote_not_whole_words; one found only across a sentence break is quote_crosses_sentences (it would mark the next sentence compiled while holding none of its condition); one not in the brief at all is quote_not_in_brief — the constraint came from nowhere; a blank quote is quote_empty;
  • a constraint naming a relation, slot or op the tool did not offer, or asking a slot something its type cannot answer, is dropped with the reason;
  • every sentence no kept constraint came from is listed as uncompiled — that part of the request was NOT searched.

On the brief path the response carries the compile under compiled (brief, constraints with brief_span and quote, dropped, uncompiled, as_of, model, usage); gaps.dropped and gaps.uncompiled repeat its gaps, and each withholds the licence (narrowed_by: constraints_dropped, brief_uncompiled). Check the compile read the request the way it was meant — and if it did not, send the corrected constraints back: compiled.constraints is the shape the constraints field takes. A brief that pins a month ("as of March 2020") compiles to compiled.as_of; an as_of in the request wins over it, and as_of_source says which applied (request or compiled).

A workspace that declares no relation answers a brief without calling a model: every sentence is uncompiled and unbound_because is workspace_declares_no_relation.

What the licence covers

coverage.completeness_licensed: true licenses every entity satisfying the request, as run, is in entities.matched — zero included, which is then there are none. When it is false, narrowed_by names every reason, and each is a different instruction:

narrowed_by what it means
acl your key reads only some of the workspace's documents
constraints_dropped / brief_uncompiled part of a brief did not compile (a brief)
constraint_unbound:<i> constraint i could not run (gaps)
vocabulary_unknown_surface:<slot> / vocabulary_unplaced:<slot> the vocabulary could not vouch for a constraint's set (vocabulary)
entities_not_recorded some entity was never looked at for a required relation
relation_not_recorded:<relation> a required relation is declared and no load ever looked at anyone for it
undated_rows_unanswerable:<relation> / open_period_rows_unanswerable:<relation> rows that cannot be placed at as_of (as_of)
corrected_rows_unanswerable:<relation> an entity holds a row a correction retired that a document still states (corrections)
withheld_rows_unanswerable:<relation> a connected export states a row for an entity that its loader could not read, and no readable row of theirs matches — they are undetermined, never none (a document-store export)
unattributed_rows_withheld:<relation> a connected export holds a document its loader could tie to no entity: whoever it was about is in no list, so these are all cannot be said of that relation until a re-sync states it readably
slot_unrecorded_rows_unanswerable:<relation> a candidate row holds no value for a slot a constraint names (an unrecorded slot)

withheld_because: truncated means only that matches is a page. None of this licenses that a matched row's values are right: read each row's receipts, and retire a wrong row with limma_correct.