· 7 min read · Android

Building Android Apps With AppFunctions

#Agents #Android

We’ve spent a long time making Android apps discoverable in one specific way: get users to open the app, then let the UI do the work. Deep links, App Indexing, Play Store listings. All of it was designed around that moment of a user choosing to launch your app.

But look at what Gemini can do right now. A user says “add milk to my grocery list” and the assistant handles it without the grocery app ever opening. Or “set a reminder before my 3pm” and the calendar gets updated. The agent takes the intent and executes it. The app is just… somewhere in the background, providing the logic.

If your app’s functions aren’t reachable from that agent, your app isn’t part of that flow. It doesn’t matter how polished the UI is. The agent can’t see it.

This is the problem Android AppFunctions is designed to solve.

What AppFunctions actually is

The short version: AppFunctions lets your app expose specific functions (typed, documented) that AI agents can discover and call on-device, without your UI being involved at all.

Google describes it as the mobile equivalent of MCP tools, which is actually a good frame. Instead of agents calling into server-side tools over a network, they’re calling into apps running on the device. Your app stops being a thing users navigate and becomes something agents can orchestrate.

The way it works: you annotate your functions with @AppFunction, KSP reads those annotations plus your KDoc and generates an XML schema, and the Android OS indexes that schema. When Gemini handles a user request, it queries the schema to find functions that can fulfill it. It reads your documentation, not your code.

That last bit is the part that changed how I think about this. The description on your function isn’t optional polish. It’s literally what Gemini reads to decide whether to call your app.

Integrating it

AppFunctions ships in Android 16 and is also available via the Jetpack library (as of now at version 1.0.0-alpha08), so expect this API to keep changing. Add the dependencies first:

dependencies {
val appfunversion = "1.0.0-alpha08"
implementation("androidx.appfunctions:appfunctions-service:$appfunversion")
implementation("androidx.appfunctions:appfunctions:$appfunversion")
ksp("androidx.appfunctions:appfunctions-compiler:$appfunversion")
}
ksp {
arg("appfunctions:aggregateAppFunctions", "true")
arg("appfunctions:generateMetadataFromSchema", "false")
}

Then the actual functions. From Google’s own docs, a note-taking app looks like this:

/**
* A note app's [AppFunction]s.
*/
class NoteFunctions(
private val noteRepository: NoteRepository
) {
/**
* 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.appNotes.ifEmpty { null }?.toList()
}
/**
* Adds a new note to the app.
*
* @param appFunctionContext The context in which the AppFunction is executed.
* @param title The title of the note.
* @param content The note's content.
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun createNote(
appFunctionContext: AppFunctionContext,
title: String,
content: String
): Note {
return noteRepository.createNote(title, content)
}
/**
* Edits a single note.
*
* @param appFunctionContext The context in which the AppFunction is executed.
* @param noteId The target note's ID.
* @param title The note's title if it should be updated.
* @param content The new content if it should be updated.
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun editNote(
appFunctionContext: AppFunctionContext,
noteId: Int,
title: String?,
content: String?,
): Note? {
return noteRepository.updateNote(noteId, title, content)
}
}
@AppFunctionSerializable(isDescribedByKDoc = true)
data class Note(
/** The note's identifier */
val id: Int,
/** The note's title */
val title: String,
/** The note's content */
val content: String
)

A couple of things worth noticing here.

The KDoc comments aren’t just for developers reading the code. The annotation processor uses them to generate the schema the agent reads. So when you write “Lists all available notes,” that’s not documentation, that’s the agent’s decision criteria.

And editNote has nullable title and content. That’s intentional. An agent doing a partial update shouldn’t need to send fields it isn’t changing. The API is designed for a caller that doesn’t fill out forms, which requires thinking about parameter design differently than you would for a UI.

For DI, you need to register a factory since the system can’t instantiate your class automatically if it has dependencies:

@HiltAndroidApp
class MyApp : Application(), AppFunctionConfiguration.Provider {
@Inject lateinit var noteFunctions: Provider<NoteFunctions>
override val appFunctionConfiguration: AppFunctionConfiguration
get() = AppFunctionConfiguration
.Builder()
.addEnclosingClassFactory(NoteFunctions::class.java) {
noteFunctions.get()
}
.build()
}

That’s the whole integration.

Testing it

Gemini can’t actually invoke AppFunctions directly yet. The full pipeline is experimental, currently limited to Pixel 10 and Samsung Galaxy S26 Ultra. So the testing workflow for now is ADB.

Terminal window
# list all functions exposed on the device
adb shell cmd app_function list-app-functions --package com.yourapp.package
# run a specific function
adb shell cmd app_function execute-app-function \
--package com.yourapp.package \
--function com.yourapp.NoteFunctions#listNotes \
--parameters {}

The list command is the useful one. It shows the full generated schema with function IDs, descriptions, parameter types, and return types. Worth reading that output carefully. Ask yourself: if you were an agent with no visual context, would you understand what this function does and when to call it? If you have to think about it, so does the agent.

One thing I ran into: the execute command requires --parameters {} even when a function takes no parameters. Leaving it out gives a confusing error. Took me longer than it should have to figure out why.

The security model

Not every app can invoke AppFunctions. EXECUTE_APP_FUNCTIONS is restricted to system-level callers: Gemini, OEM assistants, apps explicitly granted the permission by Google. A random app installed from the Play Store can’t call your functions.

Some people frame this as a limitation. I think it’s actually the right call. You’re potentially exposing payment flows, account data, user content. Having the OS own that trust boundary means you can expose real functionality without worrying about what every installed app on the device might do with it.

The honest limitation is that the ecosystem is currently just Gemini, with some OEM integrations. Whether this takes off depends on how much Gemini grows as a daily assistant. That’s genuinely uncertain.

What gets me excited

From Google’s own docs, the scenarios they’re designing for:

“Find the noodle recipe from Lisa’s email and add the ingredients to my shopping list.” That’s two separate apps, one user request, the agent coordinating the handoff.

“Create a new playlist with the top jazz albums from this year.” Music app gets the call with that intent as a query.

That first one is the pattern I keep coming back to. The agent orchestrating across multiple apps, invisibly, from a single natural language request. Users don’t pick which app handles what. They describe what they want.

And honestly, thinking about what this could mean for apps we already use every day: ordering food, sending money, booking rides, sending messages. The interactions that currently require navigating multiple apps could collapse into a single conversation.

Where things are right now

1.0.0-alpha08. Hardware limited. Early Access Program required for the full agent pipeline. Don’t build anything production-critical on this yet.

But the design work takes time: figuring out which of your app’s capabilities actually make sense to expose, how to write function descriptions that mean something to an agent, how to structure parameters for a non-UI caller. That’s not something you can do in a sprint when GA drops. Worth starting now, even on a branch.

Google is running an Early Access Program for developers who want to test the full end-to-end flow before it’s open to everyone.

Further reading