# Searching FLEx and a FLEx Scratchpad notebook with an AI app

The setup for an AI app on your own computer — Claude Code, Claude Desktop, Gemini CLI or another that can use
[FLExTools MCP](https://github.com/MattGyverLee/FlexToolsMCP) (to read a FieldWorks project) and read files (to read
your FLEx Scratchpad notebook). Give the app this page's address, or paste the text below, and say: *Follow this
setup.* Then ask it to search FLEx, your notebook, or both. It only reads your FLEx project and your notebook; it
writes one new note only when you ask it to.

FLEx Scratchpad makes the same setup with your project and your notebook's folder filled in: Settings › AI
assistant › *⎘ Copy the setup*.

---

You search my FieldWorks (FLEx) project for interlinear lines, through FLExTools MCP, my FLEx Scratchpad notebook (below)
for what I have written, or both: each question says which (see “What to search”). The texts and my notes
hold the words of real speakers, shared for this research: quote only what your answers need.

## Rules
- Only read the project. Start with `flextools_start(api_mode="flexicon", project_name=…, write_enabled=False)`;
  every `flextools_run_module` stays a dry run. Never ask for writes, never change anything.
- If a tool fails or you cannot read what you need, say so and stop. Never guess a GUID, a number or a word.
- At most 25 results unless I ask for more; if there are more, say how many.
- Write your own words in the language I write to you; never translate, correct or complete the data.

## Sharing the project with FLEx and FLEx Scratchpad
- `flextools_start` opens nothing. Each `flextools_run_module` opens the project in a process of its own for that
  one call and closes it when the call returns: nothing stays open between questions, and there is nothing to close.
- One FLExTools call at a time, never two at once. Keep each module quick (read only the texts, lines and fields
  the question needs), and never lower `timeout_seconds`: a call that runs out of time is ended without closing the
  project, and FLEx may then be unable to save.
- Use only `flextools_start`, `flextools_list_projects`, `flextools_run_module` (with `write_enabled=False`) and the
  tools that describe FLEx's API. Never call `flextools_try_word`, `flextools_parse_text`, `flextools_parse_sandbox`,
  `flextools_grammar_health`, `flextools_manage_config`, `flextools_refresh_indexes` or `flextools_prepare_report`:
  they keep the project open in a process of their own, or change files. If one of them was called, call
  `flextools_parse_release(project_name=…)` before you answer.
- If a call answers with the `error_code` `project_locked`, try it once more. If it is locked again, stop and tell
  me in one sentence what to do, by its `verdict` — never end a process, delete a `.lock` file or turn writes on:
  - `held_by_other`: a program other than FLEx has the project. When the holder is `FLEx Scratchpad` (or `python` /
    `pythonw` while FLEx Scratchpad is open), it is most likely FLEx Scratchpad: FLExTools MCP cannot tell such
    programs apart and guesses another FLExTools or Claude session. Ask me to click FLEx Scratchpad's status pill
    (top right) → “Let go of the project”, then ask again.
  - `open_exclusive`: FLEx has it open and does not share it. Ask me to close FLEx, or to share the project (FLEx:
    File › Project Management › FieldWorks Project Properties › Sharing › “Share project contents with programs on
    this computer”).
  - `stale_lock`: the lock names a program that has ended. Ask me to close FLEx and FLEx Scratchpad, open them
    again, then ask again.
- A read that worked is fine whatever `flextools_health` says about locks (its warnings are about writes). A
  `shared_mode_read_back` note means FLEx's newest edits may not be on disk yet: say so in one line.

## How to search
Run ONE module per question with `flextools_run_module` (its `user_intent` at most 200 characters): the module
below, with `TITLES` and `why()` written for the question. It looks at every line and prints only the matches,
so only they come back to you; adapt `why()` (use `words(seg)` for morphemes, glosses and categories), never
the lines that work out `guid`, `n` and `anchor`. Don't print whole texts.

```python
import unicodedata, json
def nfc(t): return unicodedata.normalize("NFC", t or "")
def T(fn, d=""):                      # read guardedly: one odd object never stops the search
    try:
        v = fn()
        return d if v is None else v
    except Exception:
        return d

TITLES = []                           # the texts to search, by their exact titles; [] = every text
MAX = 25

def words(seg):                       # each word: its form, word gloss, category and morphemes (form, gloss)
    out = []
    for tok in T(lambda: list(p.Segments.GetAnalyses(seg)), []):
        cls = T(lambda: tok.ClassName, "")
        if cls == "PunctuationForm":
            continue
        wf = tok if cls == "WfiWordform" else T(lambda: tok.Wordform, None)
        if wf is None and cls == "WfiGloss":
            wf = T(lambda: tok.Owner.Owner, None)
        elif wf is None and cls == "WfiAnalysis":
            wf = T(lambda: tok.Owner, None)
        an = T(lambda: tok.Analysis, None)
        morphs = []
        for b in (T(lambda: list(p.WfiMorphBundles.GetAll(an)), []) if an is not None else []):
            mt = T(lambda: b.MorphRA.MorphTypeRA, None)
            pre = nfc(T(lambda: mt.Prefix, "")) if mt is not None else ""
            post = nfc(T(lambda: mt.Postfix, "")) if mt is not None else ""
            morphs.append({"form": pre + nfc(T(lambda: p.WfiMorphBundles.GetForm(b), "")) + post,
                           "gloss": nfc(T(lambda: p.WfiMorphBundles.GetGloss(b), ""))})
        out.append({"word": nfc(T(lambda: p.Wordforms.GetForm(wf), "")) if wf is not None else "",
                    "gloss": nfc(T(lambda: p.Segments.GetGloss(tok), "")),
                    "cat": nfc(T(lambda: p.WfiAnalyses.GetCategoryAbbrev(an), "")) if an is not None else "",
                    "morphs": morphs})
    return out

def why(baseline, free, seg):         # THE QUESTION: return a short reason the line matches, or "" to skip it
    toks = [w.strip(".,!?;:\"'()[]").casefold() for w in baseline.split()]
    return "the word kodu" if "kodu" in toks else ""

p = project
found = 0
for text in p.Texts.GetAll():
    title = nfc(p.Texts.GetTitle(text))
    if TITLES and title not in TITLES:
        continue
    guid = str(text.Guid).lower()           # the IText's own GUID, not its StText's
    n = 0
    for pi, para in enumerate(p.Paragraphs.GetAll(text), 1):
        for k, seg in enumerate(p.Segments.GetAll(para), 1):
            n += 1                          # the running count through the text, blank lines included
            if found >= MAX:
                continue
            baseline = nfc(T(lambda: p.Segments.GetBaselineText(seg), ""))
            free = nfc(T(lambda: p.Segments.GetFreeTranslation(seg), ""))
            reason = why(baseline, free, seg)
            if not reason:
                continue
            found += 1
            anchor = str(seg.Guid).lower().replace("-", "")[-12:]
            report.Info(json.dumps({"code": "{{flex:text %s seg=%d at=%s}}" % (guid, n, anchor),
                                    "label": "%d.%d" % (pi, k), "text": title, "baseline": baseline,
                                    "free": free, "why": reason}, ensure_ascii=False))
report.Info("DONE found=%d" % found)
```

## Each result's code
Each match prints its code, the line's address in FLEx Scratchpad, exactly as you must hand it on:
`{{flex:text <text GUID> seg=<n> at=<anchor>}}` — the text's own GUID (lowercase, with dashes), the line's
running number through the text (blank lines counted; NOT FLEx's number such as 2.1), and the last 12
hexadecimal digits of the line's own GUID. A few consecutive lines that belong together are one result:
`{{flex:text <GUID> seg=<a>-<b> at=<anchor a>-<anchor b>}}`. Copy codes exactly as printed; never make one up.

## My FLEx Scratchpad notebook
My notes are in my FLEx Scratchpad notebook, a folder on my computer (ask me where it is).
Search it when my question asks you to (see “What to search” below).
Search it with your file tools: look for words in its `*.md` files (and their other forms), then read the notes
that match.
- Only read it. Never change, move, rename or delete anything in it (the one exception: ONE new note, when I ask for it; see below).
- A note is a UTF-8 Markdown file `<tab folder>/<folders…>/<title>--<id>.md`. It starts with a header between
  two `---` lines: `id` (`n_` and letters or digits: the note's name in links), `title`, `tags`, `created`,
  `updated`, and `kind` when it is not an ordinary note (`examples`: an example collection; `chart`: a chart
  note). `notebook.json` names the tabs: in `tabs`, each `dir` is a tab's folder and `name` what I call it.
- Leave out `.scratchpad/` (the program's own: caches, and `trash/` with deleted notes), `attachments/` (files
  attached to notes) and the `CLAUDE.md` / `GEMINI.md` at the top (setups like this one). `flextexts/` holds
  texts made in FLEx Scratchpad or imported into it, as .flextext XML: they are not in the FLEx project.
- In a note, a line `<!-- comment -->` marks the paragraph after it as my private comment; `[@key]` is a
  citation; `[[Title]]` and `flexscratchpad://note/<id>` link to another note.
- MY OWN STYLING is in the files too, so you can find "where I highlighted in yellow" or "what I made bold": in an
  object's code, `hl=1.2` highlights word 2 of its line 1, `hl[#fff2a8]=1.2-1.4` words 2–4 in that colour, `b=` bold,
  `u=` underline, `i=` italic, `s=` strike, `color[#b91c1c]=` the words' colour; a target may name a tier (`gls:1.2`
  the word gloss, `morph:1.3`), a line alone (`2`), characters (`1.2:4-7`), a chart row, cell or word (`r2`, `r2c3`,
  `r2c3.1`), or `"ka-"` every match of a text. In prose, `**bold**`, `*italic*`, `==highlight==`, `~~strike~~`.
  FLEx Scratchpad's own search box finds them with `style:hl`, `style:hl#fff2a8`, `style:bold`… (`OR`, `NOT`,
  parentheses, `folder:`, `note:`), if the user asks what to type.

### FLEx objects in a note
A FLEx object in a note is a code in double braces on one line, `{{flex:<kind> <GUID> …}}`:
- `{{flex:text <GUID> seg=<a>-<b> at=<anchor a>-<anchor b> …}}`: lines of an interlinear text. The GUID is the
  text's own (an IText's: `str(text.Guid)`). `seg` counts the text's lines 1, 2, 3… straight through, blank
  lines included (FLEx itself shows 1.1, 1.2, 2.1…); `seg=3` alone is one line. `at` holds the last 12
  hexadecimal digits of the first and the last line's own GUID: they find the lines even after FLEx has
  renumbered the text. The other keys (`lines=`, `hl=`, `b=`, `u=`, `tags=`, `cm[…]=`, `cmt=`…) only say how
  I show or styled the example, and a final `"…"` is my caption.
- `{{flex:chart <GUID> rows=<a>-<b> at=…}}`: rows of a text chart, a DsConstChart in
  `project.lp.DiscourseDataOA.ChartsOC`; `rows` counts its rows 1, 2, 3… (FLEx labels them 1a, 1b, 2…).
- `{{flex:entry <GUID>}}`: a lexical entry; `{{flex:sense <GUID>}}`: a sense of one. `project.Object("<GUID>")`
  reads either.
- `{{file <path> …}}`: a file attached to the note, not FLEx.
- A FLEx link, `silfw://localhost/link?…&database=<project>&tool=<view>&guid=<GUID>`, names its object by the
  `guid=` in it; a Markdown link to `flex:text/<GUID>` does the same.
- After each object the file carries a plain copy of it, for reading without the program: a link titled
  `"flex-copy"`, and under an object on a line of its own a ```` ```flex-copy ```` block with the lines it
  showed when the note was last saved (FLEx's numbers, each word over its glosses, the free translation). Search
  and quote those copies; they may be older than FLEx, which is the truth: when the exact words matter, read
  the lines in FLEx with the module below.
- A code from a note is already the address FLEx Scratchpad needs: hand it on exactly as the note has it (you
  may drop the display keys, never the GUID, `seg` or `at`).

To read the lines that notes quote as they are in FLEx now, run this module with `CODES` set to their codes:

```python
import unicodedata, json, re
def nfc(t): return unicodedata.normalize("NFC", t or "")
def T(fn, d=""):                      # read guardedly: one odd object never stops the module
    try:
        v = fn()
        return d if v is None else v
    except Exception:
        return d

CODES = []                            # the {{flex:text ...}} codes to read, copied from the notes as they are

want = {}
for code in CODES:
    m = re.search(r"\{\{flex:text\s+([0-9a-fA-F-]{36})\s+seg=(\d+)(?:-(\d+))?(?:\s+at=([0-9a-fA-F]+)(?:-([0-9a-fA-F]+))?)?", code)
    if m:
        a = int(m.group(2))
        want.setdefault(m.group(1).lower(), []).append(
            (code, a, int(m.group(3) or a), (m.group(4) or "")[-12:].lower(), (m.group(5) or m.group(4) or "")[-12:].lower()))

p = project
for text in p.Texts.GetAll():
    guid = str(text.Guid).lower()           # the IText's own GUID, as the codes hold it
    if guid not in want:
        continue
    title = nfc(p.Texts.GetTitle(text))
    lines = []                              # every line of the text: (FLEx's number, its anchor, the segment)
    for pi, para in enumerate(p.Paragraphs.GetAll(text), 1):
        for k, seg in enumerate(p.Segments.GetAll(para), 1):
            lines.append(("%d.%d" % (pi, k), str(seg.Guid).lower().replace("-", "")[-12:], seg))
    where = {anchor: i for i, (_, anchor, _) in enumerate(lines)}
    for code, a, b, first, last in want.pop(guid):
        i = where.get(first) if first else None     # an anchor finds its line even after FLEx renumbered the text
        j = where.get(last) if last else None
        if i is None and 0 < a <= len(lines):
            i = a - 1                               # else the running count
        if j is None and i is not None:
            j = i + (b - a)
        if i is None or j is None or j < i or j >= len(lines):
            report.Info(json.dumps({"code": code, "text": title, "found": False}, ensure_ascii=False))
            continue
        for label, anchor, seg in lines[i:j + 1]:
            report.Info(json.dumps({"code": code, "text": title, "label": label,
                                    "baseline": nfc(T(lambda: p.Segments.GetBaselineText(seg), "")),
                                    "free": nfc(T(lambda: p.Segments.GetFreeTranslation(seg), ""))}, ensure_ascii=False))
for guid, rest in want.items():
    for r in rest:
        report.Info(json.dumps({"code": r[0], "found": False, "why": "no text with this GUID"}, ensure_ascii=False))
report.Info("DONE")
```

## What to search
Each question says what to search: my FLEx project, my notebook, or both. When it does not say, search both.
When it is my notebook alone, use FLExTools MCP only to read the lines my notes quote, never to search the
texts. Without FLExTools MCP you can still search my notebook: tell me that FLEx is out of your reach.

## Your answers
For each line: FLEx's own number for it (paragraph.segment), its baseline, why it matches, and its code on a
line of its own. For each note: its title, its link `flexscratchpad://note/<id>` and why it answers. I may ask
you to refine the search; keep the codes as the module printed them or the notes have them.

## Putting the results into FLEx Scratchpad
When I ask you to (or when a search is done and I say to keep it), write the results as ONE new note in my
FLEx Scratchpad notebook, then give me its link:
- The folder: a folder inside one of my notebook's tabs that I name (ask me which) (make it if it is not there).
- Pick 10 random lowercase hexadecimal digits: the note's id is `n_` + those digits.
- The file name: a short title, `--`, the id, `.md`, such as `AI search kodu--n_3f9a1c07be.md`.
- The file, in UTF-8:
```
---
id: n_3f9a1c07be
title: AI search: kodu ‘snake’
tags: []
created: 2026-11-01T14:05:00
updated: 2026-11-01T14:05:00
pinned: false
kind: examples
ai: Claude Desktop
---
## Edo Gets Bit by a Snake

{{flex:text fd279969-1213-4754-80f8-a8a05fe4e6d6 seg=3 at=8469fc0a2437 ai=claudedesktop}}
> {ai}the word kodu ‘snake’{/ai}

{{flex:text fd279969-1213-4754-80f8-a8a05fe4e6d6 seg=10 at=6f2e548ad5e7 ai=claudedesktop}}
> {ai}…{/ai}
```
  (the example above is the shape only: use your own id, title, times, codes and reasons — `created` and
  `updated` are now, in local time; a `## ` heading per text, its title as FLEx has it; each code on a
  paragraph of its own, its reason on the next line after `> `, a blank line between results).
- What you add is marked as yours, for my sake: FLEx Scratchpad shows it as the AI's and never exports it as
  mine. `ai: <your app's name>` in the front matter; ` ai=<your app's name, letters and digits only>` inside
  every code you write, before its `}}` (I approve each example myself); and every word of your own — a
  reason, a heading of yours, a sentence — between `{ai}` and `{/ai}` on its own line, never across lines.
- Notes of my notebook that answer go last, under `## In this notebook`, one a line:
  `- [<the note's title>](flexscratchpad://note/<its id>) — {ai}<why it answers>{/ai}`.
- Write ONLY that new file: never change, move or delete anything else in the notebook folder.
- Then give me the link `flexscratchpad://note/<the id>`: I click it and the note opens in FLEx Scratchpad.

Before you first search FLEx in a conversation, check that FLExTools MCP opens the project (list its texts; if it
answers `project_locked`, see “Sharing the project with FLEx and FLEx Scratchpad”). If this setup is all I have
sent so far, reply only: Ready for your question. Then, when my question comes, first ask me for my FLEx project's name and my notebook's folder if I have not said them.
