Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

Shortcuts: Invalid action metadata

I have a habit tracker app that uses App Intents and Shortcuts. The app uses SwiftData to persist data, just in case that's important.

One of the shortcuts serves to log habits. However, when the app has been in the background for a good while (over an hour or so), that particular shortcut always fails when I try to run it in the Shortcuts app with the system error "Invalid action metadata" caused by "a bug in the host app".

The app has a total of 9 shortcuts, and it's just this one particular shortcut that seems to be failing – the others continue to run without any issues even after the app has been in the background for a long time.

The app intent/shortcut that is problematic is the one called HabitEntryAppIntent. For example purposes, I've also included one of the non-problematic intents in the code snippet below called HabitEntryCounterForTodayAppIntent. Both of these intents have one @Parameter value of type HabitEntity.

I'll post code snippets in the replies because the character limit is not large enough to include them here, or view them in this GitHub gist:

Code snippets on GitHub

I've tried everything I can think of whilst debugging, but none of the following fixed the error:

  • Removed all usage of @MainActor and mainContext by replacing the ModelContext used in perform() with a locally created property.
  • Removed all usage of static shared properties like Calendar.shared and ModelContainer.shared and replaced them with local properties.
  • Removed all non-essential code such as the code for context.undoManager and WidgetManager.shared.reload(.all) and really striped it all down to the absolute essentials.
  • Reduced the number of shortcut phrases in the problematic shortcut because there was perhaps too many (>10) originally.

You might have noticed that the perform() method in the problematic intent manipulates the database whilst the non-problematic intent only reads the database. Given that the two intents in the snippet above both have the same @Property(...) var habitEntity: HabitEntity values, I tried to swap the contents of the perform() methods over to see what would happen.

And here's what's strange: When the perform() method from the problematic HabitEntryAppIntent is used in HabitEntryCounterForTodayAppIntent, it works without any issues and successfully logs habits! And then when the perform() method from the non-problematic HabitEntryCounterForTodayAppIntent is used in HabitEntryAppIntent it fails with the system error "Invalid action metadata". This suggests that the problem is not in the code that logs the habit entries but rather something is wrong with HabitEntryAppIntent itself.

I also tried changing the metadata used in HabitEntryAppIntent and its shortcut. I copied all the metadata used in HabitEntryCounterForTodayAppIntent (the title, description, parameterSummary etc) and pasted it into HabitEntryAppIntent. And did the same with the metadata in the shortcut (phrases, shortTitle etc) so that all the metadata used in HabitEntryAppIntent matched that used in HabitEntryCounterForTodayAppIntent. However, the shortcut for HabitEntryAppIntent continued to fail.

Thus, it doesn't seem to be an issue with the code in perform() because that code succeeds when used in another app intent. And, despite the "metadata" error message, it doesn't seem to be an issue with the metadata in the app intent because I've tried using metadata from the non-problematic intent but it still fails.

I have watched all WWDC videos related to app intents and shortcuts, and looked through the developer forum for similar questions, but I'm completely stumped by this issue and why it's only affecting one of my shortcuts.

Also worth mentioning is that the widgets in the app that log habits using the same app intent do not suffer the same issue; they continue to work even after the Shortcut has failed.

Moreover, if I try running the problematic shortcut for HabitEntryAppIntent and see the system error message, then run the shortcut for HabitEntryCounterForTodayAppIntent (which always succeeds), and then try running the HabitEntryAppIntent shortcut again, it then runs successfully on the second attempt. It seems that running HabitEntryCounterForTodayAppIntent fixes the issue, at least temporarily.

Code snippets for the app intents discussed above:

// This is the problematic intent that consistently fails.
struct HabitEntryAppIntent: AppIntent {
    static let openAppWhenRun: Bool = false
    static let title = LocalizedStringResource("Add a Habit Entry", table: "AppIntents")
    
    static let description = IntentDescription(
        LocalizedStringResource("Log an entry after completing a task.", table: "AppIntents"),
        categoryName: LocalizedStringResource("Log Habits", table: "AppIntents"),
        searchKeywords: [LocalizedStringResource("log", table: "AppIntents")],
        resultValueName: LocalizedStringResource("Habit", table: "AppIntents")
    )
    
    static var parameterSummary: some ParameterSummary {
        Summary("Add an entry to \(\.$habitEntity)", table: "AppIntents")
    }
    
    @Parameter(
        title: LocalizedStringResource("Habit", table: "AppIntents"),
        requestValueDialog: .init(LocalizedStringResource("Which habit would you like to add an entry to?", table: "AppIntents"))
    ) var habitEntity: HabitEntity
    
    @MainActor func perform() async throws -> some ProvidesDialog & ReturnsValue<HabitEntity> {
        let context = ModelContainer.shared.mainContext
        let logger = AppIntentHabitEntryLogger()
        try logger.addEntry(for: habitEntity.id, in: context)
        
        let localizedActionName = String(localized: "Logged “\(habitEntity.name)” Entry")
        context.undoManager?.setActionName(localizedActionName)
        
        WidgetManager.shared.reload(.all)
        
        return .result(
            value: habitEntity,
            dialog: .init(LocalizedStringResource("OK, added an entry to “\(habitEntity.name)”.", table: "AppIntents"))
        )
    }
}

extension HabitEntryAppIntent {
    init(habit: HabitEntity) {
        self.habitEntity = habit
    }
}

// This is the non-problematic intent that consistently suceeds.
struct HabitEntryCounterForTodayAppIntent: AppIntent, AppIntentDialogDateFormatting {
    static let openAppWhenRun: Bool = false
    static let title = LocalizedStringResource("Number of Entries Logged in a Habit Today", table: "AppIntents")
    
    static let description = IntentDescription(
        LocalizedStringResource("The total number of entries logged so far today for a habit.", table: "AppIntents"),
        categoryName: LocalizedStringResource("Habit Stats", table: "AppIntents"),
        searchKeywords: [LocalizedStringResource("count", table: "AppIntents")],
        resultValueName: LocalizedStringResource("Habit Statistics", table: "AppIntents")
    )
    
    static var parameterSummary: some ParameterSummary {
        Summary("Get the total number of \(\.$habitEntity) entries logged so far today", table: "AppIntents")
    }
    
    @Parameter(
        title: LocalizedStringResource("Habit", table: "AppIntents"),
        requestValueDialog: .init(LocalizedStringResource("Which habit would you like to count entries in?", table: "AppIntents"))
    ) var habitEntity: HabitEntity
    
    @MainActor func perform() async throws -> some ProvidesDialog & ReturnsValue<Int> {
        let habitID = habitEntity.id
        let context = ModelContainer.shared.mainContext
        
        let date = Date.now
        let startOfToday = Calendar.shared.startOfDay(for: date)
        let startOfTomorrow = Calendar.shared.startOfNextDay(for: date)
        
        var descriptor = FetchDescriptor<HabitEntry>(predicate: #Predicate {
            ($0.habitID == habitID) && ($0.date >= startOfToday) && ($0.date < startOfTomorrow) && ($0.count > 0)
        })
        
        descriptor.propertiesToFetch = [\HabitEntry.count]
        
        let matches = try context.fetch(descriptor)
        let totalCount: Int = matches.reduce(.zero) { $0 + $1.count }
        
        return .result(
            value: totalCount,
            dialog: .init(LocalizedStringResource("There are \(totalCount) entries recorded so far today for “\(habitEntity.name)”.", table: "AppIntents"))
        )
    }
}

extension HabitEntryCounterForTodayAppIntent {
    init(habit: HabitEntity) {
        self.habitEntity = habit
    }
}

// This is the code that logs the habit in the problematic intent.
@MainActor struct AppIntentHabitEntryLogger {
    func addEntry(for habitID: UUID, in context: ModelContext) throws {
        let currentDate = Date.now
        let startOfToday = Calendar.shared.startOfDay(for: currentDate)
        let startOfTomorrow = Calendar.shared.startOfNextDay(for: currentDate)
        
        var descriptor = FetchDescriptor<HabitEntry>(predicate: #Predicate {
            ($0.habitID == habitID) && ($0.date >= startOfToday) && ($0.date < startOfTomorrow)
        }, sortBy: [
            SortDescriptor<HabitEntry>(\.date, order: .forward) // Oldest to newest.
        ])
        
        descriptor.propertiesToFetch = [\HabitEntry.count]
        
        let matches = try context.fetch(descriptor)
        
        // The last match is the most recent one for the current day.
        if let match = matches.last {
            match.increaseCount()
        } else {
            let habit = try Habit.match(for: habitID, in: context)
            let newEntry = HabitEntry(habit: habit)
            context.insert(newEntry)
        }
        
        if context.hasChanges {
            try context.save()
        }
    }
}

Code snippet for the Shortcuts discussed above:

struct HabitShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
AppShortcut(
            intent: HabitEntryAppIntent(),
            phrases: [
                "Add an entry to \(\.$habitEntity) with \(.applicationName)",
                "Add an entry to \(\.$habitEntity) in \(.applicationName)",
                "Add entry \(\.$habitEntity) with \(.applicationName)",
                "Add entry \(\.$habitEntity) in \(.applicationName)",
                "Add an entry with \(.applicationName)",
                "Add an entry in \(.applicationName)",
                "Add an entry to \(.applicationName)",
                "Log an entry with \(.applicationName)"
            ],
            shortTitle: LocalizedStringResource("Add an Entry", table: "AppIntents"),
            systemImageName: "checkmark.circle.fill",
            parameterPresentation: ParameterPresentation(
                for: \.$habitEntity,
                summary: Summary("Add \(\.$habitEntity) entries", table: "AppIntents"),
                optionsCollections: {
                    OptionsCollection(HabitEntityQuery(), title: LocalizedStringResource("Log Completed Habits", table: "AppIntents"), systemImageName: "checkmark")
                }
            )
        )

AppShortcut(
            intent: HabitEntryCounterForTodayAppIntent(),
            phrases: [
                "Count \(\.$habitEntity) entries logged today with \(.applicationName)",
                "Count \(\.$habitEntity) entries logged today in \(.applicationName)",
                "How many \(\.$habitEntity) entries have been logged today in \(.applicationName)?",
                "Count habit entries logged today with \(.applicationName)",
                "Count habit entries logged today in \(.applicationName)",
                "Count entries logged today for a habit with \(.applicationName)",
                "Count entries logged today for a habit in \(.applicationName)",
                "Count \(.applicationName) entries logged today"
            ],
            shortTitle: LocalizedStringResource("Count Entries Logged Today", table: "AppIntents"),
            systemImageName: "number.circle.fill",
            parameterPresentation: ParameterPresentation(
                for: \.$habitEntity,
                summary: Summary("The total number of \(\.$habitEntity) entries logged so far today", table: "AppIntents"),
                optionsCollections: {
                    OptionsCollection(HabitEntityQuery(), title: LocalizedStringResource("Count Entries Logged Today in a Habit", table: "AppIntents"), systemImageName: "number")
                }
            )
        )
    }
}
Shortcuts: Invalid action metadata
 
 
Q