Automation of actions using Drafts

Applescript requires application identifiers with exactly four ASCII characters (so they can all fit in one 32-byte word and be compared with one instruction: such, such were the cares).

2 Likes

I’ve been using the Drafts-to-Tinderbox action for sometime to create new Notes in Tinderbox, always successfully!

Now, I have a new use case: send a Drafts document to a particular Note in Tinderbox.

The Tinderbox Note I’m updating has a $Name of the format “yyyy-mm-dd” and if I can locate this Note then I’d like to update the $Text with the Draft contents.

Thanks for any guidance!

Copy and paste, maybe? Especially if it’s just one note to update. (My reading of the request.)

@ptc97504, great question. I tried editing the AppleScript, but clearly I’m out of my depth. For someone that actually knows what they’re doing, let’s provide a few more design parameters.

  1. How would you specify the target note name, or better yet, the path? I would suggest it would be the first line of the Drafts note.
  2. Would you want the the text of the target note over written or appended it?
  3. Might it make sense to add the new “update” note to as a child to the target note, so that you have some revision history.
  4. What other behaviors in the experience might you like to see?

Excellent! To make it really useful, I’d add the action code from this Forum about capturing $Text using the stream action code and build attributes in the note. Food for thought!

Stream parsing is slightly different to other operators as they are used in multiples (chained together) but which ones and how is very scenario dependent. Also for a single-pass parsing your targets for extraction need consistent unchanging makers and in a consistent order.

Consider: an email addressee name is prefixed with a 'To: " string and the ‘To:’ always after "From: ", etc. You can navigate around optional (i.e. possibly there) targets. But if “To: '” turns up several times or none and in no particular place a better approach for $Text capture to attributes is loop though the the $Text paragraphs and do all the tests on each.

So, a bit more about what you want the stream parsing to capture would be good. In order start to finish of $Text what are the markers for the info to capture, and how far after the maker does the desired text extent (word, to end of line, etc.?)

Note that stream parsing capture operators that match markers for text of interest, e.g. the email "To: " are case sensitive so if the operator is looking for "To: ", it will not match :to: ".

Mark, thanks for taking a look at this … humbling for me!

I have a daily workflow that goes like this:

  1. Upon waking, an iOS Shortcuts kicks-off either manually or automatically that creates a Drafts document with yyyy-mm-dd as the first line
  2. The text of the Drafts document comes from various Shortcuts-aware iOS apps, and looks like this (with sample results):
    note-time: 06:19
    day-of-year: 212
    daylength: 14:29
    precip-chance: 0
    hightemp: 91
    aqi: 43
    sleep: 7:25
    readiness: 2.4
    event:
    event:
    log:
    log:
    log:

Here’s what I’m trying to accomplish:

  1. send the Drafts document to Tinderbox and find the Tinderbox document “yyyy-mm-dd” in the “Daily” container with the list of elements above moved into the $Text attribute
  2. convert each “:” line into the attribute name in the Note.

What do I do with these Notes? I have them linked (via link from code, thanks to a previous Forum post of yours) and I browse through dates and weeks that way; I also use Attribute Browser to look at aqi across time, etc.

Thanks in advance for any clues to my next steps!

So the Event attribute needs to be a List as there is more than one event per imported note, same for log items. So you’ll needs some user attributes:

NoteTime (String type - for reasons we can look at later)
DayOfYear (Number type)
LogDate (Date type - it is useful to have the day’s date in Date data for some calculations, even if not shown in Displayed Attributes)
DayLength (Interval type)
PrecipitationChance (Number type - presume this is a %)
TempMax (Number type)
TempMin (Number type - if needed)
AQI (Number type)
Sleep (Interval)
Readiness (Number type)
Events (List type) as we’ll leave this as $text - see below
LogItems (List type) as we’ll leave this as $text - see below

I generally suggest using attributes that follow Tinderbox’s style of attribute naming as it lessens the chance of an edge case getting the action code confused as to intent. Also, attribute names don’t allow hyphens. A good suggestion is to use plurals in the names of multi-value attributes (Lists/Sets) as they remind you that they may have more than one value.

So, I think we’re expecting an imported source note like this:

2023-07-31
note-time: 06:19
day-of-year: 212
daylength: 14:29
precip-chance: 0
hightemp: 91
aqi: 43
sleep: 7:25
readiness: 2.4
event: event 1 info.
event: event 2 info
log: log item #1
log: log item #2
log: log item #3

However, as the events and log items are variable in number and of unknown duration they are perhaps better left in $Text—of the Tinderbox target note. IOW, the other data goes to attributes but the text of note ‘2023-07-31’ would be just:

event: event 1 info.
event: event 2 info
log: log item #1
log: log item #2
log: log item #3

There are all sorts of other wrinkles to try, but a tip: don’t try and do everything at once.

So step one, take our source note, look for (or create) the target Tinderbox and detect move the source info to appropriate attribute and events/logs into the note’s $Text. yuo could then delete the source not, but I’d advise you don’t do that until the import process is tested and running OK (again: not doing too many things at once.

Here’s the blank start. Note the descriptions for the User attributes: DayDataImport-01.tbx (210.8 KB)

Next to process the data. Note this is all an example. the general process can be done several ways and different people have different styles. So i’m doing your task, within your constraints in my style of ‘coding’ as it were.

†. Why note a Date type? Because likely to want these attributes as Displayed Attributes items and you can’t set per-attribute formatting in the table.

‡. I assume this is in hourse:minutes not hours:demical fraction of hours.

Tada! DayDataImport-02.tbx (225.6 KB)

In the following I set a non-zero value for precipitation (of 20) on your original example so we properly test value extraction. The result of applying the above TBX’s stamp “Process Daily Data” on the note at /import/text is this:

The stamp is this:

var:string vLogStub = "/Daily logs/";
var:string vSourceDate = $Text.paragraphList[0];
var:string vTargetPath = vLogStub + vSourceDate;
var:string vNT;
var:number vDoY;
var:number vDL;
var:number vPC;
var:number vTM;
var:number vAQI;
var:number vSleep;
var:number vReady;
var:string vText;

create(vTargetPath);
$LogDate(vTargetPath) = date(vSourceDate+" 08:00:00");
//$Text(/log) = vTargetPath;
$Text.skipTo("note-time: ").captureLine(vNT).skipTo("day-of-year: ").captureLine(vDoY).skipTo("daylength: ").captureLine(vDL).skipTo("precip-chance: ").captureLine(vPC).skipTo("hightemp: ").captureLine(vTM).skipTo("aqi: ").captureLine(vAQI).skipTo("sleep: ").captureLine(vSleep).skipTo("readiness: ").captureLine(vReady).expect("event:").captureRest(vText);
$NoteTime(vTargetPath) = vNT;
$DayOfYear(vTargetPath) = vDoY;
$DayLength(vTargetPath) = vDL;
$PrecipitationChance(vTargetPath) = vPC;
$TempMax(vTargetPath) = vTM;
$AQIValue(vTargetPath) = vAQI;
$Sleep(vTargetPath) = vSleep;
$Readiness(vTargetPath) = vReady;
$Text(vTargetPath) = vText.trim();

Note that stream processing normally expects to extract to attributes of the note being processed. so, we extract to a variable and use that to set the attributes in the new Tinderbox note.

The stamp first makes the notes if not found. The path to the note is set as a string but could for instance be a location in a config note, etc. IOW, so you can easily change or customise the location. Using create() either creates the note and returns its path or if it exists it returns the path. We need that later Next…

We step through the structured log and use .skipTo() to find the next marker, including any trailing space, then capture the rest of the line into a variable using .captureLine(). this steps the stream cursor to the next line (paragraph) in the source $Text. We repeat the same chained operations for each of the source tags up to ‘readiness’. We then check the text immediately after the stream cursor is 'event: ’ (note we ‘expect’, not ‘skip’) and then use captureRest() to capture the remainder of the source $Text into a variable.

Now using the path to the new Tinderbox note we write the variable value into the appropriate attribute. In the case of the event/log text I added a .trim(0 as it seemed to acquire a leading white space character en route.

The result is as you see.

There are all sorts of ways to improve on this but I think that gives yo a pretty good idea of the process. I’m actually quite pleased, this is the first time i’ve really used the stream processing in this depth and it works like a charm. A big help is that your data is very rigidly formatted with no ambiguity. Next you might consider if the real data is that clean and whether you need to do any tests. Luckily all the parts captured to attributes will accept a zero/no value input and not break but a Number attribute might not like being fed a value of ‘n/a’, for instance.

So the next step is testing with actual data. Then you can then review/re-name the input and log folders and consider increasing the degree of automation…

I’ve used a stamp for control —you don’t tend to want to test this sort of task in a rule lest you make a never-ending task. But once the stamp works you don’t have to use a stamp. Note that an OnAdd can call a stamp (using stamp() so the source folder, here at path ‘/import’ could actually run the above stamp on any note added to it. But again, test for glitches in a more manual fashion before automating more fully.

Don’t forget, i’ve merely taken the instructions/test data you provided. If you need slightly different use this as an example from which to customise.

I hope that helps! :slight_smile:

1 Like

Mark, thank you for the masterclass in Tinderbox! Tasks like these boost my productivity and make every follow-on Tinderbox project more interesting and effective. I now start my day with 4 or 5 Tinderbox documents open for each of my projects and every one gets better with what I’m learning on the Forum. Thanks so much …

2 Likes

You might wonder, do I have to chain so many operators together? I asked of @eastgate:

If I have code like:

$Text.skipTo("note-time: ").captureLine("NoteTime").skipTo("day-of-year: ").captureLine("DayOfYear")….

But so I can comment, I break like so:

$Text.skipTo("note-time: ").captureLine("NoteTime")
$Text.skipTo("day-of-year: ").captureLine("DayOfYear")….

does this matter? As it happens the anchors are in the coded sequence and occur only once. But I wonder if renewed $Text calls re-start the stream process. (Or, is there an efficiency impact?)

@eastgate clarified:

This is slightly less efficient, but the difference is likely to be milliseconds. Yes, each stream starts with the entire $Text, but .skipTo is quite fast.

So, breaking out the long stream parse line like up-thread above into discrete lines starting $Text. will start a new stream parse for each line. In this scenario the data format is so tight and unambiguous that there can be only one match.

Were we to re-write that long parse code line as:

...
$Text.skipTo("note-time: ").captureLine(vNT);
$Text.skipTo("day-of-year: ").captureLine(vDoY);
$Text.skipTo("daylength: ").captureLine(vDL);
$Text.skipTo("precip-chance: ").captureLine(vPC);
$Text.skipTo("hightemp: ").captureLine(vTM);
$Text.skipTo("aqi: ").captureLine(vAQI);
$Text.skipTo("sleep: ").captureLine(vSleep);
$Text.skipTo("readiness: ").captureLine(vReady);
$Text.expect("event:").captureRest(vText);
...

It would certainly be easier to read or comment each discrete capture. But, where might such as easier-to-read-code layout bite us?

Well, imagine if there were (just because) _two 'day length: ’ labels, the first occurring in $Text before 'day-of-year: ', and we wanted the second. If we broke the long parse stream call into a series of short calls, then $Text.skipTo("daylength: ").captureLine(vDL) would capture the first day length: ’ value because each $Text-anchored parse starts at the beginning of $Text.

Also the last .expect call would fail because expect tests the text immediately following the stream parse cursor position. For a new call that is the start of $Text so it would return all of $Text. But that would mean the first 'event: ’ label, having already been passed by the stream cursor would be missing from the capture. However , we could use:

$Text.skipTo("readiness: ").skipLine.expect("event:").captureRest(vText);

As we are now starting from the beginning of Text it doesn’t matter that we again start at 'readiness: '. Why do so? Because is the last known easily skipped-to label before the content we want. We also know the desired text on on the line after that, thus the .skipLine operator.

Neat. Break for new posts this one is long. Stay tuned…

OK so now we have a rule that’s easier to read:

// get path of container for storing new log notes
var:string vLogStub = $LogFolder("DocConfig");
// get date of the imported note
var:string vSourceDate = $Text.paragraphList[0];
// make path for new log note
var:string vTargetPath = vLogStub + "/" + vSourceDate;
// variables to receive data captured by stream parse
var:string vNT;
var:number vDoY;
var:number vDL;
var:number vPC;
var:number vTM;
var:number vAQI;
var:number vSleep;
var:number vReady;
var:string vText;

create(vTargetPath);
$LogDate(vTargetPath) = date(vSourceDate+" 08:00:00");
//$Text(/log) = vTargetPath;

// Capture the data for the note time label
$Text.skipTo("note-time: ").captureLine(vNT);
// Capture the data for the day of year label
$Text.skipTo("day-of-year: ").captureLine(vDoY);
// etc....
$Text.skipTo("daylength: ").captureLine(vDL);
$Text.skipTo("precip-chance: ").captureLine(vPC);
$Text.skipTo("hightemp: ").captureLine(vTM);
$Text.skipTo("aqi: ").captureLine(vAQI);
$Text.skipTo("sleep: ").captureLine(vSleep);
$Text.skipTo("readiness: ").captureLine(vReady);
// lastly, capture the rest of the source as the $Text of the new note
// NB need to find the preceding label and skip a line to capture all
//  the remaining text including labels.
$Text.skipTo("readiness: ").skipLine.expect("event:").captureRest(vText);

// use tha variables to set the relevant attributes in the new TBX note,
// using the vTargetPath variable to give the correct offset address
$NoteTime(vTargetPath) = vNT;
$DayOfYear(vTargetPath) = vDoY;
$DayLength(vTargetPath) = vDL;
$PrecipitationChance(vTargetPath) = vPC;
$TempMax(vTargetPath) = vTM;
$AQIValue(vTargetPath) = vAQI;
$Sleep(vTargetPath) = vSleep;
$Readiness(vTargetPath) = vReady;
$Text(vTargetPath) = vText.trim(); // trim stray leading/trailing space

// mark this note as processed
$IsProcessed = true;

An I’ve added an extra tweak and a new user attribute ‘IsProcessed’ which we’ll use to mark the fact this input is processed.

I’ve also parameterised some key locations, stored in a root level note ‘DocConfig’:

To change the name/path of any of these you just update the attributes and the actions using the data acts accordingly. Doing this for a personal project might be over ornate but, as this is a public demo, perhaps not. For instance, someone might not want to use ‘Daily Logs’ for the new notes nor have to check all the action code for where that path might be hard-coded. Using the ‘DocConfig’ that can make the change in one place.

Next, we can give the Imports container (where are data from Drafts is being placed) this rule:

// collect only children where $IsProcessed is true
collect_if(children,$IsProcessed==true,$IDString).each(anIDStr){
  // move to 
  $Container(anIDStr)= $ProcessedFolder("DocConfig");
};

Now the Import container constantly checks its children and if any note has a true value for $IsProcessed the note is move to the assigned folder, in this case at path /Processed Imports:

I the TBX, i’ve temporarily turned on column view and displayed ‘IsProcessed’ so I can check the code is working as intended. Sure enough, note ‘text’ has been processed to make note ‘2023-07-31’ and then moved to the ‘Processed Import’ container.

Note that doing the move of the processed notes via a rule might, as the doc grows be more ‘always on’ than needed so the same code could be used as an Edict or stamp. another approach might be to have an agent that looks at the Import container and scans for/processes actioned notes. The latter works fine, but the rule approach seems neater as we’ve only one container to monitor.

Once the processed notes have been moved to the processed container you can leave the notes as an archive or review/delete them. Or, you might add an edict there that deletes any child notes over a month or a week old. (I’ve not implemented that here but see the delete() operator). But, something like:

collect(children,$Path).each(aPath){
   var:string vSourceDate = $Text(aPath).paragraphList[0];
   var:date vDate = date(vSourceDate);
   if(vDate & vDate<date("today - 1 week")){
      delete(aPath);
   }
};

I’ve updated the TBX:

  • some new user attributes (see their description info)
  • a docConfig note to set key locations
  • column view is enable and $IsProcessed displayed - you can turn this off once you’re happy with the process.
  • a new rule (code above) as well as the old one (now renamed ‘Process Day Data - orig’)
  • a new rule for the Import container to move processed notes
  • set an edict (currently disabled) in the Processed Imports container to delete child notes over 1 week old. Use action-based deletion with caution and always test first before fully automated use.
  • the root level ‘log’ note is for logging from actions—see a commented out logging call in the new stamp.
  • the ‘specimen text’ folder holds a copy of the test note in case you delete the original copy by mistake. The example can also be copied customised to try different values.

… I think that’s everything.

Here is the revised document with the above improvements: DayDataImport-03.tbx (254.0 KB)

So, download, open the TBX, select note ‘test’ in the ‘Import’ container and then apply stamp “Process Day Data”, and what the new day note be made and the source note being moved to the ‘Processed Imports’ container.

1 Like

Meant to add for @satikusala this might make a useful background doc(s) to a meet-up topic.

Yes, BTW, this thread seems to have strayed from the original question, at least the question that I think was asked: “How do I have a Drafts Action update the $Text of a specific named note in Tinderbox.” It was that question that I could not answer. All the transformations that have arisen since then are straightforward and worthy of future discussion.

Curious, does someone have an answer to the original question?

I was re[lying to the 31 July 2023 note, not the 201 era pre-amble. The post referred to an action which doesn’t explicitly imply AppleScript (only).

OK the missing part is how we receive the ‘specific named note’. An untested assumption is that the name is unique and has a name or path with no problem characters. Otherwise stick the unique name in t a variable , e.g. vPath.

and the answer is as simple as setting:

var:string vPath = "specific named note"; // value received somehow
var:string vNewText = "some text"; // value received somehow
$Text(vPath) = vNewText;

Aside, ‘update text’ is itself ambiguous. Are we adding to or replacing $Text? Rather as with the current thread about ‘just’ getting ‘the answer’, resolving these ambiguities and unstated assumptions do matter for an automated solution.

For applescript, find note in [note or document] with path "/path/to/note" will return an object that you can then use to apply the normal Tinderbox AppleScript methos for setting an attribute. HTH, as I’ve run out of time for a dive into AppleScript syntax.

How about this AppleScript:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

set targetNote to "xyz"
set textValue to "This is a test!"

tell application "Tinderbox 9"
	tell its front document
		tell note targetNote
			set value of (attribute named "Text") to textValue
		end tell
	end tell
end tell

This assumes some other AppleScript actually populates variable targetNote with the desired note name $Title.

1 Like

Returning to the branch of this that is the stream processing of info once in Tinderbox, we can go a step further. Recall that the action we created needed to cache the stream captures into a variable (or it could be an attribute as .capture... operators can only save to the current note. But instead of code like

...
var:string vNT;
$Text.skipTo("note-time: ").captureLine(vNT);
$NoteTime(vTargetPath) = vNT;

… we could do the capture inside a function:

function fGetItemValue(iAnchor:string, iIDStr:string){
	var:string vMatch;
	$Text(iIDStr).skipTo(iAnchor).captureLine(vMatch);
	return vMatch;
} 

and then in the stamp (or rule, etc.):

$NoteTime(vTargetPath) = fGetItemValue("note-time: ",vIDStr);

A slight oddity at present is $Text in the function should access the being-stamped note, i.e. the code we want to process. However, at present (v9.6.0) it seems to need an offset address. Here, I use $IDString (cached in variable vIDStr) but $Path, $ID or a unique $Name could suffice. At worst this is being explicit about which note to read.

Another improvement we can make is that previously we’d captured "To: " vs. the less easily mis-set “To:”. Why, as we don’t want the space(s) after the skip-positioned cursor to include leading psaces. We could use .trim() on the extracted value but another way is to use .skipWhitespace as in:

function fGetItemValueAlt(iAnchor:string, iTgtAttr:string, iTgtIDStr:string, iIDStr:string){
	var:string vMatch;
	$Text(iIDStr).skipTo(iAnchor).skipWhitespace.captureLine(vMatch);
	action("$"+iTgtAttr+"("+iTgtIDStr+")="+vMatch);
}

So is a function needed here? Not really. It just provides a nice opportunity for some incremental formalisation.

Now we have functions, we could just pass in the list of anchors and a list of target attribute names. Or safer, a dictionary of attribteName:anchorString. Why that way around? Well, valid attribute names are valid key names. Anchor strings less reliably so. Perhaps one for the next version of this file.

In closing i’d note that whilst were using Stream parsing operators, we’ve moved on from a single-pass text parse as used in the very original version of this file.

Here’s a version of the previous file with the above additions (expect the dictionary-based idea at the end. Open the file slect ‘test’ and apply stamp “Process Day Data 3”. TBX: DayDataImport-05.tbx (309.6 KB)