<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ioannis Anifantakis</title>
    <description>The latest articles on DEV Community by Ioannis Anifantakis (@ioannis_anifantakis_4dc2c).</description>
    <link>https://dev.to/ioannis_anifantakis_4dc2c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2768419%2F6d6476d9-3acf-475f-a46f-bea662ae0f21.jpg</url>
      <title>DEV Community: Ioannis Anifantakis</title>
      <link>https://dev.to/ioannis_anifantakis_4dc2c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ioannis_anifantakis_4dc2c"/>
    <language>en</language>
    <item>
      <title>AppFunctions: Making Your Android App Discoverable by AI Agents</title>
      <dc:creator>Ioannis Anifantakis</dc:creator>
      <pubDate>Fri, 29 May 2026 12:27:21 +0000</pubDate>
      <link>https://dev.to/ioannis_anifantakis_4dc2c/appfunctions-making-your-android-app-discoverable-by-ai-agents-50fm</link>
      <guid>https://dev.to/ioannis_anifantakis_4dc2c/appfunctions-making-your-android-app-discoverable-by-ai-agents-50fm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A practical first look at Google's new Jetpack API for exposing on-device app capabilities as tools an agent like Gemini can call&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Google just shipped the first public docs and the first usable alpha of &lt;strong&gt;AppFunctions&lt;/strong&gt; — a Jetpack library paired with a new Android platform API that turns parts of your app into tools that AI agents can discover and execute. If you have ever used the Model Context Protocol (MCP) on the server side, AppFunctions is the Android-native equivalent: same idea, but the tool lives inside your app and runs locally on the device.&lt;/p&gt;

&lt;p&gt;It helps to be precise about what is actually new here, because the &lt;em&gt;capability&lt;/em&gt; is not — its &lt;strong&gt;universality&lt;/strong&gt; is. Until now, getting an assistant to act inside an app on your behalf was overwhelmingly a first-party affair: you could ask Google Assistant to add an event to Google Calendar, Bixby to create one in Samsung's calendar, or Siri to do the same on Apple's — each assistant wired tightly to its own vendor's apps. Third-party apps were not completely shut out, but every door was a different shape. Android's &lt;strong&gt;App Actions&lt;/strong&gt; let you expose a &lt;em&gt;fixed catalog&lt;/em&gt; of built-in intents to Google Assistant (and frequently resolved them by launching your UI rather than running logic headlessly); Apple's &lt;strong&gt;App Intents / SiriKit&lt;/strong&gt; did the analogous thing for Siri; &lt;strong&gt;Bixby&lt;/strong&gt; had its Capsules. Each was assistant-specific, usually limited to a predefined vocabulary, and never portable — meaningful agentic control was either a vendor privilege or a stack of per-assistant integrations, with your app carrying code that targeted one agent and one agent only. AppFunctions sets out to replace all of that with a single, OS-level, agent-agnostic interface: your app declares what it can do &lt;em&gt;once&lt;/em&gt;, and any caller the platform trusts can discover and execute it — no per-assistant code.&lt;/p&gt;

&lt;p&gt;This article is an early hands-on walkthrough. I started from a minimal "hello world" AppFunction on an existing app I have been using as a teaching sample — a small jokes app with a Room database and an MVI presentation layer — and then grew it into a small but rounded demo: three functions that, between them, cover a plain-text result, structured data, typed parameters and errors, explicit threading, and where this kind of code belongs in a layered codebase. It is still a demo rather than a large body of work, and I will walk through it piece by piece so you can do the same in your own project today. You will find all of it on the sample app's branch, linked below.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository branch: &lt;a href="https://github.com/ioannisa/MitropolitikoNetworkApp/tree/07-App-Functions" rel="noopener noreferrer"&gt;&lt;code&gt;07-App-Functions&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google's docs: &lt;a href="https://developer.android.com/ai/appfunctions" rel="noopener noreferrer"&gt;AppFunctions overview&lt;/a&gt; and &lt;a href="https://developer.android.com/ai/appfunctions/add-appfunctions" rel="noopener noreferrer"&gt;Add the AppFunctions API to your app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A note up front: AppFunctions is experimental. As of May 2026, Gemini integration is in a private preview with trusted testers. The developer-side path, however, is open — you can build, register, and execute AppFunctions today using &lt;code&gt;adb&lt;/code&gt;, which is exactly what we will do at the end of this article.&lt;/p&gt;




&lt;h2&gt;
  
  
  What MCP actually is
&lt;/h2&gt;

&lt;p&gt;If you have not worked with the Model Context Protocol (MCP) before, a one-paragraph primer will make everything that follows easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP&lt;/strong&gt; is an open standard, originally published by Anthropic in late 2024 and now adopted by a growing number of AI products, that defines a single way for AI agents to connect to external &lt;strong&gt;tools&lt;/strong&gt; and &lt;strong&gt;data sources&lt;/strong&gt;. The model is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;MCP server&lt;/strong&gt; is a small process that exposes a list of tools. Each tool has a name, a plain-language description, and a JSON Schema that describes its parameters and return value.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;MCP client&lt;/strong&gt; — typically the AI agent — connects to the server, asks "what tools do you have?", reads the descriptions, and decides which tool to call based on the user's request.&lt;/li&gt;
&lt;li&gt;The conversation between them is JSON-RPC, carried over stdio (for local servers) or HTTP (for remote ones).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before MCP, each integration was bespoke — every AI product had its own way of plugging in tools. With MCP, a single server that exposes "search my email" can be consumed by any MCP-compatible agent without rewriting the integration on each side.&lt;/p&gt;

&lt;p&gt;The friction is that traditional MCP servers run &lt;strong&gt;outside&lt;/strong&gt; your application, often as separate processes or cloud services. They have no privileged access to your app's state. If you wanted an MCP-style tool that can read an unsent draft inside a note-taking app, you would either have to expose that draft through some API or run the MCP server inside the app's own process. Neither is convenient on mobile.&lt;/p&gt;

&lt;p&gt;That is the gap AppFunctions fills.&lt;/p&gt;




&lt;h2&gt;
  
  
  What AppFunctions actually is
&lt;/h2&gt;

&lt;p&gt;AppFunctions is best understood as &lt;strong&gt;MCP for Android, but on-device&lt;/strong&gt;. Your app declares a class, annotates some of its methods with &lt;code&gt;@AppFunction&lt;/code&gt;, and the Jetpack annotation processor generates the metadata and the wiring needed for the OS to index those methods. From that moment on, any caller with the &lt;code&gt;EXECUTE_APP_FUNCTIONS&lt;/code&gt; permission — including system agents — can discover your function, read its description, and invoke it with parameters.&lt;/p&gt;

&lt;p&gt;Three properties make this interesting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It is local.&lt;/strong&gt; No network round-trip, no server to maintain. The agent calls into your already-running app and reads its current state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is indexed by the OS.&lt;/strong&gt; You do not register anything at runtime. The annotation processor emits a generated schema into your APK (&lt;code&gt;assets/app_function_v2.xml&lt;/code&gt;), the OS reads it at install time, and it maintains a registry that agents can query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The function's documentation becomes part of its contract.&lt;/strong&gt; When you mark a function with &lt;code&gt;@AppFunction(isDescribedByKDoc = true)&lt;/code&gt;, your KDoc is encoded into the function's metadata and shown to the agent. Writing good KDoc stops being only a documentation concern — it becomes a runtime concern.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Minimum requirements: &lt;code&gt;compileSdk = 37&lt;/code&gt; and &lt;code&gt;targetSdk = 36&lt;/code&gt; or higher, devices running Android 16 or higher. (Google's own AppFunctions tooling lists &lt;code&gt;compileSdk 37&lt;/code&gt; / &lt;code&gt;targetSdk 36&lt;/code&gt; as the floor; the sample app in this article builds against &lt;code&gt;compileSdk = 37&lt;/code&gt; / &lt;code&gt;targetSdk = 37&lt;/code&gt;.)&lt;/p&gt;




&lt;h2&gt;
  
  
  The sample app in 60 seconds
&lt;/h2&gt;

&lt;p&gt;The base app is a small Compose application that fetches jokes from a remote source using Ktor and lets the user mark favorites. It uses Room for offline cache and local favorites storage, and a typical layered architecture — DAO, data source, repository, view model, screen. Nothing here is unusual; it is the same skeleton most production Android apps have.&lt;/p&gt;

&lt;p&gt;The first feature I added is intentionally tiny: &lt;strong&gt;clear all favorite jokes&lt;/strong&gt;. The exact same capability is reachable two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user can tap a menu item in the top bar.&lt;/li&gt;
&lt;li&gt;An AI agent can call the &lt;code&gt;clearFavorites&lt;/code&gt; AppFunction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both paths end up calling the same repository method. That is the principle I want to leave you with before we even look at code: &lt;strong&gt;an AppFunction is a thin shell over normal app logic, never a duplicate of it&lt;/strong&gt;. We start with this one function, then add two more — &lt;code&gt;getFavorites&lt;/code&gt; and &lt;code&gt;setFavorite&lt;/code&gt; — once the basics are in place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — The dependencies
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;gradle/libs.versions.toml&lt;/code&gt; and add the AppFunctions artifacts. The current alpha is &lt;code&gt;1.0.0-alpha09&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[versions]&lt;/span&gt;
&lt;span class="py"&gt;appfunctions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0.0-alpha09"&lt;/span&gt;

&lt;span class="nn"&gt;[libraries]&lt;/span&gt;
&lt;span class="py"&gt;androidx-appfunctions&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"androidx.appfunctions:appfunctions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="py"&gt;version.ref&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"appfunctions"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;androidx-appfunctions-service&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"androidx.appfunctions:appfunctions-service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="py"&gt;version.ref&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"appfunctions"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;androidx-appfunctions-compiler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"androidx.appfunctions:appfunctions-compiler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;version.ref&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"appfunctions"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in &lt;code&gt;app/build.gradle.kts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;ksp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"appfunctions:aggregateAppFunctions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// App Functions&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;androidx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appfunctions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;androidx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appfunctions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;ksp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;androidx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appfunctions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compiler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;ksp(libs.androidx.appfunctions.compiler)&lt;/code&gt; dependency registers the AppFunctions compiler as a KSP processor — that is the line that actually wires annotation processing in. The &lt;code&gt;ksp { arg("appfunctions:aggregateAppFunctions", "true") }&lt;/code&gt; block above it passes a configuration flag to that processor, telling it to &lt;strong&gt;aggregate&lt;/strong&gt; every &lt;code&gt;@AppFunction&lt;/code&gt; declared across your project into one merged schema. In a multi-module project you set this flag only in the application module; library modules that contain &lt;code&gt;@AppFunction&lt;/code&gt; declarations just need the &lt;code&gt;ksp(...)&lt;/code&gt; compiler dependency.&lt;/li&gt;
&lt;li&gt;You also need at least &lt;code&gt;compileSdk = 37&lt;/code&gt; (and &lt;code&gt;targetSdk = 36&lt;/code&gt; or higher). If you are still on a lower level, this is a one-line bump in your build file. The sample app uses &lt;code&gt;compileSdk = 37&lt;/code&gt; and &lt;code&gt;targetSdk = 37&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 2 — Manifest plumbing
&lt;/h2&gt;

&lt;p&gt;The OS needs to know two things at install time: where to read your app's AppFunction metadata, and which service to bind when an agent wants to execute one of your functions. Both go inside &lt;code&gt;&amp;lt;application&amp;gt;&lt;/code&gt; in &lt;code&gt;AndroidManifest.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;application&lt;/span&gt;
    &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt;
        &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.app.appfunctions.app_metadata"&lt;/span&gt;
        &lt;span class="na"&gt;android:resource=&lt;/span&gt;&lt;span class="s"&gt;"@xml/app_metadata"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;service&lt;/span&gt;
        &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"androidx.appfunctions.service.PlatformAppFunctionService"&lt;/span&gt;
        &lt;span class="na"&gt;android:permission=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.BIND_APP_FUNCTION_SERVICE"&lt;/span&gt;
        &lt;span class="na"&gt;android:exported=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
        &lt;span class="na"&gt;tools:targetApi=&lt;/span&gt;&lt;span class="s"&gt;"36"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;intent-filter&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;action&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.app.appfunctions.AppFunctionService"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/intent-filter&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/service&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- your activities here --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/application&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each piece does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt;property&amp;gt;&lt;/code&gt; element points the OS to &lt;code&gt;app_metadata.xml&lt;/code&gt;, which carries the &lt;strong&gt;app-level&lt;/strong&gt; description — what this app is about as a whole, so the AI agents can know what each app is about. Per-function descriptions are a separate concern: they come from the KDoc on each &lt;code&gt;@AppFunction&lt;/code&gt; method.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt;service&amp;gt;&lt;/code&gt; declaration exposes &lt;code&gt;PlatformAppFunctionService&lt;/code&gt;, which is the bridge the platform uses to invoke your functions. Luckily, you do &lt;strong&gt;not&lt;/strong&gt; write this service yourself — it ships in the &lt;code&gt;appfunctions-service&lt;/code&gt; library. You only need to declare it in the manifest with the right permission and intent filter so the system can find and bind to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;BIND_APP_FUNCTION_SERVICE&lt;/code&gt; permission ensures only the platform can bind to the service. You do not need to request &lt;code&gt;EXECUTE_APP_FUNCTIONS&lt;/code&gt; — that is the &lt;strong&gt;caller's&lt;/strong&gt; permission, not yours.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Describe the app itself in &lt;code&gt;app_metadata.xml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The manifest's &lt;code&gt;&amp;lt;property&amp;gt;&lt;/code&gt; element points at &lt;code&gt;res/xml/app_metadata.xml&lt;/code&gt;. As the file name suggests, this file carries the &lt;strong&gt;app-level description&lt;/strong&gt; that the OS exposes to agents, alongside the schemas of individual functions. Here is the one I added to the sample app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;AppFunctionAppMetadata&lt;/span&gt;
    &lt;span class="na"&gt;xmlns:appfn=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.android.com/apk/androidx.appfunctions"&lt;/span&gt;
    &lt;span class="na"&gt;appfn:description=&lt;/span&gt;&lt;span class="s"&gt;"This app allows users to view and manage jokes, including marking them as favorites and clearing the favorites list."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structure is intentionally small for now — a single root element, a single &lt;code&gt;description&lt;/code&gt; attribute. But the meaning is bigger than the syntax.&lt;/p&gt;

&lt;p&gt;AppFunctions actually gives the agent &lt;strong&gt;two layers of documentation&lt;/strong&gt;, and it is worth being explicit about how they relate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Where you write it&lt;/th&gt;
&lt;th&gt;What it tells the agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App-level&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;app_metadata.xml&lt;/code&gt; (&lt;code&gt;appfn:description&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;What this app is about as a whole — its purpose, its domain, what kinds of things it can do&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Function-level&lt;/td&gt;
&lt;td&gt;KDoc above each &lt;code&gt;@AppFunction&lt;/code&gt; method&lt;/td&gt;
&lt;td&gt;What this specific function does, what its parameters mean, and what it returns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the same hierarchical model MCP uses on the server side — an MCP server has a description, and each tool within it has its own. The agent uses the high-level description first to decide &lt;em&gt;is this even the right app to look inside?&lt;/em&gt;, then drills into the function descriptions to pick the right one within that app.&lt;/p&gt;

&lt;p&gt;So treat &lt;code&gt;appfn:description&lt;/code&gt; the same way I urged you to treat your KDoc: write it as runtime input to an LLM, not as marketing copy. Short, concrete, focused on the verbs the app supports ("view and manage jokes", "marking as favorites", "clearing the favorites list") rather than on positioning ("the best joke companion on Android"). Be honest about what the app does, because that is what the agent will trust when it has to choose between three different apps that all advertise "jokes".&lt;/p&gt;

&lt;p&gt;A second attribute exists alongside it: &lt;code&gt;appfn:displayDescription&lt;/code&gt;. The two are aimed at different audiences — &lt;code&gt;appfn:description&lt;/code&gt; is the LLM-facing text the agent reasons over, while &lt;code&gt;appfn:displayDescription&lt;/code&gt; is a human-readable, user-visible description. That difference shows up in how the library declares the two attributes (in &lt;code&gt;appfunctions&lt;/code&gt;'s own &lt;code&gt;res/values/values.xml&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;attr&lt;/span&gt; &lt;span class="na"&gt;format=&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;           &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;attr&lt;/span&gt; &lt;span class="na"&gt;format=&lt;/span&gt;&lt;span class="s"&gt;"reference|string"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"displayDescription"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;appfn:displayDescription&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; have to be a string resource — &lt;code&gt;reference|string&lt;/code&gt; means it accepts either a &lt;code&gt;@string/...&lt;/code&gt; reference &lt;em&gt;or&lt;/em&gt; a literal string. Prefer the resource form (&lt;code&gt;appfn:displayDescription="@string/app_function_user_description"&lt;/code&gt;) anyway: this text is shown to users, so a string resource gives you localization for free. Note the asymmetry — &lt;code&gt;description&lt;/code&gt; is plain &lt;code&gt;string&lt;/code&gt; (write it inline, for the model), while &lt;code&gt;displayDescription&lt;/code&gt; is &lt;code&gt;reference|string&lt;/code&gt; (point it at a localizable resource, for people). The minimal sample above uses only &lt;code&gt;appfn:description&lt;/code&gt;; add &lt;code&gt;displayDescription&lt;/code&gt; once you have a user-facing surface that should show what your app exposes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;appfn:description&lt;/code&gt; is &lt;em&gt;app-wide&lt;/em&gt; — there is exactly one per app (that is why it is declared once, via the manifest &lt;code&gt;&amp;lt;property&amp;gt;&lt;/code&gt;), and it never describes an individual function; each function's own description comes from its KDoc. But precisely because it sits above every function, it is the only place that can speak about how your functions relate to &lt;em&gt;each other&lt;/em&gt;. With a single function there is nothing cross-cutting to say, so one sentence is plenty. Once an app exposes several functions, Google's own AppFunctions guidance suggests giving that one app-wide description more structure — think of it as &lt;em&gt;server instructions&lt;/em&gt; for the agent rather than a tagline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;AppFunctionAppMetadata&lt;/span&gt;
    &lt;span class="na"&gt;xmlns:appfn=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.android.com/apk/androidx.appfunctions"&lt;/span&gt;
    &lt;span class="na"&gt;appfn:description=&lt;/span&gt;&lt;span class="s"&gt;"This app lets users view and manage jokes and their favorites.
        Operational Patterns:
        - Call 'getFavorites' to obtain a valid joke id before calling 'setFavorite'.
        Constraints:
        - 'clearFavorites' is irreversible; confirm with the user before calling it."&lt;/span&gt;
    &lt;span class="na"&gt;appfn:displayDescription=&lt;/span&gt;&lt;span class="s"&gt;"@string/app_function_user_description"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two conventions worth carrying over from that guidance: an &lt;strong&gt;Operational Patterns&lt;/strong&gt; section that nudges the agent toward token-efficient, correct sequences (for example, "call &lt;code&gt;getFavorites&lt;/code&gt; before &lt;code&gt;setFavorite&lt;/code&gt;"), and a &lt;strong&gt;Constraints&lt;/strong&gt; section that draws hard boundaries. Avoid repeating individual function descriptions or adding marketing copy here — the per-function KDoc already carries the function-level detail. The release notes for the &lt;code&gt;androidx.appfunctions&lt;/code&gt; library are the place to track what else lands as the API stabilizes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — Extend the data layer (the boring, normal part)
&lt;/h2&gt;

&lt;p&gt;Before we touch any AppFunctions code, we add the new business capability through the existing layers. The point I made earlier — that an AppFunction is a thin shell over normal logic — only works if that normal logic exists first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DAO&lt;/strong&gt; (&lt;code&gt;JokesDao.kt&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UPDATE joke SET isFavorite = 0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;clearAllFavorites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Local data source&lt;/strong&gt; (&lt;code&gt;LocalJokesDataSource.kt&lt;/code&gt; and its implementation):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;LocalJokesDataSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;clearAllFavorites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LocalJokesDataSourceImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LocalJokesDataSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;clearAllFavorites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearAllFavorites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Repository&lt;/strong&gt; (&lt;code&gt;JokesRepository.kt&lt;/code&gt; and its implementation):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;JokesRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;clearAllFavorites&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JokesRepositoryImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;JokesRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;clearAllFavorites&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;safeCall&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;localDataSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearAllFavorites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing here knows or cares about AppFunctions. It is just normal Android architecture, and that is the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5 — The AppFunction itself
&lt;/h2&gt;

&lt;p&gt;Now we add the actual function the agent will call. This is a plain Kotlin class — no inheritance, no Android lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;eu.anifantakis.networkapp.jokes.features.jokes.appfunctions&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.appfunctions.AppFunctionContext&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.appfunctions.service.AppFunction&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;eu.anifantakis.networkapp.jokes.di.AppModule&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * App Functions that can be exposed to AI agents via MCP.
 */&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JokesAppFunctions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * Unmarks every joke the user has previously marked as a favorite, leaving the favorites list empty.
     *
     * Only the favorite flag is affected. The underlying jokes remain in the database and are still
     * visible in the main list. This operation is safe to repeat: calling it on an already-empty favorites
     * list is a no-op. It is irreversible: cleared favorite markers cannot be restored.
     *
     * @return A short human-readable status message describing whether the operation succeeded.
     */&lt;/span&gt;
    &lt;span class="nd"&gt;@AppFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDescribedByKDoc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;clearFavorites&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppFunctionContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jokesRepository&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearAllFavorites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"All favorite jokes have been cleared successfully."&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"Failed to clear favorite jokes: ${result.exceptionOrNull()?.message}"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things worth pausing on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;isDescribedByKDoc = true&lt;/code&gt;&lt;/strong&gt; — the KDoc above is very useful for the agent. It clearly explains what changes are involved (the favorite flag), what remains unchanged (the underlying jokes), whether the operation is safe to retry (yes, calling it again does no extra harm), and whether it can be undone (no). An agent reading this has enough to decide whether to call this function or ask the user for confirmation first. In comparison, a phrase like "Removes favorites" has the same aim but lacks the details; the agent would not understand what "remove" means or whether the operation can be reversed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same care applies to parameter names and types&lt;/strong&gt; once your function takes any. A parameter called &lt;code&gt;filter: String&lt;/code&gt; reads as concrete to a human navigating a UI, but as a black hole to an LLM reading a schema. Names like &lt;code&gt;daysOlderThan: Int&lt;/code&gt; or &lt;code&gt;category: JokeCategory&lt;/code&gt; carry their meaning forward; vague names quietly widen the space for the agent to guess wrong. Schemas your UI forgave will silently fail here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first parameter is always &lt;code&gt;AppFunctionContext&lt;/code&gt;.&lt;/strong&gt; The system passes this in; you do not. It is your hook for accessing system services and identifying the caller.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The return type here is a &lt;code&gt;String&lt;/code&gt;.&lt;/strong&gt; The agent will treat that text as the result it can show back to the user. For more sophisticated functions you would return a serializable data class annotated with &lt;code&gt;@AppFunctionSerializable&lt;/code&gt; — the agent then receives structured data and can format it however it wants. For a "Hello World" example like this one, a sentence of plain text is enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This function is &lt;code&gt;suspend&lt;/code&gt;.&lt;/strong&gt; That is not optional — by default, AppFunction implementations run on the main thread, so any I/O must suspend. In our case the repository call already handles dispatching internally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A note on dependencies: this class has a no-arg constructor and reaches into &lt;code&gt;AppModule.jokesRepository&lt;/code&gt; directly. That works for a hello world. In a real codebase you would constructor-inject the repository through Hilt or Koin, and then provide a factory so the OS knows how to instantiate the class — which brings us to the next step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5b — Beyond a string: structured data, parameters, and errors
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clearFavorites&lt;/code&gt; is a deliberately minimal "hello world": no parameters, a plain &lt;code&gt;String&lt;/code&gt; return, no error surface beyond a status sentence. Most real AppFunctions need more, and the three things they most often need are exactly the three the platform supports out of the box: &lt;strong&gt;structured return values&lt;/strong&gt;, &lt;strong&gt;typed parameters&lt;/strong&gt;, and &lt;strong&gt;typed errors&lt;/strong&gt;. Two more functions on the same class show all three.&lt;/p&gt;

&lt;h3&gt;
  
  
  Returning structured data with &lt;code&gt;@AppFunctionSerializable&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;When you want the agent to receive data it can read, count, or quote — not just a sentence — return a serializable type instead of a &lt;code&gt;String&lt;/code&gt;. You annotate a plain data class with &lt;code&gt;@AppFunctionSerializable&lt;/code&gt; and return it (or a &lt;code&gt;List&lt;/code&gt; of it) directly.&lt;/p&gt;

&lt;p&gt;A design point first, because it is the one most likely to feel like duplication. We already have three joke models: the domain &lt;code&gt;Joke&lt;/code&gt;, the network &lt;code&gt;JokeDto&lt;/code&gt;, and the Room &lt;code&gt;JokeEntity&lt;/code&gt;. Adding a fourth — an &lt;code&gt;@AppFunctionSerializable&lt;/code&gt; class — looks redundant, but it is the same boundary-model pattern those others follow. We &lt;em&gt;cannot&lt;/em&gt; annotate the domain &lt;code&gt;Joke&lt;/code&gt;: &lt;code&gt;@AppFunctionSerializable&lt;/code&gt; lives in &lt;code&gt;androidx.appfunctions&lt;/code&gt;, and dragging an Android dependency into the domain layer would break its purity. So the AppFunctions boundary gets its own DTO, exactly as the network and persistence boundaries do — and, as a bonus, the schema the agent sees can be shaped independently of the domain (note that this DTO drops &lt;code&gt;isFavorite&lt;/code&gt;, which is always true in a favorites list and would just be noise for the model). The conversion is a mapper, the analog of the existing &lt;code&gt;toJoke()&lt;/code&gt;/&lt;code&gt;toEntity()&lt;/code&gt; extensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AppFunctionJoke.kt&lt;/span&gt;
&lt;span class="c1"&gt;// Developer rationale lives in a plain `//` comment on purpose (see the note below): this is the&lt;/span&gt;
&lt;span class="c1"&gt;// agent-facing boundary DTO, the counterpart of the network JokeDto and the Room JokeEntity. We&lt;/span&gt;
&lt;span class="c1"&gt;// can't annotate the domain Joke (androidx code would break domain purity), so the AppFunctions&lt;/span&gt;
&lt;span class="c1"&gt;// boundary gets its own model — and we drop isFavorite, which is noise for a favorites list.&lt;/span&gt;
&lt;span class="cm"&gt;/**
 * A single joke from this app, modelled as a question-and-answer pair: the question is the set-up
 * and the answer is the punchline. Returned by joke-reading functions such as the user's favorites
 * list, and identified by a stable numeric id that other functions accept to act on this joke.
 */&lt;/span&gt;
&lt;span class="nd"&gt;@AppFunctionSerializable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDescribedByKDoc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;AppFunctionJoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="cm"&gt;/** Stable unique identifier of the joke. Pass this back to functions that act on a single joke. */&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="cm"&gt;/** The set-up line of the joke (its "question" part). */&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="cm"&gt;/** The punchline of the joke (its "answer" part). */&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// AppFunctionJokeMapper.kt — the boundary mapper, like toJoke()/toEntity()&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Joke&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toAppFunctionJoke&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;AppFunctionJoke&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AppFunctionJoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Lists every joke the user has currently marked as a favorite, including its set-up and punchline.
 *
 * Returns structured data rather than a sentence, so a calling agent can read, count, or quote
 * individual jokes. Use the id of a returned joke when calling "setFavorite" to unmark a specific
 * one. An empty list means the user has no favorites.
 *
 * @return The user's favorite jokes; an empty list if there are none.
 */&lt;/span&gt;
&lt;span class="nd"&gt;@AppFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDescribedByKDoc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getFavorites&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppFunctionContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AppFunctionJoke&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dispatchers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jokesRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFavorites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;joke&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;joke&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toAppFunctionJoke&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two non-obvious things about the KDoc on a serializable are worth saying out loud, because both bite &lt;em&gt;silently&lt;/em&gt; — they compile fine and only misbehave at runtime, inside the agent.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;where&lt;/strong&gt; KSP looks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;KSP only extracts KDoc that is written &lt;em&gt;inline&lt;/em&gt;, directly above each property, for &lt;code&gt;@AppFunctionSerializable&lt;/code&gt; classes.&lt;/strong&gt; If you document the properties with class-level &lt;code&gt;@param&lt;/code&gt; or &lt;code&gt;@property&lt;/code&gt; tags instead, KSP extracts nothing for those properties, and the function shows up at runtime as "metadata missing" / "AppFunction unavailable". Put the doc on the property, not in the class header.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the one place the two documentation styles in this article genuinely diverge, and it is worth pinning down because the instinct to reuse &lt;code&gt;@param&lt;/code&gt; is so strong. The properties of a data class &lt;em&gt;are&lt;/em&gt; constructor parameters, and for an &lt;strong&gt;&lt;code&gt;@AppFunction&lt;/code&gt; method&lt;/strong&gt; you absolutely do document parameters with &lt;code&gt;@param&lt;/code&gt; tags — KSP reads them (the &lt;code&gt;@param jokeId …&lt;/code&gt; on &lt;code&gt;setFavorite&lt;/code&gt; below becomes that parameter's schema description). But for an &lt;strong&gt;&lt;code&gt;@AppFunctionSerializable&lt;/code&gt; class&lt;/strong&gt; it is the opposite: &lt;code&gt;@param&lt;/code&gt;/&lt;code&gt;@property&lt;/code&gt; tags in the class header are ignored, and only the inline KDoc is read. Same word "param," two different KSP code paths. I confirmed this by documenting &lt;code&gt;AppFunctionJoke&lt;/code&gt; both ways and diffing the generated &lt;code&gt;assets/app_function_v2.xml&lt;/code&gt;: with &lt;code&gt;@param&lt;/code&gt;/&lt;code&gt;@property&lt;/code&gt; tags, every property's &lt;code&gt;&amp;lt;description&amp;gt;&lt;/code&gt; came out &lt;strong&gt;empty&lt;/strong&gt;, and the raw tag text leaked into the &lt;em&gt;type's&lt;/em&gt; description instead; with inline KDoc, each property got its proper description. Rule of thumb: &lt;strong&gt;&lt;code&gt;@param&lt;/code&gt; for functions, inline KDoc for serializable properties.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Second, and easy to miss: &lt;strong&gt;the class-level KDoc summary becomes the agent-facing description of the type.&lt;/strong&gt; With &lt;code&gt;isDescribedByKDoc = true&lt;/code&gt;, whatever you write in the KDoc block above the class is emitted into the generated schema as that type's description — exactly as the function's KDoc summary becomes the function description. I learned this the hard way by reading the generated &lt;code&gt;assets/app_function_v2.xml&lt;/code&gt;: an earlier version of this class carried a paragraph of &lt;em&gt;developer&lt;/em&gt; rationale in its KDoc ("this is the boundary DTO, the counterpart of JokeDto…"), and all of it shipped into the schema as noise the agent would have to read. The rule that falls out of this: KDoc on anything annotated for AppFunctions is &lt;em&gt;agent-facing copy&lt;/em&gt;. Keep developer-only rationale in plain &lt;code&gt;//&lt;/code&gt; comments, which KSP ignores, and keep the KDoc itself a clean, concrete description.&lt;/p&gt;

&lt;p&gt;The same trap explains a small but real detail in the function KDoc above: it says &lt;code&gt;"setFavorite"&lt;/code&gt; in plain quotes rather than the KDoc link &lt;code&gt;[setFavorite]&lt;/code&gt;. KDoc link brackets are not resolved on the way into the schema — they survive verbatim, so an agent would read raw &lt;code&gt;[setFavorite]&lt;/code&gt; (and, worse, internal symbol paths like &lt;code&gt;[AppFunctionJoke.id]&lt;/code&gt;). Quote names plainly; save the &lt;code&gt;[...]&lt;/code&gt; links for KDoc that humans read in the IDE.&lt;/p&gt;

&lt;p&gt;A note on the supported types, since you will reach for them as soon as you leave &lt;code&gt;String&lt;/code&gt; behind. As of this alpha, an &lt;code&gt;@AppFunction&lt;/code&gt; parameter or return value may be: a &lt;strong&gt;primitive&lt;/strong&gt; (&lt;code&gt;Int&lt;/code&gt;, &lt;code&gt;Long&lt;/code&gt;, &lt;code&gt;Float&lt;/code&gt;, &lt;code&gt;Double&lt;/code&gt;, &lt;code&gt;Boolean&lt;/code&gt;); a &lt;strong&gt;primitive array&lt;/strong&gt; (&lt;code&gt;IntArray&lt;/code&gt;, &lt;code&gt;LongArray&lt;/code&gt;, &lt;code&gt;FloatArray&lt;/code&gt;, &lt;code&gt;DoubleArray&lt;/code&gt;, &lt;code&gt;BooleanArray&lt;/code&gt;); a &lt;strong&gt;native type&lt;/strong&gt; (&lt;code&gt;String&lt;/code&gt;, &lt;code&gt;PendingIntent&lt;/code&gt;, &lt;code&gt;Uri&lt;/code&gt;, &lt;code&gt;LocalTime&lt;/code&gt;, &lt;code&gt;LocalDate&lt;/code&gt;, &lt;code&gt;LocalDateTime&lt;/code&gt;, &lt;code&gt;Instant&lt;/code&gt; — prefer &lt;code&gt;LocalDateTime&lt;/code&gt; or &lt;code&gt;Instant&lt;/code&gt; for date/time); an &lt;strong&gt;&lt;code&gt;@AppFunctionSerializable&lt;/code&gt; object&lt;/strong&gt;; or a &lt;strong&gt;&lt;code&gt;List&lt;/code&gt; of any supported non-primitive type&lt;/strong&gt;. Anything outside that set will not survive the schema round-trip.&lt;/p&gt;

&lt;h3&gt;
  
  
  Typed parameters and typed errors
&lt;/h3&gt;

&lt;p&gt;The second function takes parameters and reports failure the way the platform expects — by throwing, not by returning an error string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Marks or unmarks a single joke as a favorite, identified by its id.
 *
 * Required workflow: call [getFavorites] first when you need a valid joke id to act on.
 * This sets the favorite flag to an absolute value rather than toggling it, so the call is safe to
 * repeat — requesting `isFavorite = true` on a joke that is already a favorite leaves it a favorite.
 *
 * @param jokeId The unique identifier of the joke to update.
 * @param isFavorite The desired favorite state: `true` to mark as a favorite, `false` to unmark it.
 * @return A short human-readable status message describing the new state of the joke.
 * @throws AppFunctionElementNotFoundException If no joke exists with the given [jokeId]; suggest the
 * user call getFavorites or browse the jokes list to obtain a valid id.
 */&lt;/span&gt;
&lt;span class="nd"&gt;@AppFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDescribedByKDoc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;setFavorite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppFunctionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;jokeId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;isFavorite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dispatchers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;joke&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jokesRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getJokeById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jokeId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getOrNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;AppFunctionElementNotFoundException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No joke found with id $jokeId."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jokesRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFavorite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jokeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isFavorite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isFavorite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s"&gt;"Joke ${joke.id} is now a favorite."&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s"&gt;"Joke ${joke.id} is no longer a favorite."&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"Failed to update joke ${joke.id}: ${result.exceptionOrNull()?.message}"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things this second function demonstrates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typed parameters carry their meaning into the schema.&lt;/strong&gt; &lt;code&gt;jokeId: Int&lt;/code&gt; and &lt;code&gt;isFavorite: Boolean&lt;/code&gt; read clearly to a model deciding what to pass — exactly the point made earlier about parameter names being part of the contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors are thrown, not returned.&lt;/strong&gt; To report a real failure to the caller, throw a subclass of &lt;code&gt;androidx.appfunctions.AppFunctionException&lt;/code&gt; — here, &lt;code&gt;AppFunctionElementNotFoundException&lt;/code&gt; when the id does not exist. The agent receives a typed error rather than having to parse a sentence, and the &lt;code&gt;@throws&lt;/code&gt; line tells it what recovery to suggest. (The library ships a family of these: invalid-argument, element-not-found, permission-required, and more.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Required workflow:&lt;/code&gt; is a documented convention.&lt;/strong&gt; When one function depends on another, Google's KDoc guidance is to spell it out with that exact phrase — &lt;code&gt;Required workflow: call getFavorites first…&lt;/code&gt; — so the agent learns to call the read before the write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threading is explicit.&lt;/strong&gt; AppFunction implementations run on the UI thread by default, so these two functions switch to &lt;code&gt;withContext(Dispatchers.IO)&lt;/code&gt; themselves. (&lt;code&gt;clearFavorites&lt;/code&gt; got away without it because its single Room call already hops off the main thread internally — but making the switch explicit is the safer default, and the rule to teach.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these, the class now exposes three functions — &lt;code&gt;clearFavorites&lt;/code&gt;, &lt;code&gt;getFavorites&lt;/code&gt;, and &lt;code&gt;setFavorite&lt;/code&gt; — and the factory in the next step covers all of them at once, because they live on the same enclosing class.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6 — Tell the OS how to build your AppFunction class
&lt;/h2&gt;

&lt;p&gt;The OS, not your code, instantiates your AppFunctions class when an agent calls into it. So you need to declare a &lt;strong&gt;factory&lt;/strong&gt; for it. The hook is &lt;code&gt;AppFunctionConfiguration.Provider&lt;/code&gt;, implemented on your &lt;code&gt;Application&lt;/code&gt; subclass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;eu.anifantakis.networkapp&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.app.Application&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.appfunctions.service.AppFunctionConfiguration&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;eu.anifantakis.networkapp.jokes.di.AppModule&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;eu.anifantakis.networkapp.jokes.features.jokes.appfunctions.JokesAppFunctions&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyApplication&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;AppFunctionConfiguration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nc"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;applicationContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;appFunctionConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppFunctionConfiguration&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AppFunctionConfiguration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEnclosingClassFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JokesAppFunctions&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;JokesAppFunctions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;addEnclosingClassFactory&lt;/code&gt; takes the class that contains your AppFunctions and a lambda that knows how to build an instance. For multiple AppFunction classes you chain multiple &lt;code&gt;addEnclosingClassFactory&lt;/code&gt; calls before calling &lt;code&gt;build()&lt;/code&gt;. With Hilt, you would inject the class through the field and return the injected instance from the lambda — Google's official docs show exactly that pattern.&lt;/p&gt;

&lt;p&gt;Do not forget to register &lt;code&gt;MyApplication&lt;/code&gt; in your manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;application&lt;/span&gt;
    &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;".MyApplication"&lt;/span&gt;
    &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Where does this code live?
&lt;/h2&gt;

&lt;p&gt;By this point we have created several new types — &lt;code&gt;JokesAppFunctions&lt;/code&gt;, &lt;code&gt;AppFunctionJoke&lt;/code&gt;, a mapper — and the natural question for anyone with a layered codebase is: &lt;em&gt;which layer do they belong to?&lt;/em&gt; The sample uses the familiar &lt;code&gt;data&lt;/code&gt; / &lt;code&gt;domain&lt;/code&gt; / &lt;code&gt;presentation&lt;/code&gt; split per feature, so the temptation is to drop the AppFunctions code into one of those three. Resist it; the right answer comes straight from clean architecture once you classify what an AppFunction actually is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An AppFunction is an inbound entry point — a &lt;em&gt;driving&lt;/em&gt; (primary) adapter.&lt;/strong&gt; Something outside your app (the OS, an agent) reaches in and drives your business logic. That is the &lt;em&gt;exact same role your Compose UI plays&lt;/em&gt;; the only difference is the caller — a human taps a screen, an agent invokes a function. Both sit in the outer "interface adapters" ring and call &lt;em&gt;inward&lt;/em&gt; to the repository. Contrast this with your &lt;code&gt;data&lt;/code&gt; layer, which holds &lt;em&gt;driven&lt;/em&gt; (secondary) adapters — outbound gateways the app calls (Room, Ktor). AppFunctions point the opposite direction.&lt;/p&gt;

&lt;p&gt;That single classification settles the options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not &lt;code&gt;data&lt;/code&gt;.&lt;/strong&gt; The data layer is for outbound gateways, and &lt;code&gt;AppFunctionJoke&lt;/code&gt; is not a persistence or network DTO — it is the boundary model of an &lt;em&gt;inbound&lt;/em&gt; surface. Putting an entry point in &lt;code&gt;data&lt;/code&gt; conflates "things that drive us" with "things we drive."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not the root/app package.&lt;/strong&gt; Root is for genuinely cross-feature, app-wide glue. &lt;code&gt;clearFavorites&lt;/code&gt;, &lt;code&gt;getFavorites&lt;/code&gt;, and &lt;code&gt;setFavorite&lt;/code&gt; are &lt;em&gt;about jokes&lt;/em&gt;, so they belong to the jokes feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not &lt;code&gt;core&lt;/code&gt;.&lt;/strong&gt; A shared, feature-agnostic module must not depend on the jokes domain; feature logic there would invert the dependency rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;data&lt;/code&gt; / &lt;code&gt;domain&lt;/code&gt; / &lt;code&gt;presentation&lt;/code&gt; triple is the common shape, not a hard rule. Clean architecture really has the &lt;strong&gt;domain at the center&lt;/strong&gt; surrounded by an outer ring of interface adapters that holds &lt;em&gt;all&lt;/em&gt; delivery mechanisms — UI, widgets, tiles, deep links, and AppFunctions alike. Presentation and AppFunctions are &lt;strong&gt;siblings&lt;/strong&gt; in that ring, not parent and child. So the cleanest home is a fourth package beside &lt;code&gt;presentation&lt;/code&gt;, scoped to the feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;features/jokes/
├─ data/          ← driven adapters: Room, Ktor, DTOs, mappers
├─ domain/        ← Joke, JokesRepository (no Android, no androidx.appfunctions)
├─ presentation/  ← Compose screens + ViewModels   (delivery to a human)
└─ appfunctions/  ← JokesAppFunctions, AppFunctionJoke, mapper  (delivery to an agent)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mental model worth keeping: &lt;strong&gt;&lt;code&gt;appfunctions/&lt;/code&gt; is to an agent what &lt;code&gt;presentation/&lt;/code&gt; is to a human&lt;/strong&gt; — same architectural rank, different audience. And &lt;code&gt;AppFunctionJoke&lt;/code&gt; belongs &lt;em&gt;beside its adapter&lt;/em&gt; for the same reason a UI model lives in &lt;code&gt;presentation&lt;/code&gt; and a &lt;code&gt;JokeDto&lt;/code&gt; lives in &lt;code&gt;data&lt;/code&gt;: a boundary model lives with its boundary, and it depends only inward on the domain &lt;code&gt;Joke&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One more distinction makes the split clean. There are two kinds of AppFunctions code, and they live in two different places:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Where it lives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Feature-specific&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;JokesAppFunctions&lt;/code&gt;, &lt;code&gt;AppFunctionJoke&lt;/code&gt;, the mapper&lt;/td&gt;
&lt;td&gt;inside the feature, &lt;code&gt;features/jokes/appfunctions/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;App-wide wiring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;AppFunctionConfiguration.Provider&lt;/code&gt; on &lt;code&gt;MyApplication&lt;/code&gt;, &lt;code&gt;app_metadata.xml&lt;/code&gt;, the manifest &lt;code&gt;&amp;lt;service&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the app/root level&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is not an accident — the wiring is app-wide precisely because it &lt;em&gt;aggregates every feature's&lt;/em&gt; AppFunctions into one schema (recall the &lt;code&gt;aggregateAppFunctions&lt;/code&gt; flag from Step 1). Each feature contributes its own functions; the app module assembles them. This also future-proofs a multi-module split: &lt;code&gt;:jokes:appfunctions&lt;/code&gt; would depend on &lt;code&gt;:jokes:domain&lt;/code&gt;, while the aggregation flag and the &lt;code&gt;Provider&lt;/code&gt; stay in &lt;code&gt;:app&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7 — Test it (adb, a real agent, or both)
&lt;/h2&gt;

&lt;p&gt;This is where most readers will pause and ask the right question: &lt;em&gt;I have built and registered an AppFunction, so how do I actually see an agent call it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are three answers, each useful for a different reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route A — adb (the developer shortcut)
&lt;/h3&gt;

&lt;p&gt;The fastest way to verify your wiring is through &lt;code&gt;adb shell cmd app_function&lt;/code&gt;. It is worth being precise about what this command does, because it is not a fake or a mock — it is a thin shell front-end to the same OS-level &lt;code&gt;AppFunctionService&lt;/code&gt; that any agent would talk to through &lt;code&gt;AppFunctionManager&lt;/code&gt; from Kotlin code. You are not simulating an agent here; you are bypassing it and speaking to the OS directly. That makes adb the ideal tool for end-to-end verification, because it removes every AI-product variable and leaves only your own wiring under test.&lt;/p&gt;

&lt;p&gt;Build and install the app on a device or emulator running Android 16 or higher, then list the registered AppFunctions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Type this for the first 10 lines of app functions for our package&lt;/span&gt;
adb shell cmd app_function list-app-functions | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 10 &lt;span class="s2"&gt;"eu.anifantakis.networkapp.jokes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything compiled and the manifest is set up correctly, you should see something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="s2"&gt;"eu.anifantakis.networkapp.jokes.features.jokes.appfunctions.JokesAppFunctions#clearFavorites"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"packageNameHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="mi"&gt;-1179891122&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"mobileApplicationQualifiedId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"android$apps-db/apps#eu.anifantakis.networkapp"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;--&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"android$apps-db/app_functions#eu.anifantakis.networkapp/eu.anifantakis.networkapp.jokes.features.jokes.appfunctions.JokesAppFunctions#clearFavorites"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"functionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"eu.anifantakis.networkapp.jokes.features.jokes.appfunctions.JokesAppFunctions#clearFavorites"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"packageName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"eu.anifantakis.networkapp"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"com.google.android.permissioncontroller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth pointing out in this output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;functionId&lt;/code&gt;&lt;/strong&gt; is the canonical identifier of our AppFunction: &lt;code&gt;eu.anifantakis.networkapp.jokes.features.jokes.appfunctions.JokesAppFunctions#clearFavorites&lt;/code&gt;. Note the &lt;code&gt;#&lt;/code&gt; separator between the fully-qualified class name and the method name. This is exactly the string you pass to &lt;code&gt;--function&lt;/code&gt; in the next command, so copy it from this output rather than typing it from memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;packageName&lt;/code&gt;&lt;/strong&gt; is &lt;code&gt;eu.anifantakis.networkapp&lt;/code&gt; — the app's &lt;code&gt;applicationId&lt;/code&gt;. This is what you pass to &lt;code&gt;--package&lt;/code&gt;, and it is &lt;em&gt;not&lt;/em&gt; the same as the package containing the function class. (The function class lives at &lt;code&gt;eu.anifantakis.networkapp.jokes.features.jokes.appfunctions&lt;/code&gt;, but the application identifier is just &lt;code&gt;eu.anifantakis.networkapp&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mobileApplicationQualifiedId&lt;/code&gt;&lt;/strong&gt; points at &lt;code&gt;android$apps-db/apps#eu.anifantakis.networkapp&lt;/code&gt;. That &lt;code&gt;apps-db&lt;/code&gt; prefix is the OS-level database — AppSearch, internally — that the system uses to index installed apps. Our function has its own entry under &lt;code&gt;app_functions&lt;/code&gt; within that same database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;scope: global&lt;/code&gt;&lt;/strong&gt; confirms the function is visible without further gating. If we had used &lt;code&gt;@AppFunction(isEnabled = false, ...)&lt;/code&gt; and not yet enabled it at runtime, this entry would not show up here at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point worth taking away is that this output is not a diagnostic surface dressed up to look pretty for developers. It is a literal view into the OS's AppFunctions registry — the same registry an agent reads when it asks "what functions does this app expose?". Everything an agent will see about your function is in here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it end-to-end
&lt;/h3&gt;

&lt;p&gt;To actually see the function in action, run through this short demo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the app and mark some jokes as favorites by tapping the heart icon next to the ones you want to keep.&lt;/li&gt;
&lt;li&gt;Close and reopen the app. You should notice that while the rest of the jokes are refreshed from the network, the favorites you marked are preserved.&lt;/li&gt;
&lt;li&gt;Run the script below and watch the favorite marks instantly clear from your screen. Note, you do not need to have your app running for this to take effect.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell cmd app_function execute-app-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--package&lt;/span&gt; eu.anifantakis.networkapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function&lt;/span&gt; eu.anifantakis.networkapp.jokes.features.jokes.appfunctions.JokesAppFunctions#clearFavorites &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your favorites table had rows marked &lt;code&gt;isFavorite = 1&lt;/code&gt; before, they are all reset to &lt;code&gt;0&lt;/code&gt; now, and adb prints the string the function returned.&lt;/p&gt;

&lt;p&gt;The two functions from Step 5b are reachable the same way. &lt;code&gt;getFavorites&lt;/code&gt; takes no parameters and returns the structured list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell cmd app_function execute-app-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--package&lt;/span&gt; eu.anifantakis.networkapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function&lt;/span&gt; eu.anifantakis.networkapp.jokes.features.jokes.appfunctions.JokesAppFunctions#getFavorites &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;setFavorite&lt;/code&gt; takes the typed parameters as JSON — note how the keys match the Kotlin parameter names exactly. There is one sharp edge here that is pure shell, not AppFunctions: a JSON payload with quotes and spaces has to survive &lt;strong&gt;two&lt;/strong&gt; shells — your local shell &lt;em&gt;and&lt;/em&gt; the shell &lt;code&gt;adb&lt;/code&gt; spawns on the device. The naive form fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# DON'T: the device-side shell strips the quotes and splits on the space, so `cmd`&lt;/span&gt;
&lt;span class="c"&gt;# receives only `{jokeId:` →  JSONException: End of input at character 8 of {jokeId:&lt;/span&gt;
adb shell cmd app_function execute-app-function &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="s1"&gt;'{"jokeId": 179, "isFavorite": true}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to hand &lt;code&gt;adb shell&lt;/code&gt; a single double-quoted command and single-quote the JSON inside it (escaping the inner double quotes). This is why the no-argument calls above worked with a bare &lt;code&gt;'{}'&lt;/code&gt; — there were no quotes or spaces to mangle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell &lt;span class="s2"&gt;"cmd app_function execute-app-function &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  --package eu.anifantakis.networkapp &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  --function eu.anifantakis.networkapp.jokes.features.jokes.appfunctions.JokesAppFunctions#setFavorite &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  --parameters '{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;jokeId&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:179,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;isFavorite&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:true}'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns &lt;code&gt;"Joke 179 is now a favorite."&lt;/code&gt;, and a follow-up &lt;code&gt;getFavorites&lt;/code&gt; then includes joke 179 in its structured list. Pass a &lt;code&gt;jokeId&lt;/code&gt; that does not exist and you can watch the typed-error path fire instead — &lt;code&gt;Error executing app function: android.app.appfunctions.AppFunctionException: No joke found with id 999999. (code 1500)&lt;/code&gt; — the caller gets a structured error, not a status sentence.&lt;/p&gt;

&lt;p&gt;A few more &lt;code&gt;adb&lt;/code&gt; subcommands worth keeping in your back pocket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Confirm the device even supports AppFunctions (prints a help page if so).&lt;/span&gt;
&lt;span class="c"&gt;# If you instead see "cmd: Can't find service: app_function", the device doesn't support the feature.&lt;/span&gt;
adb shell cmd app_function &lt;span class="nb"&gt;help&lt;/span&gt;

&lt;span class="c"&gt;# Append --brief-yaml to any execute call for terser, more readable output.&lt;/span&gt;
adb shell cmd app_function execute-app-function &lt;span class="nt"&gt;--brief-yaml&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--package&lt;/span&gt; eu.anifantakis.networkapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function&lt;/span&gt; eu.anifantakis.networkapp.jokes.features.jokes.appfunctions.JokesAppFunctions#getFavorites &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;

&lt;span class="c"&gt;# Toggle a single function on or off at runtime (ties into the @AppFunction(isEnabled = false) flag).&lt;/span&gt;
adb shell cmd app_function set-enabled &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--package&lt;/span&gt; eu.anifantakis.networkapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function&lt;/span&gt; eu.anifantakis.networkapp.jokes.features.jokes.appfunctions.JokesAppFunctions#setFavorite &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--state&lt;/span&gt; disable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important observation&lt;/strong&gt; (also just mentioned above): this works whether your app is in the foreground, in the background, or completely closed. The OS reaches your function through the &lt;code&gt;AppFunctionService&lt;/code&gt; independently of your activity lifecycle. That is the practical cash-value of &lt;em&gt;"indexed by the OS"&lt;/em&gt; from the start of this article — agents do not need your UI to be alive in order to reach your code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other paths (I have not tried yet)
&lt;/h3&gt;

&lt;p&gt;adb is the one route I have actually used to verify this hello world. There are two other ways an external caller can reach an AppFunction, and both are worth knowing exist, even if I cannot speak to them yet from first-hand experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route B — A custom host app that uses &lt;code&gt;AppFunctionManager&lt;/code&gt;.&lt;/strong&gt; This is the closest you can get to the production shape: a second app on the device whose job is to discover and invoke functions exposed by another app over IPC, with no &lt;code&gt;adb&lt;/code&gt; in the middle. It is also the pattern Google themselves demonstrated at I/O when AppFunctions was first shown publicly — because there was not yet a real agent to demo, they built a small host app for the purpose. The catch is that &lt;code&gt;EXECUTE_APP_FUNCTIONS&lt;/code&gt; is currently a privileged permission on Android 16 release builds, so a host app cannot be installed and granted that permission via a normal &lt;code&gt;adb install&lt;/code&gt;; you need a userdebug build, a rooted device, or a &lt;code&gt;/system/priv-app&lt;/code&gt; install. I have not built this end-to-end myself yet, so I am not going to pretend to walk through it here — but a follow-up article is on schedule that will showcase a host app calling into the jokes AppFunction we wrote in this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route C — Gemini in Android Studio as an LLM-in-the-loop check.&lt;/strong&gt; Google's docs suggest using Gemini in Android Studio with a prompt that asks it to &lt;em&gt;"Execute &lt;code&gt;adb shell cmd app_function&lt;/code&gt; to learn how the tool works, then act as a chat agent..."&lt;/em&gt;. In effect this has an LLM drive adb for you, which tests whether your function descriptions are clear enough for a model to pick the right one. I have not personally tried this either, so I cannot say more than what the docs document.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about Gemini on a real phone?
&lt;/h3&gt;

&lt;p&gt;As of this writing, full Gemini integration with AppFunctions is in a private preview with trusted testers. You cannot just install your app, open Gemini on your phone, and have it call into your functions yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  A note on destructive functions
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;clearFavorites&lt;/code&gt; function in this article happens to be safe to repeat by accident. &lt;code&gt;UPDATE SET isFavorite = 0&lt;/code&gt; produces the same result whether called once or ten times. That accident is worth pausing on, because the moment you write a destructive AppFunction where repeating the call would cause harm, you have reopened every problem REST solved for deterministic callers, with one new wrinkle: the caller is now an LLM that can hallucinate, retry, or pick your function for the wrong reasons.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;deleteJoke(id)&lt;/code&gt; called twice is a bug at best. A &lt;code&gt;sendPayment(amount)&lt;/code&gt; called twice is a real problem. Before you annotate a write with &lt;code&gt;@AppFunction&lt;/code&gt;, treat the same questions REST forces you to answer: what happens under retry, what happens under partial failure, and who is allowed to call this in the first place?&lt;/p&gt;

&lt;p&gt;A few patterns worth carrying over from the REST era.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Make writes safe to repeat by design where you can
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Prefer absolute set operations to deltas.&lt;/li&gt;
&lt;li&gt;Accept a client-supplied request ID so a duplicate call can be detected and ignored.&lt;/li&gt;
&lt;li&gt;Return success identically for a no-op and a real change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In more detail:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer absolute set operations over deltas.&lt;/strong&gt; An &lt;code&gt;UPDATE&lt;/code&gt; that writes a final value, like &lt;code&gt;SET status = 'active'&lt;/code&gt;, gives the same result every time it runs. A delta like &lt;code&gt;SET value = value + 1&lt;/code&gt; accumulates with every retry, so two calls leave the counter at +2 even though the caller only meant +1. For AppFunctions, &lt;code&gt;markAsFavorite(jokeId)&lt;/code&gt; is absolute and safe to repeat; &lt;code&gt;incrementFavoriteCount(jokeId)&lt;/code&gt; is a delta and is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accept a client-supplied request ID.&lt;/strong&gt; Let the caller send a unique key with every call. Your function keeps a small record of recently-seen keys; if the same key arrives twice, you return the cached result of the first call instead of running the operation again. This is exactly how payment APIs like Stripe survive network retries without double-charging — they require an &lt;code&gt;Idempotency-Key&lt;/code&gt; header on every request, and you can borrow the same pattern for any sensitive write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Return success the same way, whether the operation did real work or was a no-op.&lt;/strong&gt; Our &lt;code&gt;clearFavorites&lt;/code&gt; already does this. Both branches return "All favorite jokes have been cleared successfully," regardless of whether ten jokes were unfavorited or zero. Different messages ("Cleared 10" vs "Nothing to clear") would leak state to the caller and tempt the LLM to pick a different next step depending on the answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Expose reads liberally, expose writes conservatively
&lt;/h3&gt;

&lt;p&gt;There is no rule that says every method on your repository deserves an &lt;code&gt;@AppFunction&lt;/code&gt;. Pick the smallest surface that is genuinely useful to an agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Assume nothing is guarding the door
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;EXECUTE_APP_FUNCTIONS&lt;/code&gt; is privileged today, but the enforcement story between agent and function is still being defined. The follow-up host-app article will dig into that layer in detail. Until then, your function is the last layer of defence, not the first.&lt;/p&gt;

&lt;p&gt;There is a strategic point underneath all of this that is worth saying out loud. We spent a decade optimising deep links, App Indexing, and search to get users &lt;em&gt;into&lt;/em&gt; the app. AppFunctions optimises for the opposite — the app never opening, the screen never lighting up, no UI between the agent and your business logic. That changes what "least privilege" looks like in practice: the UI is no longer there to ask "are you sure?" on your behalf.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;The line of code I keep coming back to is this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@AppFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDescribedByKDoc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single flag turns documentation into runtime behavior. The KDoc you write is the contract an LLM reads to decide whether to call your function and what to pass into it. Vague KDoc means a confused agent. Precise KDoc, with the same care you would give to a public API description, means the agent picks your function for the right reasons.&lt;/p&gt;

&lt;p&gt;That, more than any specific annotation or manifest entry, is the shift AppFunctions is asking us to make.&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sample repository (branch): &lt;a href="https://github.com/ioannisa/MitropolitikoNetworkApp/tree/07-App-Functions" rel="noopener noreferrer"&gt;MitropolitikoNetworkApp / 07-App-Functions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/ai/appfunctions" rel="noopener noreferrer"&gt;AppFunctions overview — Android Developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/ai/appfunctions/add-appfunctions" rel="noopener noreferrer"&gt;Add the AppFunctions API to your app — Android Developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/jetpack/androidx/releases/appfunctions" rel="noopener noreferrer"&gt;androidx.appfunctions Jetpack release notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>ai</category>
      <category>agents</category>
      <category>androiddev</category>
    </item>
  </channel>
</rss>
