Pass Dictionary to shortcut from Scriptable

I replied to a old post on this topic but it was kindly suggested that I start a new post to generate some views. I am relatively new to coding having just completed codeacademy’ s intro to JavaScript.

I have written a small script (see code from my post earlier today if interested but not necessary to answer this question) which returns a JavaScript object or dictionary. I would like to be to use the dictionary in a shortcut.

On a older post it was stated that this can only be done through pasteboard or call back URLs.
I dont know much about callback URLs but have heard that they have fallen out of favor but still have a place.

I am open to any solution and would rather not have to use x-callback url if a easy solution exists. Seems we should be able to simply return from our script.

I have made an attempt at code for x-callback url but I am confused about the base url to use. I think I have the concept of adding parameters. Any help would be appreciated. Thanks


let baseurl = "scriptable:///run?scriptName=Multipick"
let cb = new CallbackURL(baseurl)
cb.addParameter("Date", a.Date);
cb.addParameter("Odometer", String(a.Odometer));
cb.addParameter("Trip", String(a.Trip));
cb.addParameter("Mileage", String(a.Mileage));
cb.addParameter("Final_Odometer", String(a.Final_Odometer));
console.log(cb)
cb.open() 

I’ve used the following to run a Shortcut from Scriptable with a parameter that gets picked up as the Shortcut’s input.

Safari.open("shortcuts://run-shortcut?name=Return%20Processor&input=text&text=Return%20Text")

I’m stilll unclear as to the bigger picture on this of what you are switching out of Shortcuts to do (that you can’t already do in Shortcuts). I can see you are doing something with car tracking, but only in terms of data being passed back. To that end I’ll cover various options with the help of some Scriptable scripts and a Shortcuts shortcut.

The Scriptable Scripts

To follow along, copy and paste each of these scripts into a new script entry in Scriptable. The name is very important, and they could be named CarLog1, CarLog2 and CarLog3 respectively for the purposes of this example.

Scriptable Script: CarLog1
// Capture details
let alertInput = new Alert();
alertInput.title = "Enter car log details";
alertInput.message = "Please complete the details in each field below";
alertInput.addTextField("Enter trip description");
alertInput.addTextField("Enter the Initial Odometer Reading here");
alertInput.addTextField("Enter the Final Odometer Reading here");
alertInput.addAction("OK")
await alertInput.presentAlert();

// Populate JSON
let jsonCar = {};
jsonCar.date = new Date;
jsonCar.trip = alertInput.textFieldValue(0);
jsonCar.odometer = alertInput.textFieldValue(1);
jsonCar.final_odometer = alertInput.textFieldValue(2);
jsonCar.mileage = jsonCar.final_odometer - jsonCar.odometer;
console.log(jsonCar);

// Because we have had to switch to the Scriptable app from Shortcuts,
// we have to 'force' our way back with a URL (not x-callback-url as we 
// are returning to shortcuts) call, and use the clipboard as a place to
// transfer the data
Pasteboard.copy(JSON.stringify(jsonCar));
Safari.open("shortcuts://");
Scriptable Script: CarLog2
// Populate JSON without human interaction
let jsonCar = {};
jsonCar.date = new Date;
jsonCar.trip = "Driving from A to B";
jsonCar.odometer = 111;
jsonCar.final_odometer = 333;
jsonCar.mileage = jsonCar.final_odometer - jsonCar.odometer;
console.log(jsonCar);

// We have no alerts in this script, so the script can pass back the results
// to Shortcuts directly. This could be used if you are picking up content from
// a calendar and car information API for example.
Script.setShortcutOutput(JSON.stringify(jsonCar));
Script.complete
Scriptable Script: CarLog3
// Capture details
let alertInput = new Alert();
alertInput.title = "Enter car log details";
alertInput.message = "Please complete the details in each field below";
alertInput.addTextField("Enter trip description");
alertInput.addTextField("Enter the Initial Odometer Reading here");
alertInput.addTextField("Enter the Final Odometer Reading here");
alertInput.addAction("OK")
await alertInput.presentAlert();

// Populate JSON
let jsonCar = {};
jsonCar.date = new Date;
jsonCar.trip = alertInput.textFieldValue(0);
jsonCar.odometer = alertInput.textFieldValue(1);
jsonCar.final_odometer = alertInput.textFieldValue(2);
jsonCar.mileage = jsonCar.final_odometer - jsonCar.odometer;
console.log(jsonCar);

// This time we have called Scriptable using an x-call-back-url.
// Scriptabe doesn't have x-callback-url support built in per se,
// but we can handle the x-callback operation in code.
// To do this, we will access the x-success parameter passed to the script
// and append the JSON weve built as a result. To do this, we need to first
// convert the JSON to a string, then encode it for transfer via URL, and then
// append it as a suitable x-callback parameter for Shortcuts to utlise.
let queryParams = URLScheme.allParameters();
let urlSuccess = queryParams["x-success"] + "?result=" + encodeURI(JSON.stringify(jsonCar));
Safari.open(urlSuccess);

There is actually a fourth script too, but that’s embedded inside of the shortcut.

The Shortcuts Shortcut

Now that the Scriptable scripts are in place, download the following shortcut and ensure that you have set the permissions for running third party shortcuts. As always with downloaded shortcuts (and code), please look through it before running to reassure yourself that it is safe to run.

When the shortcut is run, it will execute four different ways of calling a Scriptable script to retrieve a set of car log related data in a dictionary format. In each case, to show it is retrieving data from a dictionary being created by Scriptable, the shortcut will subsequently show the name of the trip and the mileage based on the construction of a dictionary containing the following root level elements.

  • date - the current date in a standard format (e.g. 2020-02-01T13:14:15.161Z), created by code.
  • trip - a description of the trip, entered manually or set by code.
  • odometer - an initial odometer reading, entered manually or set by code.
  • final_odometer - a final odometer reading, entered manually or set by code.
  • mileage - the miles recorded by taking the difference in the odometer readings, calculated by code.

Note the variation in some of manual or code determination. You’ll see these vary across the examples. I’ve had to go this route because at this point there was no way to narrow down how the data would be gathered. e.g. from manual entry, reading from a log file that has been generated, or direct access to a car information API.

Please note:

  1. These are examples for demonstration purposes I’ve foregone adding in any content validation, error handling, etc.
  2. These calls are not passing any data into Scriptable from Shortcuts, though this is possible.

Call 1: Shortcuts Scriptable Action - Scriptable Run in App

In the first call, the shortcut uses a standard Scriptable action to run a script in the Scriptable app. The action is set to execute the script in the Scriptable app. This is necessary because CarLog1 uses an interactive UI element (an Alert) to capture information from the user. Shortcuts can’t access Scriptable in this way internally, and so we have to switch into the Scriptable app to do it.

Once in Scriptable, the JSON is built and then copied to the clipboard. It isn’t my favourite way of passing data around as it overwrites the clipboard, but you can workaround this by capturing and restoring the clipboard in Shortcuts around calling the Scriptable script.

The Scriptable script finally just opens the Shortcuts app. Because of the wait to return action after the Scriptable action, this should just pick up where we left off when we get back to Shortcuts.

Shortcuts then displays the example result from the dictionary it picked up from the clipboard.

Call 2: Shortcuts Scriptable Action - Scriptable Not Run in App

If we are calling a Scriptable script that requires no data entry by the user, we can opt not to switch to the Scriptable app to run the script; though it should be noted there are other reasons, such as memory constraints for example, why you might opt to run a Scriptable script in app rather than in Shortcuts.

This time we will use the Shortcuts output option in Scriptable as we are not switching to the app. This means no clipboard overwriting. We simply set the output as a stringified version of the JSON, and set the script to complete.

We can then extract the dictionary from the output of the Scriptable script triggering action, and show the results as before.

Call 3: X-Callback-URL Action - Scriptable Run in App

The third option is to use x-callback-url, which is the one that the original question was around and where I suggested perhaps asking a more open ended question including details about what you actually wanted to do.

Scriptable does not natively support x-callback-url. It has a URL scheme, but that is not the same thing. x-callback-url requires a URL scheme to be in place, but a URL scheme does not necessitate the availability of an x-callback-url solution. But this is Scriptable. We can work around the lack of a inbuilt x-callback-url provision without too much effort.

We start by passing the Scriptable script run URL (available for example from script properties in Scriptable) to a Shortcuts action to open an x-callback-url. The Scriptable script is opened in app, so we can utilise the manual data entry once again if required (but it isn’t necessary), and at the end we can use some code to build our own x-callback-url.

When Shortcuts calls Scriptable, it generated an x-success parameter. We can query that from the script and append the JSON as an additional parameter; after converting it to a string and URL encoding it so that it can safely be included in a URL. Then we trigger the URL to open and we’re back in Shortcuts. We’re just carrying out the actions an app with built-in x-callback-url would in our own code.

Note that because we are doing x-callback-url, in this approach we don’t need to split the shortcut to have a pre-script-run shortcut and a post-script-run shortcut. Also by eliminating hard coding the shortcut to call back to, we have the potential for greater reusability of the Scriptable script.

Once back in shortcuts, the result from the x-callback-url can be extracted, converted to a dictionary and used to display the next result.

Call 4: Shortcuts Inline Scriptable Action - Scriptable Not Run in App

The final example uses the newest Scriptable action for Shortcuts and allows a script to be run inline. In this approach there is no fourth file in Scriptable. Instead the script is held within a field in the action, and is passed through to Scriptable behind the scenes to be run.

This is very similar to the second example and the exchange of the resulting JSON in both the script and the shortcut work in just the same way.

Summary

So there you have a handful of options. Not all are applicable in all cases and some are easier to implement and to follow than others, but hopefully you can follow along with each example.

Of course the fifth option that I eluded to at the start is to do everything purely in Shortcuts. I’m still left wondering what the driver is for needing to switch between the two. Reasons absolutely can and do exist, otherwise the actions wouldn’t exist, but as it stands, for the kind of thing being discussed, I currently just don’t see what it would be.

I hope that the above helps, and please do elaborate on the bigger picture. We may be able to advise further.

2 Likes

Wow! I appreciate the amazingly complete review. Will likely take me some time to fully digest.

In response to your why question it has more to do with my learning curve and desire to further my skills than anything else. I’ve been using basic shortcuts since apple first released. My shortcuts have slowly gotten more sophisticated but have still felt limited in functionality and interface of shortcuts app gets hard to follow as the shortcut get longer and more complex. I work in healthcare not IT and work on shortcuts to solve problems as time permits. Have several shortcuts that calculate patient level data and present in a organized fashion to simplify my day job. These shortcuts have gotten quite long and even with comments get difficult for me to follow when there are delays between working on them.

Simultaneously I Newly started to take up coding with online classes. Started with python for data analysis for my day job. However I need to enjoy what I’m doing to progress. Soley honing my coding skills towards data analysis wasn’t “fun” so I transitioned to JavaScript bc it overlaps with automation on IOS which I have enjoyed. I am almost a 100% iPad kind of guy. Only turn to Mac for very specific use cases now a days.

I recently got through code academy intro to JavaScript so I know just enough to be clumsy at best. I discovered the scriptable inline script function and created a shortcut (link included minus api key) which would be triggered by car play to present me some options that allowed me to log to Airtable some mileage data for trips which can count for a tax deduction when tax time roles around. Initially I was quite happy with the script but my wife suggested I needed further granularity. I am certain I could likely make this in shortcuts but I was struck by the simplicity of having code on one full page of scriptable and ease of manipulation compared to semi long script in shortcuts interface that gets hard to follow. When I combined this with my budding new JavaScript skill set and desire to expand it I have turned towards creating in scriptable and triggering with shortcut. Since I haven’t fully worked through the JavaScript airtable API code experience yet and I have that functionality built in my shortcut I was simply hoping to pull the data back into my shortcut as a dictionary and send to airtable api.

This is likely more than you wanted to hear but you asked :slight_smile:

https://www.icloud.com/shortcuts/d8988cfa34e94277a0a00d5e301f397c


let a = {}
//Generates the date
let b = new Date();
let day = b.getDate();
let month = b.getMonth();
let year = b.getFullYear();
let date = `${month}-${day}-${year}`;
a.Date = date
//console.log(`Date is  ${date}`);


let odometerAlert = new Alert();
odometerAlert.title = "Current Milage";
odometerAlert.addTextField("Odometer", null);
odometerAlert.addAction('Submit');
odometerAlert.addCancelAction('Cancel');

await odometerAlert.presentAlert();
let odometer = odometerAlert.textFieldValue(0);
a.Odometer = odometer

//set up alert 
let travelTaxDeduction = new Alert();
travelTaxDeduction.title = "Pick from Options:";
travelTaxDeduction.addCancelAction('Cancel');
travelTaxDeduction.addAction("Durham Clinic <==> DRH");
travelTaxDeduction.addAction("Durham Clinic <==> Alamance");
travelTaxDeduction.addAction("Durham Clinic <==> Duke North");
travelTaxDeduction.addAction("Durham Clinic <==> Raleigh");
travelTaxDeduction.addAction("Home <==> Airport");
travelTaxDeduction.addAction("Durham Clinic <==> Airport");
travelTaxDeduction.addAction("Other");




let startStopLocations  = await travelTaxDeduction.presentAlert();

if  (startStopLocations === 0){
a.Trip = "Durham Clinic <==> DRH";
a.Mileage = 0.4;
} else if (startStopLocations === 1) {
a.Trip = "Durham Clinic <==> Alamance";
a.Mileage = 24;
} else if (startStopLocations === 2) {
a.Trip = "Durham Clinic <==> Duke North";
a.Mileage = 4.3;
} else if (startStopLocations === 3) {
a.Trip = "Durham Clinic <==> Raleigh";
a.Mileage = 29;
} else if (startStopLocations === 4) {
a.Trip = "Home <==> Airport";
a.Mileage = 15;
} else if (startStopLocations === 5) {
a.Trip = "Durham Clinic <==> Airport";
a.Mileage = 16;
} else if (startStopLocations === 6) {

let otherAlert = new Alert();
otherAlert.title = "Trip Details:";
otherAlert.addTextField("Start Location");
otherAlert.addTextField("End Location");
otherAlert.addTextField("Distance");
otherAlert.addCancelAction('Cancel');
otherAlert.addAction("Submit");

let otherLocationPromise  = await otherAlert.presentAlert();
let enteredStartLocation = otherAlert.textFieldValue(0);
let enteredEndLocation = otherAlert.textFieldValue(1);


a.Trip = `${enteredStartLocation} <==> ${enteredEndLocation}`;
a.Mileage = otherAlert.textFieldValue(2);
} else {console.log("Cancelled")};




let finalOdometer = Number(odometer) + Number(a.Mileage);
a.Final_Odometer = finalOdometer


console.log(a)
//QuickLook.present(a)
//console.log(`Initial odometer reading is ${odometer}`);
//console.log(`Final odometer reading is ${finalOdometer}`);


// Safari.open("shortcuts://run-shortcut?name=Return%20Processor&input=text&text=Return%20Text")
// let baseurl = "scriptable:///run?scriptName=Multipick"
// let cb = new CallbackURL(baseurl)
// cb.addParameter("Date", a.Date);
// cb.addParameter("Odometer", String(a.Odometer));
// cb.addParameter("Trip", String(a.Trip));
// cb.addParameter("Mileage", String(a.Mileage));
// cb.addParameter("Final_Odometer", String(a.Final_Odometer));
// console.log(cb)
// cb.open() 


Script.complete();

Finally finished my shortcut. Thanks for your help.