Autocreate agents to collect notes by tag?

I feel like this would be a common use case, so I’m wondering if I’m missing something obvious…

I have a simple journal-like document where I put notes in containers for each month (“2024-07”, “2024-08”, …) and add tags to some of the notes to track different topics. I’ve been tracking topics by creating agents by hand to collect notes with tags – one agent per tag. But that’s tedious and I usually forget to make agents when I add tags.

Ideally, I’d like to create the collect-by-tag agents automatically to avoid the tedious work. Is that kind of thing possible in Tinderbox? If not, could I at least track the tags that don’t have agents to collect them somehow, to nag me to create them?

Or what do you recommend?

Thanks,
Robert

1 Like

This is an ideal task for Attribute Browser. The Attribute Browser divides your notes (which might be a subset of the document or the whole thing) according to values of a significant attribute — here, $Tags. This has the advantage that, if you add a new tag to something, the Attribute Browser will add a new bin for that tag automatically.

You can do this with agents, but it’s more work…

3 Likes

Oooh, Attribute Browser… that’s even better, thanks!

The attribute browser is great. Also, having an agent for each tag has some advantages, like being able to take notes in that agent’s $Text, examining the agents in a map, creating links from one tag agent to the next, and the like.

So I’ll post here how I did that. Hope it’s helpful to someone, and would be very interested in hearing how this process could be improved!

  1. Created a prototype, TagCollector, with attributes:
  • $AgentQuery: $Tags.contains($Name(agent))
  • $DisplayExpression: $Name + " (" + $ChildCount + ")"
  1. Created a container: /TagAgents/
  2. Created a stamp, createTagAgents:
$MySet = collect(all, $Tags).unique.sort;
$MySet.each(X){
	createAgent("/TagAgents/" + X);
	};
$Prototype(children) = "TagCollector";
  1. Applied the stamp to /TagAgents/ whenever I wanted to update the agents.

Couple notes on this:

  • The stamp will not remove old unused tags. Unused tags will appear as tag agents with zero children and can be removed manually.
  • I usually work with “Update Agents Automatically” turned off if I have a lot of tags.
  • createTagAgents could also be set up as a rule or edict, but that also might be too demanding.

I sometimes want to see how a tag relates to other tags, so I created a stamp to link from a given tag agent to another tag agent if there are notes that use both tags. linkToSharedTags:

$MySet = collect(children, $Tags).unique;
$MySet.each(X){
	linkTo("/TagAgents/"+X);
	};

I’ve attached a file with these stamps and the prototype. Hope this helps. Also, feedback will be much appreciated.
TagAgents.tbx (139.1 KB)

2 Likes

Interesting. In your ‘real’ work files, how many tags are you tracking via discrete agents. Do you have a master on/off switch for the latter?

If the agents mainly exist to report the use count, an alternate approach—and one that might work better at scale—is to make a note per discrete $Tags value. You can do this using values(). You can then use linkToOriginal() and other (un)linking tools to connect the note to note using that tag or disconnect notes no longer using the tag. Your tag notes’ $Text can be annotated as would be the case with the agent approach. Depending on the number of discrete tags you can use rules, edicts or stamps to link/unlink. As the links may run between different containers, don’t overlook hyperbolic view as a means to review a tag’s associated notes.

There’s no ‘best’ solution here. the two main variables are the style of set up (what suits our personal style/expertise) and how you want to visualise the resulting relationships between notes.

Hi Mark, you’ve given me some good ideas about how to get what I need without overloading the document with agents by collecting just the values I need and storing them in a note devoted to a given tag. Much appreciated!

Interesting. In your ‘real’ work files, how many tags are you tracking via discrete agents. Do you have a master on/off switch for the latter?

I’m tracking lots of tags, and as I mentioned I work with automatic agent updates turned off. I guess that’s my “on/off” switch. I also tend to avoid rules and edicts in favor of stamps; most of the time I don’t need info updated in the background and prefer the “on demand” aspects of stamps and the control that they provide.

… make a note per discrete $Tags value. You can do this using values(). You can then use linkToOriginal() and other (un)linking tools to connect the note to note using that tag …

My Tinderbox skills are too weak to follow this. You can batch-create multiple notes with values()? How do you do that? And how do you use linkToOriginal?

Is there an example Tinderbox document that shows off this kind of thing?

Thanks!
Robert

The values() operator gets a Set (a de-duped list, sorted case-sensitively) of all discrete values for the specified attribute. Optionally, you can set the scope of the notes from which to collect values—the default is whoe document.

The old method of making notes from this list which I note as you may see it described was to re-format the list with a line break between each value, set that as a $Text of a note and explode3 the lines as new notes.

The quicker current way is to take the list, iterate the value list and with each list item use create() to make a note whose $Name is that value.

So, for example:

// make per-tag notes in container at path '/My Tags' 
// first store the path for later use
var:string vPath = "/My Tags";
// collect all the values for attribute 'Tags'
var:list: vValues = values("Tags");
// iterate the list
vList.each(aValue){
   // make a note per value in the tag list in the given container
   create(vPath, aValue);
}

A less verbose option without the variables:

values("Tags").each(aValue){
   create("My Tags", aValue);
}

Here is a worked example, with the code run as the rule of note ‘Cast List’, capturing members of the Futurama show:

We can see that ‘Farnsworth’ was incorrectly spelt in one case as we have capitalised and all-lowercase notes for that character. As the values() call is scoped to children of ‘some Data’, the $Tags values for ‘A note’ are not collected.

Once you are happy the method works, re-visit the documentation for values() and create() so you can customise the code to sort your own scenario. Try writing the code following the example rather than copy/paste/editing as that way you’ll get a feel for the parts of the process.

See also the A-Za-z sorting of the Set generated by values(). The order of the notes created reflects the order in the source Set-type data, i.e. the order of the iteration. In this demo, no sort is set in the ‘Cast List’ container, though that could be done if desired.

Here is the TBX for the above worked example (as in the screen grab above): Cast-List.tbx (186.8 KB)

A further thing to consider, in this general pattern of work, is adding explicit links to or from the per-value notes and those notes using that value. This is not a requirement in most cases if you just want to visualise the usage as Attribute Browser fills the need without any links being made. By comparison if you wanted to use Hyperbolic view to investigate a value and the notes using it, you would need to add links.

As usual, there is no one ‘right’ approach, what each user needs will depend on their personal style and requirments.

Hi Mark, you also suggested, and Robert asked about, how to link these notes created from tags to the notes that have that tag. You mentioned linkToOriginal().

For what it’s worth, my implementation of your suggestion used linkTo() in a stamp applied to a note created from the list of $Tags, and creates a link of type “TaggedNote” (or whatever you want to call that type of link).

var vName = $Name;
linkTo(find($Tags.contains(vName)), "TaggedNote");

The number of links can be included in the $DisplayName with the expression $Name + " (" + $OutboundLinkCount + ")".

And the stamp below should retrieve any attributes from the linked notes and place them in the $Text of the note named after the tag:

$Text = links.outbound."TaggedNote".$Name;

(replace $Name with whatever attribute you are interested in)

Mark, even though there’s no one right way to do these things, I’d be glad to hear of different/better ways. Thanks!

1 Like

Looks pretty good to me. :slight_smile:

The difference of linkto() and linkToOriginal() is simply that the latter ensures the link created is between original notes. that can be useful if using an agent to make links (where the action is being run on an alias of notes. The same holds for linkFrom() vs. linkFromOriginal().

It’s a good idea to give links a type: here, yours is ‘Tagged Note’. Then, iIf for any reason you’ve other sorts of links, other links from the note that aren’t to tags, you can use links() to get a count only of the tagging notes:

$MyCount = (links.outbound."TaggedNote".$Name).count;
1 Like

Wow! Thank you for putting together that example.

I didn’t realize Tinderbox had support for scripting note creation. And I also didn’t know that you could write rules, etc with that kind of syntax.

So if you can create notes with create("path/to/note"), can you also assign values to that note’s attributes?

Something like:

note = create("path/to/note");
note.Prototype = "CoolNotePrototype";

Almost exactly right! But there are a few syntax changes:

var:string note=create("path/to/note");  // ← create() returns a path to the note
$Prototype(note) = "CoolNotePrototype";  // ← note attribute syntax

In Tinderbox, we refer to attributes as $AttributeName. The note to whose attribute is described in parentheses; if no note is designated, this is assumed.

You stumbled upon a SUPER POWERFUL skill in Tinderbox. I use this capability all the time!

Wowow! So putting this together with the idea of making “tag notes” with links to tagged notes, you could have a note to create a collection of “tag notes” at, say, /Collections/tags with this edict:

values("Tags").each(tag){
    var:string path = create($Path, tag);
    $Prototype(path) = "LinksToTag";
}

And a prototype LinksToTag with:

var:string name = $Name;
find($Tags.contains(name)).each(path) {
    linkToOriginal(path, name + " tag");
}

I think that’s close to what you were doing with stamps. Maybe?

PS I tried find($Tags.contains($Name)) but that found notes where a tag matched the found note’s name, not the name of ‘this note where I’m writing the edict’. That surprised me, but I guess there’s something to be learned there about how queries are evaluated.

PSS I now see that the $Attribute(path) syntax is described nicely in “Getting Started with Tinderbox,” which I had completely missed. Are there antecedents for that kind of syntax or is it unique to Tinderbox? Not a big deal, just curious.

Lots of precedent, though I can’t recall precisely what I had in mind. Back in 2002 or so, a 14-year-old Aaron Swartz visited Eastgate and told us we ought to use Path syntax; he was not wrong.

1 Like

The only difference between a rule, an edict and a stamp is periodicity and scope. A rule runs constantly on its ‘host’ note. An edict does the same, nut deliberately less frequently as in big/heavily coded docs rules may become a bottleneck. Essentially an edict aims to run on start-up and then c.1 and hour, although it can be triggered on demands: see more.

A stamp differs in scope and timing. A stamp is an action that can be run on selection of one or more note(s). A stamp is always run on demand and execute once on each selected note.

So by moving code from a stamp to an edict you are:

  • restricting it to running on the note holding the edict
  • running whenever the edict is triggered.

Of course, if the edict is set in a prototype, then all notes using the prototype inherit the same edict by default. The prototype approach makes it easy to correct 00s of notes simply by editing the rule or edict in the prototype.

A find() is a query—as you’d use in an agent. In queries a reference, e.g. $Name, refers to the note being tested, i.e. every note—in turn—in the scope of the query.

If you want to test for an attribute value in the calling note, e.g. here the note running the edict, you can use the designator ‘that’ (see more). Thus your edict code:

var:string name = $Name;
find($Tags.contains(name)).each(path) {
    linkToOriginal(path, name + " tag");
}

can alternatively be written—to the same outcome— as:

find($Tags.contains($Name(that))).each(path) {
    linkToOriginal(path, name + " tag");
}

If a note is called ‘ant’, the above code will find and link to any (original) note where that tested note includes a $Tags value ‘ant’. Thus, in my test below, a note called ‘ant’ links to the originals of two notes that contain a discrete $Tags value of ‘ant’:

Looking at the note running the edict we see it has 2 outbound links of linktype “ant tag”:

Here is the test document: find-tag1.tbx (121.1 KB)

What were you expecting to happen?

As regards specifying offset references for attributes, i.e. getting/setting an attribute is a different note from the current one, see more here.

Action code has evolved greatly over the life of the apps with syntax gaining in capability over time. I believe it borrows from conventions of a number of sources (but not formally recorded as such). A useful way to compare this is by looking at older versions of aTbRef, all of which are still online.

Action code is best understood in terms of the available Tinderbox documentation (app Help, app Help menu tutorials, and aTbRef), rather than trying to derive syntax from some other language. In parts it may look like [some language] but that doesn’t necessaerily apply across all code.

[Disclaimer re aTbRef: I am aTbRef’s (sole) author]

†. Most users will never meet this. It generally relates to ‘power users’ doing complex tasks or a small subset of new users who just apply rules to everything and then wonder why the app is working hard.

This is all obvious now that I understand it. Remembering that there’s an implicit (this) after $attributes helps me read the expressions.

Thanks for all the pointers. :wink:

1 Like