There's a model shift happening in how users interact with apps that I think most Android developers haven't fully processed yet. The old model: user opens your app, taps through your navigation, completes a task. The new model that Android 16 is betting on: user tells an AI agent what they want, the agent calls into your app directly, and the task gets done without the user ever switching screens.
That second model is what AppFunctions makes possible. It's Android's on-device equivalent of the Model Context Protocol — a way to annotate Kotlin functions so the platform can index them, and AI agents like Google Gemini can discover and invoke them on the user's behalf. The Jetpack library hit 1.0.0-alpha10 on July 1, 2026, and while it's still in alpha, the architecture is solid enough to be worth understanding properly now rather than scrambling later when it stabilizes.
What AppFunctions Actually Are
The MCP (Model Context Protocol) framing that the Android team uses is the clearest way to understand this. In the cloud world, an MCP server exposes tools — typed, documented functions that an LLM can call to take actions or retrieve information. AppFunctions is that same pattern, but running entirely on the device, against your app's live local state, with Android as the discovery and execution broker.
The concrete difference this makes: when a user asks Gemini to "find the note I wrote about the API design and share it with Priya," the agent can call your app's searchNotes(query) function and then your shareNote(noteId, contact) function — with direct access to your local database, not a synced cloud replica of it. Latency is lower, the data is fresh, and the user stays in their current context without launching your app.
Samsung shipped a version of this integration with Galaxy S26 devices in partnership with Gemini, enabling functions like searching photos in Samsung Gallery without opening the app. That's the production shape of what AppFunctions enables more broadly.
Implementing AppFunctions: The Three Pieces
1. @AppFunction — mark what the agent can call
The core annotation is @AppFunction. You put it on any suspend function you want to expose. The first parameter must always be AppFunctionContext — the platform injects this at call time with execution metadata. Everything after it is the function's typed parameter surface, which becomes the tool's input schema.
/**
* Adds a new note to the app.
*
* @param appFunctionContext The context in which the AppFunction is executed.
* @param title The note's title.
* @param content The note's body content.
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun createNote(
appFunctionContext: AppFunctionContext,
title: String,
content: String
): Note {
return noteRepository.createNote(title, content)
}
The isDescribedByKDoc = true flag tells the compiler plugin to extract your KDoc comments into the generated schema. This is not optional if you want the agent to actually understand what the function does — the LLM reads your documentation to figure out which function to call and how to map user intent to parameters. Treat your KDoc as the API contract, not as a comment.
2. @AppFunctionSerializable — mark what crosses the boundary
Any custom type used as a parameter or return value needs @AppFunctionSerializable. The compiler plugin handles the serialization; you just annotate the class and write KDoc for its fields:
/**
* A note stored in the app.
*/
@AppFunctionSerializable(isDescribedByKDoc = true)
data class Note(
/** The note's unique identifier. */
val id: Int,
/** The note's title. */
val title: String,
/** The note's body content. */
val content: String
)
The serializable constraint applies transitively — if your Note contains a Tag, the Tag class needs the annotation too. Primitive types (String, Int, Boolean, LocalDateTime) are handled automatically. The library added Parcelable support back in alpha08, so if your existing domain models are already Parcelable, the migration path is cleaner than it looks.
3. @AppFunctionServiceEntryPoint — the alpha10 change
The biggest API change in alpha10 is @AppFunctionServiceEntryPoint. Previously, you declared functions wherever and the compiler would find them. Now all @AppFunction-annotated methods must live inside a class that's contained within an AppFunctionService annotated with @AppFunctionServiceEntryPoint. This is more structured than the previous approach and gives the platform a clear, single registration point per app:
@AppFunctionServiceEntryPoint
class MyAppFunctionService : AppFunctionService() {
private val noteRepository by lazy { NoteRepository(applicationContext) }
/**
* Lists all available notes.
*
* @param appFunctionContext The context in which the AppFunction is executed.
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun listNotes(appFunctionContext: AppFunctionContext): List<Note>? {
return noteRepository.notes.ifEmpty { null }?.toList()
}
/**
* Adds a new note to the app.
*
* @param appFunctionContext The context in which the AppFunction is executed.
* @param title The note's title.
* @param content The note's body content.
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun createNote(
appFunctionContext: AppFunctionContext,
title: String,
content: String
): Note {
return noteRepository.createNote(title, content)
}
}
The dependency setup is three lines in your module's build file:
dependencies {
implementation("androidx.appfunctions:appfunctions:1.0.0-alpha10")
implementation("androidx.appfunctions:appfunctions-service:1.0.0-alpha10")
ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha10")
}
The compiler artifact is what generates the XML schema that Android uses to index your functions. Without it, you're just annotating dead code.
How the Execution Flow Actually Works
Understanding the execution path helps you write better functions. Here's what happens from annotation to agent call:
- Build time: the KSP compiler plugin processes your
@AppFunctionannotations and generates an XML schema describing every exposed function — its name, parameter types, and the KDoc you wrote. - Install time: Android indexes this schema and registers your app's AppFunctions with the platform. The OS maintains a searchable registry of all AppFunctions across all installed apps.
- Runtime: when an AI agent (Gemini, or any app with the
EXECUTE_APP_FUNCTIONSpermission) handles a user request, it queries the platform registry for functions that match the intent. The LLM uses your KDoc to decide which function fits. - Execution: the agent calls
AppFunctionManagerto invoke the selected function with typed parameters. Yoursuspend funruns on the device, with full access to your app's local state.
No background process required: the platform handles the IPC. Your function runs in your app's process, but the agent doesn't need your Activity or Fragment to be live. AppFunctionService manages the lifecycle — think of it like a ContentProvider, not a BoundService.
Testing What You Built
The fastest feedback loop during development is ADB. Once you've installed an app with AppFunctions declared, you can verify the platform registered them correctly:
# List all AppFunctions registered on the device
adb shell cmd app_function list-app-functions
# Grep for your package to confirm yours were indexed
adb shell cmd app_function list-app-functions | grep "com.your.package"
For end-to-end testing against an actual agent, the AppFunctions GitHub repo (github.com/android/appfunctions) ships a sample agent app you can sideload. It exposes a simple invocation UI so you can trigger specific functions without needing Gemini integration to be live in your region or account.
What to Actually Expose in Your App
Not every feature belongs in an AppFunction. The pattern that works is exposing discrete, stateless actions that complete a meaningful chunk of user intent without needing clarifying UI. Thinking through my own apps:
For a notes app like Nodat: createNote(title, content), searchNotes(query), listNotebooks(), moveNoteToNotebook(noteId, notebookId). These are atomic — the agent can chain them to handle "find my API design note and move it to the Work notebook" without any UI handoff.
For a music app like Musist: createPlaylist(name), searchTracks(query), addTrackToPlaylist(trackId, playlistId), getCurrentlyPlaying(). Keep them small and composable. Don't try to build a "do everything in one call" mega-function — the agent is the composer.
For a news app like Samachar: searchArticles(query, category), getTopHeadlines(category), saveArticle(articleId). The agent can handle "save the three top tech stories from today to my reading list" by calling getTopHeadlines("tech") and then saveArticle three times.
The KDoc discipline matters more than the code: I've seen function implementations that were 3 lines of repository delegation, but the KDoc took 15 lines to precisely describe what "query" accepts, what counts as a match, what null return means, and what the side effects are. That documentation is literally the schema the LLM uses to decide whether to call your function and how to construct the parameters. Write it like you're writing a REST API spec.
The Alpha10 API Caveats to Know Now
The library is evolving quickly and alpha10 brings breaking changes. The existing AppFunction and AppFunctionConfiguration APIs are being migrated toward @AppFunctionServiceEntryPoint, and the release notes explicitly note that AppFunctionConfiguration "will be replaced" in upcoming releases. If you're starting fresh now, build against alpha10's model — don't reach back for earlier patterns.
The two bigger constraints worth knowing:
- Android 16+ only. This is a hard platform floor, not a graceful-degradation situation. AppFunctionManager returns null on older devices, so guard with a null check rather than a version check — it's cleaner and forward-compatible.
- Gemini integration is still in private preview. You can build and test with the sample agent app today. Production Gemini integration with the full discovery-and-execution flow requires joining the early access program at
goo.gle/eap-af. The EAP isn't automatic — selected apps are notified by email.
The new @AppFunctionInstruction and @AppFunctionSignature annotations added in alpha10 are for declaring runtime-registered functions — a pattern where the available function set can change after install without a schema rebuild. That's a more advanced use case (think apps where users configure which workflows are available) and worth ignoring until you've got the basics shipping.
Why This Actually Matters
I think about this from the user retention angle. In Nodat's case, a user who can access their notes through a voice assistant without launching the app isn't losing engagement with the content — they're getting faster access to value. The app that exposes AppFunctions well will show up in Gemini flows as a reliable tool. The app that doesn't will eventually feel like it's missing from the user's AI-powered workflow.
The apps that gain most from this first are the ones with clean, atomic data operations — notes, tasks, calendars, media libraries, shopping lists. If your app's core actions are "create, read, search, share," you can probably expose all of them as AppFunctions in a day. The harder case is apps with complex multi-step flows or UI-driven workflows, which need more thought about where the agent boundary is.
The Android team has also shipped an AI agent skill for Android Studio (at github.com/android/skills/tree/main/device-ai/appfunctions) that analyzes your app's existing workflows, generates the required Kotlin code and optimized KDoc, and produces the ADB commands for testing. If you want to explore without writing from scratch, that's the fastest starting point.
This is still early. The library will have breaking changes before 1.0 stable. But the architecture — suspend functions, typed parameters, KDoc as schema — maps directly onto how Kotlin developers already write code. There's no new mental model to learn here, which makes the adoption curve much lower than it looks at first glance.