Given the rise of agentic interaction on Android, we need a fast, reliable API to make app capabilities discoverable and executable by agents. Through the years, Google has introduced several native frameworks to bridge the gap between the operating system, its system-level assistants, and apps.
App Actions is the long-standing predecessor to AppFunctions. It uses shortcuts.xml and built-in intents to map specific user requests directly to app features. Android Slices were an attempt to surface interactive snippets of an app’s UI directly within the assistant or search interface; they have been effectively deprecated since 2021. Then there's the Direct Actions API, a framework introduced to allow voice assistants to query a foreground app for its specific capabilities in real-time. Gone too. Finally, the Assist API: the fundamental system-level hook that allows a native agent to read the screen context, providing the situational awareness necessary for agents to act on behalf of the user.
In retrospect, the failure of these predecessors likely wasn't due to a lack of vision, but rather a fundamental mismatch between static engineering and the needs of dynamic intelligence. App Actions relied on a rigid library of built-in intents. If an app feature didn't fit into one of Google’s binding categories, it effectively didn't exist to the assistant. Android Slices were killed by the UI maintenance trap. By forcing developers to build and maintain restricted, templated versions of their interface that often felt out of place, Google asked for too much effort for too little user engagement. The Direct Actions API failed because of its requirement that an app is actively running on the screen, which prevents the assistant from performing tasks autonomously. And while the Assist API provided the eyes for the system, it lacked the intelligence. It could scrape a messy tree of text and nodes from the screen, but it couldn't reliably parse that data into meaningful actions without massive compute power and significant privacy trade-offs. Ultimately, these frameworks offered narrow shortcuts when the ecosystem instead required a universal language.
AppFunctions
Unlike its predecessors, which tried to force apps into predefined boxes or complex UI mirrors, the AppFunctions model treats the app as a collection of capabilities to be indexed, rather than a destination to be visited. By shifting the focus from how the app looks to what the app can do, Google is moving toward a model where the agent doesn't just deep-link you into a screen, but picks up the tools to finish the job for you.
AppFunctions have been in the works since late 2024. Although the official android.app.appfunctions package didn't land in the core framework until API level 36, the missing link for developers was the appfunctions Jetpack library, which began its alpha rollout in May 2025. This library allowed early adopters to start wiring their apps for tool use before the corresponding platform APIs were finalized. At that stage, it was a framework waiting for a brain; Jetpack supplied the plumbing, but assistants such as Gemini were not consistently able to invoke those tools on every device or build. Android 16 adds the platform hooks for discovery and execution on supported hardware. As of today, Google still frames the overall agent push as early / beta and describes two parallel tracks:
- AppFunctions as structured, self-describing entry points (what this article is about: discrete capabilities agents can call)
- UI automation for longer flows when there is no tailored integration: previewed on devices such as the Galaxy S26 series and select Pixel 10 models, in limited verticals and regions, with multi-step delegation already part of that story
In Google’s February 2026 post on the intelligent OS, the Looking ahead section states that Android 17 is meant to broaden these same capabilities; that includes structured AppFunctions and the agentic UI automation previews already tied to hardware such as the Galaxy S26 series and select Pixel 10 models; the stated aim is to reach more users, more developers, and more device manufacturers.
Let's turn to the stack you can use today: Gradle, Kotlin, and the device-facing adb checks that validate a real APK against the current Jetpack and platform drops.
Implementing an AppFunction
To start implementing AppFunctions, your development environment might require a few specific upgrades. First, ensure you are running a recent version of Android Studio to access the latest Gemini-integrated testing tools. While the Jetpack library itself can target a lower minSdk where compatibility allows, you’ll want compileSdk 36 and typically targetSdk 36 so the Android 16 framework can index and run your AppFunctions on device. Next, declare the Jetpack coordinates in your version catalog, then wire plugins, SDK level, KSP, dependencies, and merge ordering in the app module.
Version catalog (gradle/libs.versions.toml)
[versions]
appFunctions = "1.0.0-alpha08"
[libraries]
androidx-appfunctions = {
group = "androidx.appfunctions",
name = "appfunctions",
version.ref = "appFunctions"
}
androidx-appfunctions-service = {
group = "androidx.appfunctions",
name = "appfunctions-service",
version.ref = "appFunctions"
}
androidx-appfunctions-compiler = {
group = "androidx.appfunctions",
name = "appfunctions-compiler",
version.ref = "appFunctions"
}
App module (app/build.gradle.kts)
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
// Apply KSP to process the @AppFunction annotations
alias(libs.plugins.google.devtools.ksp)
}
android {
// compileSdk 36 aligns with Android 16, where platform AppFunctions APIs land
compileSdk = 36
// ... rest of your config
}
ksp {
arg("appfunctions:aggregateAppFunctions", "true")
}
dependencies {
implementation(libs.androidx.appfunctions)
implementation(libs.androidx.appfunctions.service)
ksp(libs.androidx.appfunctions.compiler)
}
// Run each merge*Assets after its matching ksp*Kotlin so AppFunctions metadata is generated first
tasks.configureEach {
if (!name.startsWith("merge") || !name.endsWith("Assets")) return@configureEach
if (name.contains("ArtProfile")) return@configureEach
val variant = name.removePrefix("merge").removeSuffix("Assets")
val kspTask = "ksp${variant}Kotlin"
if (tasks.names.contains(kspTask)) {
dependsOn(kspTask)
}
}
While the Jetpack library automates the plumbing (from generating schemas to registering them) the system fundamentally relies on AppSearch for on-device indexing. The beauty of the library is that it handles the AppSearch integration entirely behind the scenes; you don't need to manage sessions or write storage boilerplate yourself for your AppFunctions to become discoverable. With the environment ready, the next step is to spell out what distinguishes an AppFunction in source.
At its core, an AppFunction is a standard Kotlin function; but it carries a few specific decorations that turn it from a private app method into a public system tool:
- The
@AppFunctionannotation signals to the compiler that a method should be exported as such a system-level tool. Use@AppFunction(isDescribedByKDoc = true)when you write a real KDoc block on that method; the compiler folds that documentation into the metadata agents and indexers consume, so parameter semantics (for example thatapp1andapp2are package names) are not left implicit. -
AppFunctionContextprovides the function with essential situational awareness, such as information about the calling party or access to the app's own resources. -
AppFunctionSerializableensures your custom data classes are properly handled while they travel across process boundaries.
Let's see this in action in a real utility. In my app Be nice, a core feature is the ability to create app pairs (launching two apps in split-screen simultaneously). By exposing this as an AppFunction, we turn a sequence of UI interactions (opening a dialog, choosing apps, customizing parameters) into a single voice command. On eligible devices and assistant builds (as mentioned, Google’s rollout is still limited) you can ask Gemini to create an app pair for contacts and clock. The agent will call our AppFunction, passing the two apps’ package names as strings.
package de.thomaskuenneth.benice.appfunctions
import androidx.appfunctions.AppFunctionContext
import androidx.appfunctions.service.AppFunction
import de.thomaskuenneth.benice.R
class BeNiceFunctions {
/**
* Launches two installed apps together in split screen.
*
* @param context Execution context supplied by the AppFunctions runtime.
* @param app1 Name of the first app in the pair.
* @param app2 Name of the second app in the pair.
* @return A localized message describing success or failure.
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun createAppPair(
context: AppFunctionContext,
app1: String,
app2: String
): String {
val success = performPairing(app1, app2)
return if (success) {
context.context.getString(
R.string.pair_created_success,
app1, app2
)
} else {
context.context.getString(
R.string.pair_created_failure
)
}
}
private fun performPairing(a: String, b: String): Boolean {
// Be Nice logic omitted for brevity
return true
}
}
I used suspend fun, like Google’s own AppFunctions examples do, so we can easily call other suspending APIs from the body whenever the implementation does real async work instead of returning immediately.
Writing a function with @AppFunction creates the capability. However, because AppFunctions are designed to be executed headlessly by the system (even if the app isn't in the foreground), the Android framework needs a static entry point to find and instantiate the code. Previous versions of the Jetpack library required quite a bit of additional boilerplate. Thankfully, most of that is now handled automatically through Manifest Merging and KSP: when you include the appfunctions-service dependency, a pre-built PlatformAppFunctionService is merged into your app's manifest, acting as the universal entry point for the system. The ksp { } block and tasks.configureEach section in the Gradle listing earlier connect your code to that service: appfunctions:aggregateAppFunctions tells the compiler to emit the aggregate inventory and related assets AppSearch reads, and the merge-after-KSP ordering ensures that output is packaged into the APK.
Testing AppFunctions
Next, let's check that your AppFunctions succeed on real setups.
adb shell cmd app_function list-app-functions | grep -F de.thomaskuenneth.benice
This command should print one or more lines that mention your package and expose each AppFunction’s stable id (often a ClassName#methodName form). This proves the OS indexer has picked up the app after install.
On some Android 16 emulator images that command may return No shell command implementation. In my case, updating the AVD to a system image at API level 36.1 brought the app_function shell path to life; Android Studio shows that revision when you choose the platform image, as in the screenshot below.
Executing an AppFunction on the command line can look frightening:
FID=$(adb shell cmd app_function list-app-functions | grep -F de.thomaskuenneth.benice | grep -oE '[A-Za-z0-9_.]+#[A-Za-z0-9_]+' | head -n1) && adb shell "cmd app_function execute-app-function --package de.thomaskuenneth.benice --function \"$FID\" --parameters '{\"app1\":[\"com.foo\"],\"app2\":[\"com.bar\"]}'"
This command prints a JSON payload: on success it is the AppFunction’s return value (here, the string that createAppPair builds); on failure you may see App function not found (wrong id) or a JSON parse error if --parameters does not use the same AppSearch-style encoding as the example; note how each string argument is passed as a one-element JSON array.
If listing or execution still fails, confirm the aggregate assets are actually packaged (for example unzip -l app/build/outputs/apk/debug/app-debug.apk | grep app_functions should list app_functions.xml and app_functions_v2.xml).
For automated tests, treat the layers separately: run the adb checks above on a device to verify that your metadata is packaged and the indexer has picked up your app. Jetpack’s AppFunctionTestRule is built for local JVM runs (the docs pair it with Robolectric-style environments) so you can exercise AppFunctionManager and your @AppFunction logic without a cable; Google explicitly says to prefer real system-level checks when you can. Add instrumented or integration coverage on an API 36+ image when you care about the full stack (AppSearch sync, shell availability, release vs debug). None of that replaces the device-facing section here; it complements it, and deserves its own write-up once you outgrow copy-paste snippets.
Wrap-up
To close this article, let's see the invocation of the Be nice AppFunction end to end:
AppFunctions sit at the intersection of Jetpack, KSP, manifest merging, and on-device indexing (messy in preview, powerful when the wiring is right). When you integrate them, you keep coming back to three things: platform context, a small Gradle and Kotlin surface that connects the aggregate compiler flag to PlatformAppFunctionService, and the device or APK checks that show whether packaging and indexing still line up.
On the official AppFunctions overview, Google only documents adb shell cmd app_function list-app-functions as a shell check; there is no second, documented adb path for the full schema text (including KDoc folded in via isDescribedByKDoc). For that, read assets/app_functions.xml / app_functions_v2.xml from the APK, or query metadata through AppFunctionManager-style APIs—the same place agents are expected to pull richer descriptions. Anything further that adb shell cmd app_function help shows on a given device is platform-specific and is not spelled out on that overview page.
One caveat worth carrying forward: in my project, making each merge*Assets task depend on the matching ksp*Kotlin task was necessary so KSP-generated AppFunctions assets were present before packaging. That ordering is not spelled out in every official sample, and it may stop being required as the Android Gradle Plugin, KSP, or the AppFunctions toolchain tightens its own task graph. Treat it as something to validate on your stack: if app_functions.xml / app_functions_v2.xml show up in the APK without the extra tasks.configureEach block, you can drop it; if they are missing at runtime, the dependency ordering is still a reliable fix.
If you ship with minify enabled (isMinifyEnabled / R8), the AppFunctions AndroidX artifacts ship consumer ProGuard rules that keep much of the generated and reflection-heavy surface for you. You should still smoke-test a release build on a device: if execution or discovery fails only after shrinking, inspect R8 output and add targeted -keep rules for your own @AppFunction classes or related types; start from what the library already merges rather than copying random snippets from older posts.
In this article, I showed you how to implement an AppFunction. As a publisher you expose AppFunctions in your APK and rely on indexing plus your own validation of arguments. Callers that discover or execute other apps’ functions go through AppFunctionManager-style APIs and sit behind platform rules; privileged assistants hold permissions such as EXECUTE_APP_FUNCTIONS that ordinary store apps do not get by declaring a line in the manifest. Google still describes much of the end-to-end agent path as experimental and capacity-limited, so assume your parameters can be reached only by trusted system-side callers today, and still treat them like untrusted input.
References
The Future of Android Apps with AppFunctions by fellow GDE Shreyas Patil goes deep on dependency injection with AppFunctionConfiguration, a note-taking sample, and adb-driven execution. Further reading beyond the links already woven through the article (overview, Jetpack release notes, platform android.app.appfunctions, intelligent-OS blog, Play listing, and AppFunctionTestRule):
-
AppFunction annotation (
isDescribedByKDoc, compiler behavior, supported types) - AppFunctionManager (Jetpack discovery and execution APIs)
- AppSearch (on-device indexing stack the runtime builds on)
-
Merge multiple manifest files (how library manifests contribute
PlatformAppFunctionServiceand related entries) -
EXECUTE_APP_FUNCTIONS(permission called out in the trust discussion)



Top comments (0)