Deciding whether to use a rule or an agent

I am using both rules and agents but I find that I am confused as to which one would be best under some given circumstances (assuming there even is a “best”). As a simple example, if I want to change the badge of a note based on an attribute value, would that be a rule in the prototype or an agent that selects for the prototype and then has an action? Presumably both would work, but is there an advantage/disadvantage to using one over the other?

All things being equal, agents use less computational power. Also, agents work nicely together, so building an agent that gathers some useful group of notes today may also make a second agent easier to make tomorrow.

Use rules when you can’t easily define the agent query, where you’d like the constraint to be enforced more frequently. or when it’s simply easier for some reason.

1 Like

Advanced tip. Agents create an alias for each note matched and aliases run rules, as well as orignials. So, if using an agent to do tasks on notes running rules, try to craft the query so the agent action result stops a note matching the query.

2 Likes

Mark A, thanks for this clarification too. But I wonder if you could explain a little more about the sentence above? Or an example of what one should do, and not do? Thanks.

If you find yourself putting if clauses in your rule, you’re probably, unintentionally, doing filtering that might more efficiently be done at query level. But, to do that you might have to think through your data a bit. Just closing up here, I’ll try and give a better answer tomorrow.

2 Likes

Thanks, I understand the point now.

1 Like

@JFallows Thanks, that’s saved me another essay!

Mark A, I am going to drag you back into essay-writing! Or at least short explanation writing.

Earlier in this thread you said that it made sense to move filtering work from Rules (with “if”) to Agents (with queries), for computational efficiency. Let me see if I follow that, with this example:

  • I have a series of containers, with a *Projects prototype. Their child items are to-do tasks for each project, with a *Tasks prototype.

  • I have wanted to flag Project containers that include items that are not yet done, and whose due date has arrived. So I set up a rule for the *Projects prototype, thus:
    if( any (children, !$Done & $DueDate<=date(today) ) ) {$Badge=“flag yellow”}
    That looks fairly clotted, but again it’s just testing to see if anything is already due, and is not yet done.

  • Would it be theoretically better to have an agent someplace called “DueNow,” whose query was $DueDate<=date(today) and whose action was to set a new attribute, thus: $DueNow=true. And then I could change the rule for *Project to
    if(any (children, $DueNow & !$Done) ) {$Badge=“flag yellow” Better? Worse? Merely different? Or to streamline it even more, to have the query for DueNow as !$Done & $DueDate<=date(today), leaving the rule simply as if( any(children,$DueNow) )? More efficient still?

Thanks. Again just asking to understand-by-doing the ins and outs of these codes.

I’ll start by saying, for the benefit of readers just starting out, that the matters here have little effect on a document with a few tens of notes. It is when you’ve many hundreds or thousands of them (or intend to scale to that size) that some consideration is warranted. It’s also important to say that this isn’t like building a database - you don’t have to get it all correct and set-yup before you start. Tinderbox is very tolerant of emergent structure and letting you easily re-structure your notes and action code. With that proviso out of the way.

As MB has pointed out: “All things being equal, agents use less computational power.”, i.e. if the agent and rule are essentially doing the same task. But if designing for efficiency you also want to consider scope. If an agent is searching the whole hundreds of notes just to always find the same 2 items and apply some action, then it’s likely more efficient to put a rule in each of the 2 notes.

A rule runs every cycle and not just in the original but every alias of it as well. Consider a complex rule (e.g. searching all notes for those whose text contains a value in one of this note’s attributes, e.g. a note on an author finding all notes mentioning that author, and to that for 50 different authors. Now consider other agents doing various tasks each have aliases of most per-author notes. So now, you’ve maybe 4 x 50 (original + 3 aliases of each), so now you’ve 200 iterations of that rule. An agent de-dupes so matches each note only once so if the rule code were moved to an agent, the code would run only 50 items per cycle instead of 200 times.

Scope it pertinent in your case. The action only needs to run on containers of a certain prototype, So the query wants to be designed to cut the number of notes tested for the matching the query, before we even get to running any actions. In your second example, you propose a query of $DueDate<=date(today) which has to test every note in the doc when we know only containers using the prototype *Projects are of interest. This would be a better query:

$Prototype=="*Projects"

Better yet, this query:

$Prototype=="*Projects" & any(children(original),!$Done & $DueDate<=date(today))

I did test this in v7 as, given the essentially nested query, I wasn’t 100% sure it works - but it does. Note that the any() argument is run only on notes that have the correct prototype, thus cutting the amount of work done overall. Now, the only items the agent has to act upon are those for which you want to set a badge. Agent action is now:

$Badge="flag yellow";

Here, we’ve abstracted all the querying away to the agent query. Bear in mind though that if each project had 110s of direct children (not descendants in aggregate) you might want to rethink the process. This brings out the point that there is no one right way. In a very small file, any valid code that works is probably OK. It’s only as your note count rises and the degree of action code use rises that you want to be open to re-writing your actions for efficiency.

One aspect is periodicity. Instead of rules, you can consider edicts (essentially rules run very infrequently). Or, for agents consider the agent’s priority ($AgentPriority). Lower priority agents don’t run every agent cycle and agents can actually be turned off - they retain their aliases if any but don’t act on them, although the rules in those aliases still run. See how complex it potentially gets, but a reminder to users just starting out, you don’t need to worry about this sort of nuance in basic use of Tinderbox.

FWIW, aTbRef has over 100 agents (!), 2.2k+ plus notes as well as c.4.4k aliases but only c.25 rules (and most of those are back-of-house for export, code notes, etc.). So, most agents are kept at low priority or off and I generally turn off automatic updating whilst editing. In part this is because many agents only exist to show a listing of, e.g. attributes, of a certain type or purpose. Such agents generally have no action and usually only add if a new attribute gets added. By comparison, users doing CTD or date-based project management are likely to have quite a lot of query/actions code triggering of dates or completions and may do well to review their code as their docs grow. But if you’re not having issues of timeliness, don’t feel the need to fiddle unless it’s out of interest. Conversely, apparent slow-down might be a sign you want to review your code. In such a case, do ask here for help in cases of bafflement! Remember that code samples, or better, a link to a (test) TBX showing the general issue as then we have a common frame of reference.

I hope that’s helped. Not sure… </essay>

1 Like

Mark A, sincere thanks! By spelling it out this clearly and patiently, I know that you have helped me, and I trust that you’ve informed lots of other people who will read this thread now and into the future.

I hadn’t stopped to think that I could just get all this work done in a single query (shown above), rather than by rules or a combo of queries and rules, but you’ve explained how to do that and why and when it would have advantages. Thanks for taking this time to give us all this look into the program’s workings and tradeoffs.

2 Likes

Mark A, here is a followup, which I think can be handled in a very terse answer! Again I ask for learning-by-doing purposes. Scenario:

  • I have the query, discussed above, for finding Projects that have due-but-undone items inside them, and flagging them with a certain badge.
  • I would like to have a way to clear that badge once a project no longer has any such undone, due items inside it.
  • If I were doing this via Rule, I could solve it with an else construction: if certain conditions are met, set the flag one way, else set it another way.
  • But since agents don’t work with “else” structures in the same way, I’d need to create another agent that looks for projects that no longer meet the criteria, and changes them back. (Otherwise, any project that ever contained an undone item would retain the flag.)

So, what is the simplest way to write an agent that searches for items with the right prototype that do not meet the criteria for the previous agent? That is, what is the right sytax for a query whose intention is: *$Prototype=="Projects" & [not chosen by the agent that searches for undone items] ?

Thanks! A line of the right code will suffice this time!

Update/pentimento: Or might the answer be to complexify a single agent? Thus, its query could be just to look for the given $Prototype. But then its action could have the if/else structure. If certain conditions are met (namely undone-and-due items), then set the flag one way; else clear the flag.

Would this accomplish what I want? Or just get me deeper into Rube Goldberg land?

How about this (slightly different) query?

$Badge==“flag yellow” & !(inside(/path/to/theAgent))

where theAgent is the agent that finds due-but-undone items. This locates notes that have been flagged, but that are no longer eligible.

A new and fancy approach would be to use the new OnRemove action.

OnAdd: $Badge=“flag yellow”;
OnRemove: $Badge=;

Thanks!

inside() was in fact what I was looking for, but had forgotten the right name of. And the spiffy new OnRemove also seems designed for purposes just as this. (Kind of the poor man’s else, right?) Appreciate it.

Incremental formalisation in action. Having solved one thing, we now want to add another. The temptation is to write another agent, but first I’d step back and clarify the actual logic (of which I’m still not 100% certain. Let’s assume (if if not the case yet) that if all project items are $Done then so should be the project container. Badges are yellow flag for overdue and nothing (default otherwise. The latter’s important, if other badge values are in the mix you need to allow for that.

Now we will make a new, replacement agent for the one above. It queries for all containers of the “*Projects” prototype and where $Done is false, IOW, we only look at incomplete projects and ignore completed ones. That’s the query done. Then we do the rest of the work in the query action.

Query: $Prototype=="*Projects" & $Done==false

I’m using the full version of a true/false test to aid other readers. More expert folk know how to shorten that if desired.

Now the action is:

if(every(children(original),$Done==true)){
	$Done=true; $Badge=;
}else{
	if(any(children(original),$Done==false&$DueDate<=date(today))){
		$Badge="yellow flag";
	}else{
		$Badge=;
	}
}

The outer part of the action checks if every child is $Done. If so it makes the project container as $Done and resets the badge. As $Done is now true for this project it falls out of the agent next cycle as it no longer matches the query. This is the original point about changing something so that the changed item falls out of scope of the query. This is efficient unless you truly want the object to have the action applied every cycle.

The nested if deals with part-complete or un-started projects. The first branch does the same as our earlier agent above. The ‘else’ branch deals with items where there are incomplete items which have not reached due dates and there are no overdue dates. Thus we can safely reset $Badge.

The result of this is that a yellow flag is seen only for incomplete projects and then only if one or more items are not $Done and are on or past their due date.

The action might seem complex but it handles all the branches and unlike a rule is run only in the agent, thus a maximum once per action cycle, whereas a note with a rule and aliases would run it discretely once for the original and then each alias.

As the logic of what you are trying to achieve, it’s worth roughing it out on paper (or in a separate doc as a map) to ensure there are no unhandled branches that give you the wrong feedback.

Same reminder for applies for those new to Tinderbox: this sort of thinking makes no real impact in a small document with only a few notes and actions.

Mark A: Once again sincere thanks for this careful and illuminating discussion. The reason I’m putting you through this – rather, the reason I’m grateful you’re doing it – is that over the years I’ve learned a tremendous amount from this kind of back-and-forth, and I imagine that future readers wrestling with related-but-different challenges might find the same.

So, thank you. (And your branching structure makes sense.)

Mark B: Thanks as well for your suggestions. On first experimentation, a simple, works-like-a-charm solution to my problems is:

  • Agent query as we’ve described, to find Projects that have un-done, now-due items within them;
  • Agent action to flag all such Project-containers with an appropriate danger sign;
  • Agent on-remove that simply says $Badge=;. As Mark A points out, this means I’m just using two varieties of badge, but that’s OK for me.

Really appreciate the careful tutelage here. And now I’ll think about the implications in other data bases of the branching approach Mark A lays out.