Skip to content

About

Expose React Native app actions to AI agents through Android AppFunctions and iOS App Intents, declared once in TypeScript.

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

22 Commits

Folders and files

Repository files navigation

react-native-agent-actions

한국어

Let AI agents call into your React Native app. You declare actions once in TypeScript, implement them in JavaScript, and the library exposes them through:

  • Android AppFunctions, which Gemini and other on-device agents use to call app functions, and
  • iOS App Intents, which Siri, Shortcuts and Spotlight use.
One config generates an Android AppFunctions service, iOS App Intents and TypeScript types. Agents, Siri and Shortcuts call the app, and a native dispatcher hands each call to your JavaScript handler, starting the app in the background when needed.
iOS: Shortcuts runs actions while the app is not running Android: an agent calls actions while the app is not running
The Shortcuts app runs Summarize notes while the example app is not running and shows the summary from the JavaScript handler. Opening the app shows that both calls ran in the background. Three app function calls run while the example app is not running. Opening the app shows them in its call log, and two more calls arrive while it is open.

Recorded with the example app on an iOS simulator and an Android 17 emulator. On Android the calls come from adb shell cmd app_function execute-app-function, which goes through the same system API that agents use. MP4 versions: iOS, Android.

Warning

Early release. Google's AppFunctions library is still alpha (androidx.appfunctions 1.0.0-alpha13) and changes between versions, and calling app functions from Gemini is limited to an early access program. Expect breaking changes.

Caution

Any caller the OS allows can run the actions you declare, including when the app is in the background and no user is looking at it. Expose actions that are safe to run without confirmation, and validate every parameter in your handler.

Requirements

Area Requirement
React Native 0.87 or later, New Architecture. AppFunctions needs compileSdk 37 and Android Gradle Plugin 9.1, which React Native 0.87 provides.
Native modules react-native-nitro-modules 0.37 or later
Android The app builds for any minSdk. Agents can see the actions on Android 16 (API 36) and later.
iOS The app builds for any deployment target React Native supports. The actions exist on iOS 16 and later.
Node 22.18 or later to load a .ts config. Older versions can use .mjs or .json.

Install

npm install react-native-agent-actions react-native-nitro-modules

1. Declare actions

Create agent-actions.config.ts next to your app's package.json:

import type { AgentActionsConfig } from 'react-native-agent-actions';

export default {
  actions: {
    searchNotes: {
      title: 'Search notes',
      description: 'Finds notes whose title or tags contain the query.',
      params: {
        query: { type: 'string', description: 'Text to look for.' },
        limit: {
          type: 'integer',
          optional: true,
          description: 'Maximum number of notes to return. Defaults to 5.',
        },
      },
      result: {
        type: 'array',
        description: 'Matching notes, best match first.',
        items: {
          type: 'object',
          name: 'Note',
          properties: {
            id: { type: 'string', description: 'Stable identifier of the note.' },
            title: { type: 'string', description: 'Title of the note.' },
            pinned: { type: 'boolean', description: 'Whether the note is pinned.' },
          },
        },
      },
      ios: {
        phrases: ['Search notes in ${applicationName}'],
        systemImageName: 'magnifyingglass',
      },
    },
  },
} satisfies AgentActionsConfig;

Agents decide when to call an action from its description and the descriptions of its parameters, so write them for a reader who has never seen your app.

2. Generate the native code

npx react-native-agent-actions generate
cd ios && pod install

Run it again whenever you change the config. It writes:

File Purpose
android/app/src/main/java/dev/agentactions/generated/AgentActions.generated.kt AppFunctions entry point service with one function per action
ios/<App>/AgentActions.generated.swift One App Intent per action, plus App Shortcuts for actions with phrases
ios/<App>/<locale>.lproj/AppShortcuts.strings Phrases in other languages, one file per locale in localizedPhrases
src/agentActions.generated.ts Types for your handlers

The first run also wires the generated code into your project: it adds the service to AndroidManifest.xml, the KSP Gradle plugin to android/build.gradle, apply from: .../agent-actions.gradle to android/app/build.gradle, the Swift file to the Xcode target, and the AgentActionsCore pod to the Podfile. Later runs keep the localized phrase files in the Xcode target and the Siri app names in Info.plist in sync with the config. Commit the generated files.

Use npx react-native-agent-actions generate --check in CI to fail when the generated files are out of date.

3. Handle the actions in JavaScript

Register the handlers once, at module scope in index.js. The OS can start your app in the background to run an action, and then no screen is rendered, so handlers registered inside a component would never be reached.

// index.js
import { AppRegistry } from 'react-native';
import { registerAgentActions } from 'react-native-agent-actions';
import { handlers } from './src/agentActionHandlers';
import App from './src/App';

registerAgentActions(handlers);

AppRegistry.registerComponent('MyApp', () => App);
// src/agentActionHandlers.ts
import type { AgentActionHandlers } from './agentActions.generated';

export const handlers: AgentActionHandlers = {
  searchNotes: async ({ query, limit = 5 }) => {
    const notes = await notesStore.search(query);
    return notes.slice(0, limit);
  },
};

Typing the object as AgentActionHandlers makes TypeScript require a handler for every action and check the parameters and results. If a handler throws or rejects, the agent receives the error message.

A handler can also return reply(value, { dialog }) to give Siri a sentence to say. See Siri.

Siri

Actions with ios.phrases become App Shortcuts, so people can run them by talking to Siri without setting anything up. Three options decide whether that works well outside your own language and device.

Phrases in other languages. Siri only matches phrases in its own language. Add localizedPhrases with one phrase per entry in phrases, in the same order:

ios: {
  phrases: ['Search notes in ${applicationName}'],
  localizedPhrases: {
    de: ['Notizen in ${applicationName} suchen'],
    es: ['Buscar notas en ${applicationName}'],
  },
},

The generator writes <locale>.lproj/AppShortcuts.strings for each locale and adds the files to the Xcode target.

Names people can say. Siri finds the app by its display name. When the name is hard to say, or written in a different script from Siri's language, add the names people would actually use:

export default {
  actions: { /* ... */ },
  ios: {
    appNames: ['Notes Pro', { name: 'NP', pronunciationHint: 'en pee' }],
  },
} satisfies AgentActionsConfig;

The generator writes them to INAlternativeAppNames in Info.plist, and Siri accepts them wherever a phrase says ${applicationName}.

What Siri says. When a handler returns a string, Siri shows and speaks it. For any other result, return reply(). The caller still receives value, and Siri says dialog:

import { reply } from 'react-native-agent-actions';

export const handlers: AgentActionHandlers = {
  getStats: async () => {
    const stats = await loadStats();
    return reply(stats, { dialog: `You have ${stats.noteCount} notes.` });
  },
};

Siri says the dialog exactly as written, so choose its language in the handler, for example from the device locale. Android ignores the dialog.

Things to know when you test on a device:

  • The first time someone uses one of the app's phrases, Siri asks whether to turn on the app's shortcuts.
  • Avoid words that also name a built-in app. On a Korean device, "메모" (memo) made Siri offer Apple Notes, so the example app says "노트" instead.
  • Siri on the Simulator cannot match App Shortcut phrases and answers that the app is not on the device.

Schema

Parameters can be string, number, integer, boolean, or array of string. Mark them optional: true when the agent may leave them out.

Results can be any of those types, object (with a PascalCase name and properties), or array of any of them except arrays. Object properties can be optional: true, which means null or missing. Leave result out for actions that return nothing.

Config Android (Kotlin) iOS (App Intents) TypeScript
string String String string
number Double Double number
integer Long Int number
boolean Boolean Bool boolean
array of string List<String> [String] string[]
array of number / integer / boolean DoubleArray / LongArray / BooleanArray [Double] / [Int] / [Bool] number[] / boolean[]
object, array of object @AppFunctionSerializable data classes JSON string interfaces

Other options per action:

Option Default Meaning
timeoutMs 10000 How long the native side waits for your handler. iOS caps it at 25000 because the system stops App Intents after 30 seconds.
platforms ['android', 'ios'] Limit an action to one platform.
ios.phrases none App Shortcut phrases for Siri and Spotlight, in the app's development language. Each must contain ${applicationName}. At most 10 actions can have phrases.
ios.localizedPhrases none The same phrases in other languages, keyed by locale (ko, ja, pt-BR). Each list has one phrase per entry in phrases, in the same order.
ios.systemImageName sparkles SF Symbol for the App Shortcut.

Options for the whole app, next to actions:

Option Default Meaning
ios.appNames none Other names Siri accepts for the app, written to INAlternativeAppNames. Strings, or { name, pronunciationHint }.
ios.infoPlist Info.plist in the app folder Where to write appNames.
ios.sourceDir, ios.xcodeproj, ios.target, ios.podfile found in ios/ Set them when the app has more than one Xcode project or target.
android.sourceDir, android.manifestPath android/app/src/main/... Where the Kotlin file and the manifest are.
typescript.outputFile src/agentActions.generated.ts Where the handler types are written.

How calls reach JavaScript

The native side keeps a queue of calls. When a call arrives before JavaScript has registered its handlers, it waits in the queue:

  • Android: if the app process was started for the call, the library starts React Native without an Activity, the bundle runs registerAgentActions, and the queued call is delivered. Each call runs as a Headless JS task, because React Native pauses JavaScript timers in the background unless a headless task is active. Your Application must implement ReactApplication, which the React Native template does.
  • iOS: the system launches the app in the background. React Native starts from AppDelegate as usual, and the queued call is handled once the bundle has loaded.

Measured on an emulator and a simulator with release builds, a call to an app that was not running returned in about 0.1 s on Android and 0.5 s on iOS. If the handler does not answer within timeoutMs, the caller gets an error. Errors thrown by a handler reach the caller with their message.

Testing

iOS: actions with phrases show up in the Shortcuts app under your app's name, and every action also appears as an action you can add to a shortcut. Running them from the Shortcuts app works on the Simulator. Test Siri phrases on a real device, as described in Siri.

Android: on an Android 16 or later device or emulator, list the indexed functions and call one with adb. Function ids have the form dev.agentactions.generated.AgentActionsServiceBase#<action>.

adb shell cmd app_function list-app-functions --package your.app.id

adb shell cmd app_function execute-app-function \
  --package your.app.id \
  --function 'dev.agentactions.generated.AgentActionsServiceBase#searchNotes' \
  --parameters "'{\"query\":\"trip\"}'"

Run adb shell am force-stop your.app.id first to test a call that has to start the app.

Limitations

  • Android: action names must not clash with methods of android.app.Service, because the generated functions live in an AppFunctionService subclass.
  • iOS: an app can have only one AppShortcutsProvider. If your app already has one, do not use phrases; the intents still work in Shortcuts.
  • iOS returns objects as a JSON string, because App Intents can only return a fixed set of value types.
  • Enums, maps, dates and nested arrays are not supported yet.
  • Expo config plugin: not yet. In a prebuild project, run the generator after expo prebuild.

Example app

example/ declares a few actions in agent-actions.config.ts and implements them in src/agentActionHandlers.ts.

yarn
yarn generate
yarn example ios     # or: yarn example android

License

MIT

About

Expose React Native app actions to AI agents through Android AppFunctions and iOS App Intents, declared once in TypeScript.

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages