Can input be passed to Scriptable from Shortcuts app?

I am so excited about this app to extend the new Shortcuts app. I can add a script into a shortcut. I can, it appears, pass in an input into the script. I can log this in Scriptables, but it logs out:

[object ScriptableKit.Arguments]

However, if I try and access that object (eg Object.keys(args)) an empty array is returned, and args[0] returns undefined. Am I missing something or is this just not possible in v1?

Any suggestions appreciated :smiley:

Have you tried args.all? Take a look at the documentation in the app for the args object.

Thanks for the suggestion. Yes I had tried that it also returned [] - ie an empty array.

At present apple doesn’t support passing inputs to third party apps.

One can use the pasteboard in the scriptable to access the content as an input from the pasteboard in shortcuts

See the example here which passes some data from Shortcuts to a Scriptable script using the URL scheme.

I get why this is confusing but args is only used when running the script from a share sheet.

I’ve briefly described two approaches for passing input/output between shortcuts:

2 Likes

Pasteboard is a great idea - should have thought of that! I did think that Shortcuts does let an app take input - but maybe they are all using url schemes in the background (eg Drafts or Bear)

Hi, discovered Scriptable today, but I have zero Javascript knowledge and wanted to experiment a little bit with images.
I got a couple of things working, but I have literally no clue what I’ve to do to pass an image from shortcuts to scriptable via pasteboard.

What I tried: I made a basic test shortcut
Find my latest screenshot/photo -> copy to pasteboard -> use scriptable to display the image.

What I wrote in Scriptable:
let input = Pasteboard
let img = await input.pasteImage()
QuickLook.present(img)

The image is copied to the clipboard. If run the script in Scriptable it works, if I run it as a Siri shortcut/suggestion I get ‘null’ as an output instead of an image.

I really would like to know how wrong my first try is and ideally how to handle pasteboard images correctly.
Thanks in advance for your help.

1 Like

Hi, did you find a solution? I have the same issue, I can use the pasteboard from Shortcuts with text but with an image it shows “null” as well…

1 Like

One of the first things I tried when I got Scriptable was passing data, any data, from Shortcuts. Base64 worked well.

2 Likes

I went with the clipboard route. It seems a little slow, but was simpler than url callbacks. Hopefully Apple will let data flow through Shortcuts more fully in a future update

Did anyone find a solution going the clipboard route? I still get null.

Looking at the documentation, are we expected to know the APIs that are used by scriptable by heart? Otherwise perhaps the documentation could provide a little more info. E.g. It says:


But in fact, it cannot be used like this.
You just have to know in beforehand that you need to find it in Pateboard and also that it is asynchronous.

Docs says pasteString()
But you actually need to Write :

let input = Pasteboard
let copyText= await input.pasteString()

How are we supposed to know that!

Because it’s listed in the documentation as part of the Pasteboard object, so you have to have a Pasteboard object to call it on. Just like you have to have an Alert object to call the methods listed in that section of the documentation.

You could also just do this:

let copyText = Pasteboard.pasteString()

without having to create a new variable.

3 Likes

Having heavily used Scriptable’s Keychain and Device modules inside my shortcuts, I have developed some solutions for both approaches I thought I would share here.

The Pasteboard approach

Because the scripts I pass are often one liners, but vary a lot in content, I wanted a generic way to have Scriptable evaluate some JS from Shortcuts without actually opening the Scriptable app. The result is a two part setup. On Scriptable’s side, I have a script Scriptable Intent with his code:

var result

try {
  let code = Pasteboard.pasteString()
  if (code == null) throw new EvalError('No code to evaluate.')
  result = {kind: 'Result', value: Function(code)()}
} catch (e) {
  result = {kind: e.name, value: e.message}
}

Pasteboard.copyString(JSON.stringify(result))
Script.complete()

On Shortcuts’ side, I have a shortcut that will take text input, save the current pasteboard, put the input on the pasteboard, call the intent (you need it to run in Scriptable at least once for Siri, and hence Shortcuts, to pick it up), turn the JSON on the pasteboard into a dictionary, restore the pasteboard and return the dictionary. You can then pass your code to it via the Call Shortcut action (just make sure it includes a top level return statement), and the returned dictionary allows you to handle errors (when the dictionary’s kind key contains “Error”) or use the result value if no error occurred as your needs dictate, all without clobbering your pasteboard. Find the shortcut here for your convenience.

The x-callback approach

As convenient as calling Scriptable via the intent is, you sometimes need to switch to the app, notably when you want to display UI elements (like an Alert). The call itself is pretty straightforward, but I found handling the result callbacks a bit of a hassle, so I generalised the thing away into a class, CallbackHandler. Basically, it looks like this:

// Handle the x-callback results, with fallbacks when no callback URLs are provided.
class CallbackHandler {
  constructor (parameters) {
    const urlBuilderFor = base => {
      return base == null ? null : payload => {
        let url = base
        if (payload != null) {
          let parts = Object.keys(payload).reduce((acc, cur) => {
            if (payload[cur] != null) {
              let arg = encodeURIComponent(cur)
              let val = encodeURIComponent(JSON.stringify(payload[cur]))
              acc.push(`${arg}=${val}`)
            }
            return acc
          }, [])
          if (parts.length > 0) url += `?${parts.join('&')}`
        }
        return url
      }
    }
    this.successURL = urlBuilderFor(parameters['x-success'])
    this.cancelURL = urlBuilderFor(parameters['x-cancel'])
    this.errorURL = urlBuilderFor(parameters['x-error'])
  }

  success (message, payload) {
    if (this.successURL != null) {
      let url = this.successURL(payload)
      Safari.open(url)
    } else {
      if (message == null) throw(new Error('Missing message String!'))
      console.log(message)
    }
  }

  cancel (message, payload) {
    if (this.cancelURL != null) {
      let url = this.cancelURL(payload)
      Safari.open(url)
    } else {
      if (message == null) throw(new Error('Missing message String!'))
      console.log(message)
    }
  }
  
  error (err) {
    if (this.errorURL != null) {
      let url = this.errorURL({errorCode: err.name, errorMessage: err.message})
      Safari.open(url)
    } else {
      throw err
    }
  }
}

which allows me to do:

const params = URLScheme.allParameters()
const handle = new CallbackHandler(params)

// depending on outcome:
handle.success('All is well', {foo: 'bar'})
handle.cancel('Aborted by user', {foo: 'bar'})
handle.error(err)

and automagically get the correct callback URL called, with all parameters set and properly escaped, or a log message written when some callback URLs are unspecified.

Now, as the class definition is rather a handful of code to include into your scripts, I suggest you use my module importing hack and wrap the class code into a module, i.e. its own file with the following content:

const exports = {
  CallbackHandler: // replace comment by the full class code shown above
}

then require() it as per the linked post.

7 Likes

@kopischke Thanks for sharing :pray:

This is a javascript/object oriented programming thing. I’ve never used javascript before in my life, but coming from other programming languages it was immediately clear to me what I needed to do.

I think you just need to keep this into perspective: scriptable is an interpreter for javascript so it just needs to assume that the people using it are already familiar with javascript or are learning it and want to use it in this environment. There’s only so much handholding it can do before it becomes Yet Another Javascript Tutorial of which there are literally tens of thousands already.

Edit: To add a little bit more info, the key words in that screenshot you posted are at the top where it says “static method.” This alone tells me that the pasteString function is a “function belonging to the Pasteboard class,” and it being “static” tells me how to call it. So, my point is, the documentation is very helpful and meaningful. Perhaps not to people totally unfamiliar with oop, but then again Scriptable doesn’t claim to be a resource for learning javascript.

1 Like

Thank you @fork for sharing the fact that you don’t know JS, but I’m not sure how that is helpful in this particular case.
I actually know Javascript though and these objects, or classes (which btw are just syntactical sugar in js) are not part of the language. After some investigation I guess they are JS bridges to iOS classes (UIpasteBoard perhaps).
But thank you for the contribution.

To @simonbs Now that I know how to interpret the documents, perhaps

  1. Spell the class names correctly ie Pasteboard, not pasteboard, or even better,
  2. Make that classname linked (clickable) to the info page about that class.

Cheers and thanks for a great app.

Yes, but only if you happen to go to that page first. As a new user of Scriptable, searching for paste in the documentation, you would probably click paste() because that looks like the thing you want. Looking at that page, being a javascript web developer, the syntax static paste(): string looks unfamiliar and doesn’t necessary hint that it is a method belonging to a class, especially since JS isn’t a strongly typed language.

I’m merely sharing my thoughts to the developer to enhance the UX :blush:. I don’t think it will make the app harder to use if you instead of saying…

paste
Pastes a string from the pasteboard or null if no string is in the pateboard

…would say:

paste –Function property of the Pasteboard object
Returns a string from the clipboard or null if no string is in the clipboard.
Usage:
let clipboardContent = Pasteboard.paste()

ps. @simonbs, small typo in the documentation …or null if no string is in the pateboard Should be pasteboard (unless you don’t want to use the more general term clipboard, or Pasteboard if you mean the object/class)