Basic app intent always showing error in Shortcuts app "The action could not run because an internal error occurred."

I have a very basic App Intent extension in my macOS app that does nothing than accepting two parameters, but running it in Shortcuts always produces the error "The action “Compare” could not run because an internal error occurred.".

What am I doing wrong?

struct CompareIntent: AppIntent {
    static let title = LocalizedStringResource("intent.compare.title")
    static let description = IntentDescription("intent.compare.description")
    static let openAppWhenRun = true
    
    @Parameter(title: "intent.compare.parameter.original")
    var original: String
    @Parameter(title: "intent.compare.parameter.modified")
    var modified: String
    
    func perform() async throws -> some IntentResult {
        return .result()
    }
}
Answered by DTS Engineer in 822268022

I looked into this further, and the openAppWhenRun can't be set to true in an intents extension. The purpose of the extension point is for intents that you expect to complete without a full app launch for their normal functionality (that is, I'm leaving aside error conditions that may require an app launch for the customer to resolve that error). Intents that you design to always require an app launch should live inside the main app, since that is what will wind up executing to complete that app launch.

— Ed Ford,  DTS Engineer

Do you get the same results with just the relevant code in a small test project? If so, please share a link to your test project. While the code above is helpful, I want to see it in context as you are, including your Xcode project target setup. If you're not familiar with preparing a test project, take a look at Creating a test project.

—Ed Ford,  DTS Engineer

Here's a link to a sample project: https://www.swisstransfer.com/d/e8ad8888-ae2e-4371-8e82-0d5915c967c7

I really just created an empty macOS App project, added a target App Intents Extension named CompareIntent, and pasted the code I provided above in the CompareIntent.swift file. Then I build the project and copied the app into my Applications folder, and I can still reproduce the issue when running the app intent in Shortcuts.

Accepted Answer

I looked into this further, and the openAppWhenRun can't be set to true in an intents extension. The purpose of the extension point is for intents that you expect to complete without a full app launch for their normal functionality (that is, I'm leaving aside error conditions that may require an app launch for the customer to resolve that error). Intents that you design to always require an app launch should live inside the main app, since that is what will wind up executing to complete that app launch.

— Ed Ford,  DTS Engineer

Thank you for your help. By moving the code into my main target and removing the App Intents Extension I was able to make it work.

It feels strange though. Since when is it sufficient to add a declaration in code which is then automatically available outside of the app? Is the app intent's perform() code still running within the app? When I was using the App Intents Extension I used to encode the intent's arguments and send them via a distributed notification which was then observed by the main app. But now apparently I can directly pass the arguments to the relevant AppKit view:

struct CompareStringsIntent: AppIntent {
    static let title = LocalizedStringResource("intent.compare.strings.title")
    static let description = IntentDescription("intent.compare.strings.description")
    static let openAppWhenRun = true
    
    @Parameter(title: "activity.compare.files.original")
    var original: String
    @Parameter(title: "activity.compare.files.modified")
    var modified: String
    
    #if os(iOS)
    @Dependency private var openStrings: OpenStrings
    #endif

    func perform() async throws -> some IntentResult {
        Task { @MainActor in
            #if os(macOS)
            let delegate = (NSApp.delegate as! AppDelegate)
            ...
            #elseif os(iOS)
            openStrings.original = original
            openStrings.modified = modified
            openStrings.trigger = true
            #endif
        }
        return .result()
    }
}

And for iOS, which uses SwiftUI, I wasn't able to find a more elegant solution than using a trigger variable to signal that the intent should do something in the main app:

struct CompareApp: App {
    
    private var openStrings = OpenStrings.shared
    
    init() {
        let openStrings = openStrings
        AppDependencyManager.shared.add(dependency: openStrings)
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(openStrings)
        }
    }
}

struct OpenStringsView: View {
        
    @Environment(OpenStrings.self) private var strings

    var body: some View {
        NavigationStack {
            Form {
                ...
            }
            .onChange(of: strings.trigger, { _, trigger in
                if trigger {
                    strings.trigger = false
                    compareStrings()
                }
            })
        }
    }
    private func compareStrings() {
        task = Task {
            ...
        }
    }
}

@Observable final class OpenStrings: @unchecked Sendable {
    
    static let shared = OpenStrings()
    
    var original: String
    var modified: String
    var trigger = false
    
    init(original: String = "", modified: String = "") {
        self.original = original
        self.modified = modified
    }
    
}

Since when is it sufficient to add a declaration in code which is then automatically available outside of the app? Is the app intent's perform() code still running within the app?

I think you're asking these questions from my prior statement:

The purpose of the extension point is for intents that you expect to complete without a full app launch

When I say full app launch there, what I actually mean is a foreground app launch that is visible to the user. An intent extension allows your code to run in the background, where your app may not have any UI to show, or only show a small piece of UI to show a result or provide status. These extensions run as dedicated process for your extension point, and are much lighter weight than the main app's process. (This is broadly true for the entire set of extension points available on the system, this is not specific to App Intents extensions.) That means your code is still running "in your app", as the intent extension process here is still dedicated to your app, but this process is also not the same one as your main app process, which may not even be running.

Did I understand your question here, and then does that answer clear it up for you?

— Ed Ford,  DTS Engineer

Thanks for the explanations. I thought App Intents needed to always be a separate target as they would be executed in a separate process, but for an App Intent that needs the app to run it makes sense that it's declared inside the app. Although I'm still wondering how the Shortcuts app displays the App Intent UI. I guess it's somehow extracted at build time so that it can be loaded and displayed even when the app is not running? That's somewhat counterintuitive to me: the relevant code is in the main target, but available outside the app (in the Shortcuts app).

Although I'm still wondering how the Shortcuts app displays the App Intent UI. I guess it's somehow extracted at build time so that it can be loaded and displayed even when the app is not running? That's somewhat counterintuitive to me: the relevant code is in the main target, but available outside the app (in the Shortcuts app)

There's a lot of inter-process communication that happens with system features that are hidden away from what you need to worry about at the API level. This example is still one where a process "from your app" is running, and there are layers of subsystems that manage the communication between what you see in Shortcuts (and its processes), and your app (and its processes). When an intent from your app is run in Shortcuts, the system launches a process for your app (or its extension, depending on your project configuration) if its not already running, and then calls the perform() method of the relevant intent. Your code is reporting back a result to the system from the perform() method, which can include a UI snippet (though adoption of ShowsSnippetView). When the system receives this result back within your app's process, it sends those results out of your process and back to the Shortcuts app through those inter-process communication systems.

While you're building up a conceptual understanding of how these features are possible, I want to emphasize that the way the system handles this back and forth between your app and the Shortcuts app isn't something that you need to think about when writing code that uses the App Intent APIs.

— Ed Ford,  DTS Engineer

Written by DTS Engineer in 824169022
There's a lot of inter-process communication that happens with system features that are hidden away from what you need to worry about at the API level.

Of course that's very pleasant for us in the end and how it should be. It's just unexpectedly easy for someone used to various XPC mechanisms and distributed notifications to communicate between processes.

Basic app intent always showing error in Shortcuts app "The action could not run because an internal error occurred."
 
 
Q