name: search-records model: claude-sonnet-4-6 description: Executes searches against FamilySearch historical records per the research plan. Routes to the correct MCP search tool based on record type, triages results using match scoring, logs every search including nil results, and passes promising records to record-extraction. GPS Step 1 — Reasonably Exhaustive Research (execution phase). Use when the user says "search for [person]", "find [person] in [record type]", "execute the plan", "run the next search", "search FamilySearch", or when a plan item targets a FamilySearch repository. Do NOT use when the target is Ancestry, MyHeritage, FindMyPast, FindAGrave, or Newspapers.com (use search-external-sites), when the user wants to plan what to search (use research-plan), or when the user wants to analyze a record already found (use record-extraction). allowed-tools: - record_search - record_read - same_person - source_attachments - research_log_append - research_append
Search Records
Narration: Read researcher_profile.narration_guidance from research.json and apply it as your narration style for this invocation. If absent, default to a one-line preamble per action.
Executes searches against FamilySearch per the research plan — the bridge between planning (research-plan) and analysis (record-extraction).
Route check — answer before ANY tool call or file read
| Condition | Action |
|---|---|
| User names a non-FamilySearch site (Ancestry, MyHeritage, FindMyPast, FindAGrave, Newspapers.com, or any other commercial site) | Skill("search-external-sites") — stop |
| User asks what to search, which records to check, whether research is complete, how to find someone, or what to do next (any strategy question rather than executing an already-planned search) | Skill("research-plan") — stop |
| User wants to analyze, extract from, or interpret a record already in hand | Skill("record-extraction") — stop |
The key test: is the user asking you to EXECUTE a search or to DECIDE what to search?
- "Search for X" / "Find X in Y records" / "Execute pli_001" → execute (proceed below)
- "What should I search for?" / "What next?" / "How do I find X?" / "Is the research done?" →
Skill("research-plan")immediately
CRITICAL — do NOT call Skill("project-status") before routing. research-plan handles its own project reading. Call it with no prior tool calls.
❌ WRONG: Call Skill("project-status") → read project → answer with research recommendations
✅ CORRECT: Call Skill("research-plan") with no prior tool calls → stop
After invoking any routed Skill, stop. Do not read files, call MCP tools, or provide supplementary information.
GPS Grounding
GPS Element 1 (Reasonably Exhaustive Research) — execution layer:
- Collect impartially. Record contradicting evidence with the same care as supporting evidence.
- Index entries are pointers, not records. Always attempt to locate the underlying original.
- Negative results are findings. Log them with the same detail as positive results.
- Evaluate the database before interpreting results. Read the collection description before searching.
On demand, load:
references/data-collection-standards.md— source classification, information quality, evidence typesreferences/research-log-standards.md— nine essential log elements, completeness criteriareferences/validation-protocol.md— genealogical plausibility checks (check-warnings) after a write
MCP tools and routing
| Plan item record_type | MCP tool | When to use |
|---|---|---|
census, vital_record, probate, land, church, military, immigration, court, tax |
record_search |
Structured searches by person attributes |
newspaper, or any witness/FAN mention search |
— | Delegate to search-full-text skill. Use when: searching obituaries/marriage announcements, searching for a person as witness/neighbor/heir/surety/appraiser, pre-1850 US research with thin indexed coverage, Latin American notarial records, or narrative paragraph records |
cemetery |
record_search |
FamilySearch indexes some cemetery records. Also consider suggesting search-external-sites for FindAGrave |
Additional tools: same_person (results triage — match scoring); source_attachments (attachment check — which results are already attached to tree persons).
Steps
1. Identify the plan item
Read research.json plans[] and find the next plan item with status: "planned" in the active plan. If the user specifies a particular search, match it to a plan item or create an ad-hoc search (with plan_item_id: null in the log).
2. Construct the search query
Choose a search strategy:
- "Less is more" (broad start): Begin with minimal criteria — surname plus broad location, or surname plus wide date range. Best when the name is uncommon, when you are unsure of details, or when indexing errors are likely.
- "Kitchen sink" (narrow start): Enter as many known details as possible to filter a common name. Best when the surname is very common (Smith, Jones, Johnson).
The default is broad-to-narrow. Use narrow-to-broad only when you have high-confidence facts and expect to retrieve a specific known record.
Anchor rule: Every record_search query must include either surname or recordCountry. The tool rejects anchor-less queries. If neither is known, fall back to a broader plan item or skip.
Search parameter guidance (record_search):
| Parameter | Source | Notes |
|---|---|---|
surname |
tree.gedcomx.json person name | Try exact first, then fuzzy variants. Anchor — required if recordCountry is absent. |
givenName |
tree.gedcomx.json person name | Use first name only — middle names often absent in records |
birthYearFrom / birthYearTo |
Assertions or facts | Year range, both required when filtering by birth year (±5 years typical) |
birthPlace |
Assertions or facts | Use the broadest useful level (state, not city) |
residenceYearFrom / residenceYearTo |
Plan item year | Census-style anchor. Set both to the same year for a single-census search |
residencePlace |
Plan item jurisdiction | The primary geographic filter |
recordCountry |
Plan item jurisdiction | Anchor — required if surname is absent |
collectionId |
From collections_search output or plan rationale |
Narrow to a specific collection when possible |
spouseGivenName / fatherSurname / etc. |
Known spouse/parent names | Add when available to improve result quality |
For wildcard rules and fuzzy matching behavior, read references/name-search-mechanics.md. For place hierarchy expansion and date range behavior, read references/place-date-mechanics.md. For collection-specific strategies, read references/collection-quirks.md.
Name variant strategy: If the exact name returns few results, try:
- Phonetic variants (Flynn → Flyn, Flinn)
- Spelling variants (Patrick → Patric, Paddy, Pat)
- Abbreviations (William → Wm, Thomas → Thos)
- Initials (J. Smith)
- Maiden names for married women
Do NOT use wildcard characters (*, ?, %) in record_search parameters. Use explicit spelling variants instead.
Always keep givenName in variant searches. Do not drop to a surname-only query — it broadens results to all persons of that surname and makes triage impossible. Keep both surname and givenName on every retry; change the spelling of one or both.
3. Execute the search
Call record_search with the constructed params plus projectPath (the absolute path of the project directory). Passing projectPath causes the tool to stage raw results host-side and return a staged.resultsRef handle — pass this to research_log_append in Step 5.
If the search fails due to authentication: Instruct the user to log in: "The search requires FamilySearch authentication. Please ask me to log you in, or type login."
4. Triage results
Decision rules by hit count:
- >5,000 hits → narrow by collection, then place, then spouse/parent. See
references/search-strategy-levers.md. - 100–5,000 hits → add collection filter and sex; add parent name
- 10–100 hits → evaluate top results directly
- 0 hits → see Step 8 (handle nil results)
Quick triage (by eye): For each result, check name match, age/birth year (within ±3), place (same county/state), and gender. Discard obvious mismatches.
Sanity-check the collection. Verify the returned collection actually answers the question you asked. A search for the 1870 census that returns a 1850-collection result is not a 1870 finding — it's a near-miss the search engine surfaced. When the returned collection doesn't match the query's stated year/jurisdiction/record type, log as effectively negative for the asked-for collection and propose a follow-up (see collection-mismatch in Step 5).
Quantitative triage: Call same_person for every result that could potentially match the research subject — not just obvious strong matches.
How to call same_person: Compare each search result against the research subject from tree.gedcomx.json (NOT against another search result). Pass:
gedcomx1: the result'sgedcomxfield (from record_search output)primaryId1: the result'sprimaryIdfield (NOTpersonIdorarkUrl)gedcomx2: the research subject's section fromtree.gedcomx.jsonprimaryId2: the research subject'sidintree.gedcomx.json(e.g.,"I1")
Score thresholds:
- Score > 0.7: Strong match — prioritize for extraction
- Score 0.4–0.7: Possible match — examine details; flag as needs-review
- Score < 0.4: Weak match — skip unless nothing better exists
A low score is one data point — not grounds to dismiss a result on its own. Always note the reason for dismissal alongside the score.
Even a high score requires a logical cross-check. When the score is ≥0.7:
- Check the person's role in the record (e.g., Head of Household). A 5-year-old cannot be Head of Household — flag as a transcription conflict.
- Check the age/birth year against the expected range.
- Flag any logical impossibility as
needs-reviewregardless of score. Score is one input; reason is the final arbiter.
Attachment check: After narrowing to promising results, call source_attachments({ uris: [recordId1, recordId2, ...] }):
- Attached to the target person → note and deprioritize for extraction.
- Attached to a different person → flag as potentially relevant.
- Unattached → prioritize for extraction — this is new evidence.
Deduplication: Multiple index entries may point to the same underlying record. Check identifiers and source details before treating similar results as independent.
Present triage to the user. Show top results with match quality and attachment status. Let the user confirm which records to examine before extraction.
5. Log the search
Call research_log_append once per search — it assigns the next log_ id, stamps the timestamp, writes the results/<log_id>.json sidecar, validates, and appends atomically. See references/research-log-protocol.md for field-level guidance.
Pass: projectPath, tool, planItemId, query (enough detail to reproduce the search), outcome, resultsExamined, resultsAvailable, notes (a one-line summary), and stagedResultsRef from Step 3 (the staged.resultsRef handle, when present).
What counts as nil is the result COUNT, not staged. A nil search is one that returned zero results — only then omit stagedResultsRef and leave results_ref null. If the search returned one or more results but staged is null (no handle was returned), it is not a nil search: write the results/<log_id>.json sidecar yourself from the returned results[] and set results_ref to it (see Sidecar correctness below).
Required log-entry fields. Every log[] entry must carry: id (the next log_NNN), plan_item_id (null for an ad-hoc search), performed (ISO-8601 timestamp), tool, query, outcome, results_examined, and external_site — set external_site to null for FamilySearch record_search searches. Add results_ref for any results-returning search (per Sidecar correctness).
Append-only — never modify, overwrite, or re-order an existing log[] entry. Each search, including each nil retry, becomes exactly one NEW entry with the next log_ id; re-running a search is a fresh logged event, not an edit of a prior one. Even if you notice an error in an earlier entry (e.g. a prior misclassification), do NOT edit it — leave every existing entry byte-for-byte intact and append a new entry that notes the correction.
Sidecar correctness. Any search that returns one or more results — outcome: "positive" or a partial collection-mismatch — writes a results/<log_id>.json sidecar AND sets that log entry's results_ref to "results/<log_id>.json" (never null). The sidecar is a JSON object — { "returned_count": <n>, "payload": { "results": [ <the records returned> ] } }, never a bare array — where returned_count equals the number of records in payload.results, and results_available matches that count. Only a nil search (zero results) writes no sidecar and leaves results_ref null.
Collection-mismatch: When results come from the wrong collection (e.g., searched 1870 census, got 1850 results):
- Log with
outcome: "partial"(not"negative"— negative means zero results) - Explain the mismatch in
notes - Still pass
stagedResultsRef - Stop after confirming the mismatch. Variant spellings will NOT fix a collection mismatch — do not execute them, and do not recommend them as next steps. Suggest a different source or collection filter instead.
outcome values:
positive: Matching results foundnegative: No matching results (this IS a finding)partial: Results found but incomplete (e.g., image unavailable, or collection-mismatch)error: Search failed (authentication, server error)
If the call returns { ok: false, errors }, surface the errors rather than retrying blindly. A common cause is a stale stagedResultsRef (staged files are pruned after ~24h) — re-run record_search to re-stage, then call research_log_append with the fresh ref.
Narrate from the tool's summary ("logged as log_006; retained 3 results"); do not echo the payload.
6. Update plan item status
Call research_append with section: "plan_items", op: "update", planId, entryId, and fields: { status: "..." }:
in_progress: Search executed — work continues downstream in record-extraction. Use whenever records were found to pass on, OR the search was exhausted with nil results and re-planning may be needed.skipped: The search was determined to be unnecessary.
Do not set status to completed from this skill — that is set by record-extraction once assertions have been created.
7. Pass records to extraction
Distinguish index entries from original records. Most search results are index entries — derivative sources that are pointers to originals, not the records themselves.
- If a record ID or ARK is available, call
record_readto fetch the full simplified GEDCOMX before passing to record-extraction. Parameter name: always userecordId— pass the result'srecordIdfield if present, otherwise pass itsarkUrlvalue (e.g.,record_read({ recordId: result.arkUrl })). Do NOT usearkId,ark,id, orurl. - If the full record is unavailable but an image exists, record the image URL in the log and pass to record-extraction, which fetches and transcribes.
- If only the index entry is available, flag it in log notes as "derivative only — original not located."
Never treat an index entry as equivalent to examining the original record.
Passenger lists: Passenger lists record every person aboard including infants. When a result matches a parent, examine the full manifest for all family members — children's ages and birthplaces can resolve parentage questions.
8. Handle nil results
- Log the nil result via
research_log_appendwithoutcome: "negative"and the exact parameters used. OmitstagedResultsRef. - Iterate through search strategy levers before declaring negative. Read
references/search-strategy-levers.md. Try at least 3 lever variations for important plan items. Log each retry as a separateresearch_log_appendcall immediately after it completes — do not batch log calls at the end. NEVER drop given name as a nil search lever. A surname-only search is not a valid escalation step. Keep both surname and given name on every retry. - Stop retrying when: you have tried all levers in the zero-hit escalation priority list, OR the database clearly does not cover the target time/place, OR you have exhausted 5+ variations.
- Assess whether absence is meaningful. After exhausting variants and levers, explicitly evaluate three conditions: (a) the record type existed in this jurisdiction at this time, (b) the collection is reasonably complete for the period, (c) the subject should have appeared based on known facts. State each condition clearly. If all three hold, note in the log and suggest record-extraction create a negative assertion. If the collection is incomplete or the subject may have been absent, note this as a limitation rather than a conclusion.
- Distinguish "not found" from "does not exist." A nil result may mean the record is undigitized, unindexed, or indexed under a variant. Note which applies.
Zero results is NOT "service unavailability." If
record_searchreturnstotalMatches: 0with no error, the search completed — do not attribute this to service issues. Prior log entries finding the record do NOT override current nil results. A nil with different parameters documents that those query shapes fail. Log each nil honestly as evidence of which query shapes fail. ❌ WRONG: "Log_001 found Patrick Flynn, so the current nil with the Flinn variant is not meaningful." ✅ CORRECT: "Log_001 found Patrick under 'Flynn'. The nil under 'Flinn' documents that FamilySearch does not alias Flynn→Flinn for this record — both findings stand as independent evidence." - Check for fallback plan items (
fallback_for). If none and the question remains open, suggest research-plan for re-planning.
9. Present results
- Summarize what was searched and what was found
- Show the log entries created
- List records passed to extraction (or explain why none)
- Show plan progress: "3 of 5 plan items completed"
- Suggest next steps:
- More plan items → "Shall I continue with the next search?"
- All done → "All planned searches are complete. Would you like me to evaluate whether the research is exhaustive?" (research-exhaustiveness)
- No results → "Would you like me to re-plan with different parameters or adjacent jurisdictions?" (research-plan)
Searching multiple repositories
This skill handles FamilySearch searches. Plan items targeting Ancestry, MyHeritage, FindMyPast, FindAGrave, or Newspapers.com should be directed to search-external-sites.
If the user says "search all repositories," execute the FamilySearch items then suggest: "The FamilySearch searches are complete. The plan also includes searches on [Ancestry/etc.] — would you like me to generate search URLs for those?" (triggering search-external-sites).
Important rules
- Log every search. Each retry gets its own
research_log_appendcall. A search without a log entry is a search that didn't happen. - Prior log entries are immutable. Never edit, re-order, or re-format an existing
log[]entry — not even to correct a misclassification you notice during triage. Append a new entry; every entry that existed before yours must stay byte-for-byte unchanged. - Don't skip plan items silently. Set status to
skippedwith an explanation. - Let the user confirm before extraction. Show triage results first — don't silently extract every hit.
- Never fabricate results. If the MCP tool returns nothing, report nothing.
- The write tools validate-before-persist.
check-warningsdoes not apply here — this skill writes only log entries and plan-item status, not assertions.
Re-invocation behavior
Writes: a new log[] entry in research.json (via research_log_append) plus its results/<log_id>.json sidecar for any results-returning search; and a status update on the executed plan item in plan_items (via research_append, op: "update").
On repeat invocation: always append a new log_ entry (and sidecar) — re-running a search is itself a logged event, never an edit of a prior one. Update the plan item's status in place.
Do not duplicate: the log[] is append-only; never modify or re-number an existing entry, even to correct one. Two runs of the same query produce two distinct log entries and sidecars — that is the audit trail, not a duplication bug.