Agent query: referencing calling note's attribute inside find() — nothing works as expected

I’m building a CRM in Tinderbox, still. And with Claude’s help. I have two containers: Connections Raw (connection notes, each with a $LinkedinURL attribute) and Messages Raw (message notes, each with a $MessageKey attribute containing a LinkedIn URL, and a $ProspectTrigger attribute that may equal “Prospect”).

The goal of Agent: Flag Prospects is to find connection notes that have at least one matching message note where $MessageKey == $LinkedinURL and $ProspectTrigger == “Prospect”, then set $IsProspect = true on the connection note.

The query I want to express:

For each note descended from Connections Raw, find any note in the document where $MessageKey equals this connection note’s $LinkedinURL and $ProspectTrigger equals “Prospect”. If such a note exists, include this connection note in the agent’s results.

What I’ve tried:

  1. Original query:
descendedFrom("Connections Raw")
& $LinkedinURL != ""
& !$IsProspect
& find($MessageKey == $LinkedinURL & $ProspectTrigger == "Prospect" & !$IsAlias) != ""

Result: flagged all 12 connection notes, including 6 that have no matching message notes. Diagnosis: inside find(), $LinkedinURL resolves to the note being tested by find() (a message note), not the calling connection note. Since message notes have no $LinkedinURL, the comparison $MessageKey == $LinkedinURL was comparing $MessageKey to an empty string — and some message notes have an empty $MessageKey, producing false matches.

  1. Query using that designator, per aTbRef:
descendedFrom("Connections Raw")
& $LinkedinURL != ""
& !$IsProspect
& find($MessageKey == $LinkedinURL(that) & $ProspectTrigger == "Prospect" & !$IsAlias) != ""

Result: flags no notes at all. aTbRef says that is evaluated in stamps — it may not be valid in agent queries.

Test data:

  • 12 connection notes in Connections Raw, each with a unique $LinkedinURL

  • 6 message notes with $MessageKey matching the first 6 connection note URLs, $ProspectTrigger = “Prospect”

  • 6 message notes with $MessageKey matching the same first 6 URLs, no $ProspectTrigger

  • 6 message notes with $MessageKey set to URLs that match none of the connection notes, $ProspectTrigger = “Prospect”

Expected result: exactly the first 6 connection notes flagged.

The question: What is the correct way to reference the calling note’s $LinkedinURL value inside a find() expression within an agent query?

I’ve uploaded the TBX file containing dummy data.

TheodoreCRM2026_clean4.tbx (137.9 KB)

THE find(query) operator looks at each notes in turn to see whether that note satisfies the query. When evaluating query, this is bound in turn to each note in the document, and that is bound to the note that was bound to this.

find($MessageKey == $LinkedinURL & $ProspectTrigger == "Prospect" & !$IsAlias) will be true if there is at least one note, anywhere in the document, for which (a) $MessageKey == $LinkedinURL, and (b) $ProspectTrigger==“Prospect”, and (c) $IsAlias is false. I suspect that’s not quite what you want to test.

Thank you, @eastgate. That clarifies the scoping precisely.

You’re right that it’s not quite what I want to test. What I need is: for each connection note being evaluated by the agent, find a message note where $MessageKey equals that specific connection note’s $LinkedinURL — not $LinkedinURL in general, but the value held by the calling note.

Based on your description, that should be the right tool: when the agent evaluates a connection note, that is bound to it, so $LinkedinURL(that) should give me the calling connection note’s URL to compare against $MessageKey inside the find().

But when I use:

find($MessageKey == $LinkedinURL(that) & $ProspectTrigger == "Prospect" & !$IsAlias) != ""

the agent flags nothing at all. The data looks correct — $MessageKey and $LinkedinURL values match exactly on the notes I’d expect to pair.

Is $LinkedinURL(that) valid syntax inside a find() in an agent query? Or is there another way to pass the calling note’s attribute value into the find() expressio

Following up with a new test result.

I confirmed that (that) resolves correctly in a basic agent query. This agent catches all 12 connection notes as expected:

descendedFrom("Connections Raw")
& $LinkedinURL(that) != ""

But this agent catches nothing:

descendedFrom("Messages Raw")
& $MessageKey == $LinkedinURL(that)

So (that) appears to work in the outer agent query but not inside a find() or a cross-container comparison. Is there a correct way to pass the calling note’s $LinkedinURL value into a find() expression within an agent query?

that isn’t bound in an agent; it’s bound in find because find hijacks this.

Suggestion: see what find is returning.

  1. Try a rule or a stamp: $MyList=find($MessageKey="some message key"); Look at the results. Are they what you expect?

  2. Put the message key into a string attribute of a configuration note, and use that: $MyList=find($MessageKey=$MyString(/config) ); Is the result what you expect?

This sort of systematic, step by step, incremental building usually resolves puzzles like this, because you can see all the intermediate results.
4.

(Agent) Query vs. find(). You don’t need to use both together. A find() is essentially running a query, but outside its normal place, i.e. in an agent. The origin of find(), if I recall, was because things like collect() have a scoped argument, e.g. collect(children, .... But, what if there isn’t a designator (like children) that defines the items to her collected? Enter find(), first added in v5.6.0, whereas the agent query has been in the app since the start.

Looking at your TBX the root issue is you want to find connections with one or more matching messages. IOW you want to match each message to be treated as a match if it $LinkedinURL matches the $MessageKey within a list of the $MessageKey values of all children of ConnectionsRaw.

After some cleaning up on inappropriate prototypes settings, I added some badges including (via the ‘Message’ prototype) a rule setting the badge as to whether a message’s $ProspectTrigger is “Prospect” (blue disc), a project and with a $MessageKey in $MyList of `/Connections Raw’ (green disk), or any other value (red disk). This is helpful for de-bugging (I caught to code edge cases). Our agent should never list a red or blue disk item.

Now, ‘Connections Raw’ gets a rule:

$MyList = collect(children,$LinkedinURL);

Now we can make our agent “Agent: New Prospect Messages”:

inside(/Messages Raw)&$ProspectTrigger="Prospect"&$MyList(/Connections Raw).icontains($MessageKey)

I set Display Expression on the agent and the ‘Massage Raw’ container as a sense check—they should match.

Once you know this works you can delete the badges of the messages and their container’s Display Expression as it’s code you don’t really need. The Display Expression count on the agent is useful as it shows, even when collapsed, how many un-handled messages there are, assuming that after processing their $MessageKey is set to some other value. For efficiency, some code could also most processed messages to a new container.

OK, left do that. New container ‘Messages Handled’ (check out its container $OnAdd, the new prototype) and a new agent ‘Agent: Move Processed Messages’. This looks for messaged with $MessageKey value ‘Done’ and moves the original [sic] of that message to the ‘Messages Handled’ container.

Another small tweak, the $OnAdd of ‘Prospects’ sets new child notes to prototype ‘Connections Prospect’. I think that was your intent, but not coded quite so.

My demo based on your file (with corrections/additions) : TheodoreCRM2026_clean5.tbx (144.2 KB)

Reflections:

  • Don’t use find() in a query. If you feel the need, you likely need to think the metadata through a bit more. No criticism there, once you move past the basic $A="B" form of queer, extra considerations come into play
  • There is invariably more than one way to do the above. I think my solution works but other approaches are possible. there may be wrong ways but no one single right way.

†. I was very confused until I figured you’d somehow set ‘MSG-ZW-A’ to use an italic title and the note is not at alias. Removing the italics (why needed?) makes the data easier to understand.

Thank you, @eastgate. Build incrementally and inspect intermediate results. Point taken.

Thank you, @mwra!

I should have added, what foes as a Rule and what as an Edict is down to taste and scale (in a very big file (no not most) edict or even manually invoked stamps can avoid having loads of always-on rules. That said, there’s no harm developing/testing with rules that ‘adjusting’ to an edict where it makes sense.

A hidden wrinkly here was the 1-to-N relationship of the prospects (people) to messages (emails). Done correctly, all emails ought to match one person (I assume the blue badges in my test file were deliberate test edge cases). But one person could match multiple emails.

One ‘missing’ action is that you can’t use ‘==’ with a list as source, as you match the whole list not as most assume any one item. One way to avoid the List.contains() approach might be to use a user function:

function fListItemMatch(iMatch:string,iList:list){

   var:boolean vMatch = false;

   iList.each(anItem){
      if(anItem==iMatch){
         vMatch=true;
         return vMatch; // got a match, bail out now
      }
   }

   return vMatch; // returns the fail condition

} //END

I’m not 100% sure if functions are allowed in queries (@eastgate?), but as the above returns a boolean (like all query items should) it ought to work. And it seems to…

See: TheodoreCRM2026_clean6.tbx (214.7 KB)

Look at the new agent ‘Agent: New Prospect Messages via function’. the function itself is in a Library note within the newly-added /Hints container.

I wish this function could be given some syntactic sugar to make a built-in action like (a proposed) listItemMatch(matchStr,list) to allow == matching of a string (matchStr to any item in list. I think that would save people having to use regex calls like .contains() where really they want a string equality check albeit against an individual list item. The operator would essentially replicate the custom code above.

Edit: Actually a more useful version of the operator would be a .dot-form [list].listItemMatch(matchStr), which closer follows the string pattern:
$MyString == "foo";
Thus we can’t do (with the same outcome):
$MyList == "foo";
but suggest:
$MyList.listItemMatch("foo");
subject-operator-match and also close in written order form to:
$MyList.contains("foo");
For now, you’d need to use custom code:
fListItemMatch("foo",$MyList);

1 Like

If you want a case-insensitive match option:

function fListItemMatch2(iMatch:string,iList:list,iNoCase:boolean){

   var:boolean vMatch = false;

   iList.each(anItem){
      if(iNoCase==true){ // want a case insensitive match
         if(anItem.lowercase==iMatch.lowercase){
            vMatch=true;
            return vMatch; // got a match, bail out now
         };
      }else{ // want a case sensitive match
         ifanItem==iMatch){
            vMatch=true;
            return vMatch; // got a match, bail out now
         };
      }
   }

   return vMatch; // returns the fail, unmatched, condition

} //END

This does mean an extra mandatory argument:
fListItemMatch2("foo",$MyList,false) // does a case-sensitive match
or
fListItemMatch2("foo",$MyList,true) // does a case-insensitive match

Or, one could simply have 2 discrete functions with different names avoiding the need for a third argument. 2 simpler functions or one more complex. Which is approach better? Neither, it boils down to personal choice.

1 Like

Short-term memory vs long-term memory… The need for my function solution above is 16 years out of date! :flushed_face:

From v5.7.0+ .contains() and .icontains() behave differently depending on the chained data type. The regex matching is used only where the source is String-type data. For List or Set types the test is a == equivalence test. The latter also means meaning this method cannot be used to do a partial match of an item, such as may be done with a source String.

List.contains(), List.icontains(), String.contains(), String.icontains().

I’ll leave the code examples above as they still be useful for someone wanting to try out making a user function.