os_intents 0.3.0 copy "os_intents: ^0.3.0" to clipboard
os_intents: ^0.3.0 copied to clipboard

Declare OS-level app actions in Dart. Generates iOS App Intents and Android AppFunctions — no Swift or Kotlin by hand.

0.3.0 #

IntentResult.failure, and an exception no longer reaching the user #

A handler can now say why it could not run:

if (!await auth.isSignedIn) {
  return IntentResult.failure('Sign in to add tasks.');
}

Both platforms report it the way they report any failed action — a thrown error carrying the message — so the sentence written here is the one the user hears.

Behaviour change, and the reason this was added. An unhandled exception used to cross as its own toString(), and the system reads that out: Bad state: no auth token for alice@example.com was a thing Siri could say. A handler that throws is now reported as The action could not be completed, with the real message logged instead. OsIntents.unexpectedErrorMessage replaces that fallback — set it from main() to localise it — and OsIntents.debugIncludeErrorDetail, true in a debug build, still passes the exception through, since during development the only reader is whoever wrote the handler.

A test surface beyond the harness #

package:os_intents/testing.dart and FakeOsIntentsPlatform. Everything the package offers past running a handler — donate, publishStatic, pushShortcut and the rest — goes through the platform, whose default implementation answers false and an empty list so a plugin-less build keeps working. A test of code that donates therefore passed whether or not it donated. The fake records what it was given, in wire form, and can drive an invocation the way the OS does, through the installed dispatcher.

IntentHarness gained entityTypes, entitiesByIds, entitiesMatching and suggestedEntities: an @EntityQuery runs before any handler does, and nothing in a flutter test could reach one. OsIntents.debugReset() forgets the installed registry so a second test does not dispatch through the first one's handlers.

providesDialog on @AppIntent #

IntentResult.done() was documented as "succeeded, nothing to say or show", but the generated perform() always declared ProvidesDialog and spoke an empty string instead — there was no way for the generator to know a handler never speaks. @AppIntent(providesDialog: false) says so: the generated Swift drops ProvidesDialog from perform()'s return type and answers with a bare .result().

Opt-in and additive: every existing intent keeps declaring ProvidesDialog (the default), so nothing that compiled before changes.

0.2.1 #

Documentation only. The install snippet on the package page still named 0.1.0 a release after 0.2.0 shipped; it now tracks the current version. The status block links the public roadmap to 1.0 and invites pilot apps into the repository discussions.

0.2.0 #

Dynamic Android shortcuts #

OsIntents.pushShortcut, shortcuts, removeShortcuts and maxShortcuts, with a DynamicShortcut to describe one. The Android half of what donate does on iOS, and under its own name on purpose: a donation is a hint to a ranking model that iOS may act on, while a dynamic shortcut is an entry the app puts on the launcher and then owns. Both answer "the user just did this, offer it back", and each returns false on the platform without a counterpart, so both are safe to call unconditionally.

A dynamic shortcut runs the named intent through exactly the path a generated app shortcut uses, so the handler sees args as if the system had filled them in — converted the same way a donation's are.

Pushing the same id replaces the entry rather than adding a second, which is what makes "the last five things you did" cheap to keep. At the cap, Android 11+ drops the lowest-ranked entry itself — the platform's own policy, via pushDynamicShortcut. Below that there is no such call, and pushShortcut returns false rather than picking one of the app's shortcuts to throw away.

Verified on an emulator, not only in the wire format: dynamic_shortcuts in probe/android_appfunctions pushes, lists, replaces and removes through the real ShortcutManager.

More kinds of parameter #

Uri, Duration, Measurement and IntentFile join String, int, double, bool and DateTime. Additive throughout — nothing that compiled against 0.1.0 changes.

  • Uri becomes a URL on iOS and its text on Android.

  • Duration becomes the system's own "how long" control. It crosses as microseconds, which is Dart's own integer form of a Duration — the same rule by which a DateTime crosses as its millisecondsSinceEpoch.

  • Measurement is a quantity with a unit picker. Which picker is part of the generated Swift type, so it is declared rather than inferred: @Param(title: 'Distance', dimension: Dimension.length). Whatever unit the user chooses, value arrives in the SI base unit.

    Seven dimensions — length, mass, duration, speed, temperature, volume, energy. App Intents has 22 and the other fifteen are iOS 17, which was measured against the SDK rather than read off documentation.

  • IntentFile is iOS only. The bytes are already staged on disk when the handler runs, so path can be read straight away. androidx.appfunctions has no file type at all — Android's model is a content URI plus a permission grant — so an intent taking one is left out of what Android offers to agents, and sync --android says which and why rather than coercing it to a String.

returns: accepts Uri, Duration and IntentFile too. Not Measurement: a bare Type has nowhere to put a dimension, and the generator refuses it rather than emitting Swift that will not build.

0.1.0 #

First released version. Pre-alpha: both pipelines work end to end and are verified on a device, but nothing here has carried a real user's app yet. What ran on a device, what only compiles, and what has never been observed at all is listed in docs/verified.md — read that before trusting anything below.

Declaring actions #

  • @AppIntent on a top-level function: title, description, spoken phrases, SF Symbol, and a stable identifier — which is what users' saved shortcuts point at, so it is worth setting before you ship.
  • Three execution modes. foreground opens the app, background runs with no UI, static_ answers from published state with no Dart running at all.
  • @Param for parameters: String, int, double, bool, DateTime, an @AppEntity or an @AppEnum. requestValueDialog is what the system asks when a value is missing.
  • @AppEntity + @EntityQuery, so the system can resolve, search and suggest values that live in your data. iOS only — androidx.appfunctions has no entity concept at all.
  • @AppEnum for a closed set, which works on both platforms: a real AppEnum on iOS, a value constraint on Android.
  • returns: hands a value to the next step of a Shortcut.
  • showsSnippet: true answers with a card, and SnippetSpec.actions puts buttons on it that run your other intents. Buttons need iOS 17; below that the card renders without them.
  • confirmBeforeRunning: makes the system ask first; the handler is not called at all if the user declines.

Reaching the user #

  • OsIntents.publishStatic keeps the static_ answers fresh.
  • OsIntents.donate tells the system an action just happened, so it can suggest it later. iOS only, and returns false on Android rather than pretending.
  • Android app shortcuts and Assistant capabilities are generated by default. AppFunctions — the headless layer — is opt-in behind sync --android, because it forces compileSdk 37, AGP 9.1.1 and Gradle 9.3.1 on your app.

Testing #

  • IntentHarness runs a handler with no device, simulator or Siri involved.

Known gaps #

Apple's iOS 26 SnippetIntent, where a card reloads itself after a button runs; AssistantIntent schemas; and entities or snippet cards on Android. Siri invoking a phrase by voice has never been observed — the OS calling perform() has, from the Shortcuts app.

verified.md has the evidence for every claim above; troubleshooting.md covers the ways this fails silently.

1
likes
160
points
198
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Declare OS-level app actions in Dart. Generates iOS App Intents and Android AppFunctions — no Swift or Kotlin by hand.

Repository (GitHub)
View/report issues
Contributing

Topics

#app-intents #siri #shortcuts #assistant #codegen

License

MIT (license)

Dependencies

flutter, meta, os_intents_android, os_intents_ios, os_intents_platform_interface

More

Packages that depend on os_intents