---
title: How to Use Apple Intelligence in a Capacitor App
description: Use Apple Intelligence in a Capacitor app with the Capacitor LLM plugin — on-device text generation, chat sessions, streaming and limits on iOS 26.
date:
  created: 2026-09-07
  updated: 2026-09-07
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
  - SDKs
links:
  - Capacitor LLM: sdks/capacitor/llm.md
faq: true
---

# How to Use Apple Intelligence in a Capacitor App

Apple Intelligence gives every eligible iPhone an on-device language model, and since iOS 26 third-party apps can use it through Apple's Foundation Models framework. This guide shows how to use Apple Intelligence in a Capacitor app with the [Capacitor LLM plugin](../../sdks/capacitor/llm.md): check whether the model is available, run chat sessions with instructions, stream tokens into your UI, cancel a generation, and stay within the context limit. Prompts and responses never leave the phone, and there is no model to bundle and no API key to manage.

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Build and deploy your Capacitor app with Capawesome Cloud" src="https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1" />
  </a>
</div>

## Key Takeaways

- Apple Intelligence text generation in a Capacitor app requires iOS 26 or later, an Apple Intelligence-enabled device (iPhone 15 Pro or later), and Apple Intelligence turned on in Settings.
- Building the app requires Xcode 26 or later and Capacitor 8.
- The Capacitor LLM plugin wraps Apple's Foundation Models framework in one TypeScript API: `getAvailability()`, `createChat(...)`, `generateText(...)`, `streamText(...)`, and `cancelGeneration(...)`.
- All inference runs on the device. The plugin bundles no model files, needs no API keys, and makes no network calls.
- Each chat has a context window of about 4,096 tokens. A `GENERATION_FAILED` rejection usually means the chat is full and you should start a new one.
- The same code runs Gemini Nano on Android. Only the availability states and the parameter limits differ.

## What Apple Intelligence Offers Developers

Apple opened its on-device language model to third-party apps with the [Foundation Models framework](https://developer.apple.com/documentation/foundationmodels){:target="_blank"} in iOS 26, iPadOS 26, macOS 26, and visionOS 26. The model is the same one that powers Writing Tools and other system features. It runs on the Neural Engine, costs nothing per request, and works in Airplane Mode.

The [Capacitor LLM plugin](../../sdks/capacitor/llm.md) exposes that framework to your web code. It is part of [Capawesome Insiders](../../insiders/index.md), a paid subscription, and built only on public platform APIs, so it passes App Review and survives OS updates. What you get from JavaScript:

- **Chat sessions**: a chat keeps the conversation context across generations and can carry instructions, the equivalent of a system prompt.
- **Token streaming**: responses arrive chunk by chunk through an event, so the UI updates while the model writes.
- **Cancellation**: stop an in-flight generation, for example when the user leaves the screen.
- **Typed availability**: a status value tells you whether the model can be used right now and why not.
- **Tunable parameters**: temperature and maximum output length per chat or per request.

Apple's framework also supports guided generation into typed structures and tool calling. The plugin does not expose those yet, so plan for free-text responses.

Typical uses are the ones where sending user data to a server is a problem or a cost: summarizing notes and messages, suggesting replies, rewriting or proofreading text, and offline assistants. The plugin pairs with the [Capacitor Speech Recognition plugin](../../sdks/capacitor/speech-recognition.md) for voice prompts and the [Capacitor Speech Synthesis plugin](../../sdks/capacitor/speech-synthesis.md) for spoken answers.

## Which iPhones Support Apple Intelligence for Developers?

The Foundation Models framework is available on every device that can run Apple Intelligence, provided it runs iOS 26 or a sibling OS from 2025. Apple lists the eligible hardware on its [Apple Intelligence support page](https://support.apple.com/en-us/121115){:target="_blank"}:

| Device family | Eligible models |
|---|---|
| iPhone | iPhone 15 Pro and 15 Pro Max, all iPhone 16 models and later |
| iPad | iPad mini (A17 Pro), iPad models with M1 and later |
| Mac | Mac computers with Apple silicon |
| Apple Vision Pro | All models |

Hardware alone is not enough. The model also needs:

| Requirement | Detail |
|---|---|
| OS version | iOS 26, iPadOS 26, macOS 26, or visionOS 26 for the framework. Apple Intelligence itself shipped with iOS 18.1, but the developer API did not. |
| Apple Intelligence turned on | The user enables it under Settings > Apple Intelligence & Siri. Until then the plugin reports `not-enabled`. |
| Storage | About 7 GB of free space for the model download, which the system manages. |
| Language and region | The device language must be one Apple Intelligence supports. Apple lists the current languages and regions on the same support page. |
| Build toolchain | Xcode 26 or later to compile the plugin. |

On a device that runs iOS 18, every plugin method except `getAvailability()` rejects as unavailable. On an iPhone 15 or older, the status is `device-not-eligible`. Both cases are normal, and the next section shows how to handle them.

## Set Up Apple Intelligence in Your Capacitor App

Before you write any code, confirm three things about your project:

- **Capacitor 8**: the plugin supports Capacitor 8 and later. If you are still on Capacitor 7, follow [How to Upgrade Your Capacitor App to Capacitor 8](./how-to-upgrade-your-capacitor-app-to-capacitor-8.md) first.
- **Xcode 26**: required to build against the Foundation Models framework. Since April 28, 2026, Apple also rejects App Store submissions built with older versions, as covered in [Apple's Xcode 26 Requirement for Capacitor Apps](./xcode-26-requirement-for-capacitor-apps.md).
- **A test device**: an iPhone from the table above with iOS 26 and Apple Intelligence turned on. The Simulator is discussed in the FAQ.

You also need a Capawesome Insiders license key to install the plugin from the Capawesome npm registry. To install the Capacitor LLM plugin, please refer to the [Installation](../../sdks/capacitor/llm.md/#installation) section in the plugin documentation. On iOS, no further configuration is required, because the framework ships with the operating system.

## Step 1: Check Whether the Model Is Available

Call [`getAvailability()`](../../sdks/capacitor/llm.md#getavailability) before you show any AI feature. The method never rejects. On iOS it resolves with one of five statuses:

| Status | Meaning on iOS | What your app should do |
|---|---|---|
| `available` | The model is ready. | Enable the feature. |
| `device-not-eligible` | The hardware cannot run Apple Intelligence. | Hide the feature or use a server-side fallback. |
| `not-enabled` | Apple Intelligence is turned off in Settings. | Explain how to enable it, then check again. |
| `not-ready` | The system is still preparing or downloading the model. | Show a waiting state and listen for changes. |
| `unavailable` | No system model on this OS version, for example iOS 18. | Hide the feature or use a fallback. |

A small helper turns the status into a decision your UI can use:

```ts
import { Llm } from '@capawesome-team/capacitor-llm';

const checkAppleIntelligence = async () => {
  const { status } = await Llm.getAvailability();
  switch (status) {
    case 'available':
      return { enabled: true };
    case 'not-enabled':
      return { enabled: false, hint: 'Turn on Apple Intelligence in Settings to use this feature.' };
    case 'not-ready':
      return { enabled: false, hint: 'The on-device model is still being prepared. Try again in a moment.' };
    default:
      return { enabled: false };
  }
};
```

The status can change while your app is open, for example when the user enables Apple Intelligence and comes back. Attach a listener for the [`availabilityChange`](../../sdks/capacitor/llm.md#addlisteneravailabilitychange-) event and re-run your check when it fires:

```ts
import { Llm } from '@capawesome-team/capacitor-llm';

const watchAvailability = async (onChange: (status: string) => void) => {
  return Llm.addListener('availabilityChange', (event) => {
    onChange(event.status);
  });
};
```

The plugin only watches the system status while at least one listener is attached, so remove the listener when the screen that needs it goes away.

## Step 2: Create a Chat with Instructions

Every generation belongs to a chat. On iOS, each chat is backed by a native language model session that keeps the conversation context, so a follow-up prompt can refer to the previous answer. Create one with [`createChat(...)`](../../sdks/capacitor/llm.md#createchat) and pass instructions that define the model's role and format:

```ts
import { Llm } from '@capawesome-team/capacitor-llm';

const createSummaryChat = async () => {
  const { id } = await Llm.createChat({
    instructions:
      'You summarize notes into at most three short bullet points. Keep the language of the note.',
  });
  return id;
};
```

The `id` is optional. If you pass your own, for example one per document, a second `createChat(...)` with the same id rejects with the `CHAT_ALREADY_EXISTS` error code. Chats live in memory until you delete them, and each one holds a native session, so call [`deleteChat(...)`](../../sdks/capacitor/llm.md#deletechat) when the user leaves the screen:

```ts
import { Llm } from '@capawesome-team/capacitor-llm';

const closeChat = async (chatId: string) => {
  await Llm.deleteChat({ id: chatId });
};
```

Deleting a chat also cancels a generation that is still running in it.

## Step 3: Generate a Response

[`generateText(...)`](../../sdks/capacitor/llm.md#generatetext) sends a prompt into a chat and resolves with the complete response:

```ts
import { Llm } from '@capawesome-team/capacitor-llm';

const summarize = async (chatId: string, note: string) => {
  const { text } = await Llm.generateText({
    chatId,
    prompt: `Summarize this note:\n\n${note}`,
  });
  return text;
};
```

Only one generation can run per chat at a time. Starting a second one before the first resolves rejects with `GENERATION_IN_PROGRESS`, so disable the submit button while a request is pending or use one chat per task.

## Step 4: Stream Tokens into the UI

For anything longer than a sentence, use [`streamText(...)`](../../sdks/capacitor/llm.md#streamtext) instead. It emits a [`textChunk`](../../sdks/capacitor/llm.md#addlistenertextchunk-) event for every piece of the response and still resolves with the full text at the end. Filter the events by `chatId`, because chunks from all chats arrive through the same listener:

```ts
import { Llm } from '@capawesome-team/capacitor-llm';

const streamSummary = async (chatId: string, note: string, onUpdate: (text: string) => void) => {
  let output = '';
  const listener = await Llm.addListener('textChunk', (event) => {
    if (event.chatId === chatId) {
      output += event.text;
      onUpdate(output);
    }
  });
  try {
    const { text } = await Llm.streamText({
      chatId,
      prompt: `Summarize this note:\n\n${note}`,
    });
    return text;
  } finally {
    await listener.remove();
  }
};
```

Append the chunks in the order they arrive. The text in the resolved promise equals the concatenated chunks, so you can use either.

## Step 5: Cancel a Generation

Give users a way out of a long response. [`cancelGeneration(...)`](../../sdks/capacitor/llm.md#cancelgeneration) stops the running generation of a chat, and the pending promise rejects with the `GENERATION_CANCELED` error code. On iOS the cancellation takes effect immediately:

```ts
import { Llm } from '@capawesome-team/capacitor-llm';

const stop = async (chatId: string) => {
  await Llm.cancelGeneration({ chatId });
};

const summarizeWithCancel = async (chatId: string, note: string) => {
  try {
    const { text } = await Llm.generateText({ chatId, prompt: note });
    return text;
  } catch (error) {
    if ((error as { code?: string }).code === 'GENERATION_CANCELED') {
      return null;
    }
    throw error;
  }
};
```

Treat a cancellation as a normal outcome in your UI. The chat stays usable afterwards.

## Tuning Temperature and Output Length

Two parameters shape the response. `temperature` controls how deterministic the output is, and `maxOutputTokens` caps its length. Set defaults per chat in `createChat(...)` and override them per request in `generateText(...)` or `streamText(...)`:

```ts
import { Llm } from '@capawesome-team/capacitor-llm';

const createReplyChat = async () => {
  const { id } = await Llm.createChat({
    instructions: 'You suggest one short, friendly reply to a message.',
    temperature: 0.4,
    maxOutputTokens: 120,
  });
  return id;
};

const suggestCreativeReply = async (chatId: string, message: string) => {
  const { text } = await Llm.generateText({
    chatId,
    prompt: message,
    temperature: 1.2,
  });
  return text;
};
```

The limits differ per platform, and iOS is the more permissive one:

| Parameter | iOS (Apple Intelligence) | Android (Gemini Nano) |
|---|---|---|
| `temperature` | Values above `1.0` are allowed. | Must be between `0.0` and `1.0`. |
| `maxOutputTokens` | No documented hard limit. | At most `4096`. |
| Context size | About 4,096 tokens per chat session. | Input must stay under about 4,000 tokens. |

The context window is the limit you will hit first. Instructions, every prompt, and every response in a chat count against it. Once it is exceeded, the next generation rejects with `GENERATION_FAILED`. The same error code appears when Apple's safety guardrails block a prompt or a response. The error message carries the platform's reason, so log it, and create a fresh chat when the window is full. For a summarizer that means one chat per document rather than one chat for the whole app.

## Putting It Together: Summarize Notes on Device

The pieces above combine into a screen that summarizes a note while the user watches, with a stop button and clean teardown:

```ts
import { Llm } from '@capawesome-team/capacitor-llm';

export class NoteSummarizer {
  private chatId?: string;

  async start(): Promise<boolean> {
    const { status } = await Llm.getAvailability();
    if (status !== 'available') {
      return false;
    }
    const { id } = await Llm.createChat({
      instructions: 'You summarize notes into at most three short bullet points.',
      maxOutputTokens: 200,
    });
    this.chatId = id;
    return true;
  }

  async summarize(note: string, onUpdate: (text: string) => void): Promise<string | null> {
    if (!this.chatId) {
      throw new Error('Call start() first.');
    }
    const chatId = this.chatId;
    let output = '';
    const listener = await Llm.addListener('textChunk', (event) => {
      if (event.chatId === chatId) {
        output += event.text;
        onUpdate(output);
      }
    });
    try {
      const { text } = await Llm.streamText({ chatId, prompt: note });
      return text;
    } catch (error) {
      if ((error as { code?: string }).code === 'GENERATION_CANCELED') {
        return null;
      }
      throw error;
    } finally {
      await listener.remove();
    }
  }

  async stop(): Promise<void> {
    if (this.chatId) {
      await Llm.cancelGeneration({ chatId: this.chatId });
    }
  }

  async dispose(): Promise<void> {
    if (this.chatId) {
      await Llm.deleteChat({ id: this.chatId });
      this.chatId = undefined;
    }
  }
}
```

Call `start()` when the screen opens, `summarize()` on each note, `stop()` from the cancel button, and `dispose()` when the screen closes. Because each note is short, one chat per screen is enough. For long documents, create a new chat per document so the context window never fills up.

## The Same Code on Android

The plugin uses the platform's own model on Android too, so the code above runs unchanged. There Gemini Nano is provided by AICore through the ML Kit GenAI Prompt API, and three things differ:

- **Availability**: Android reports `available`, `downloadable`, `downloading`, and `unavailable`. When the status is `downloadable`, call [`downloadModel()`](../../sdks/capacitor/llm.md#downloadmodel) and show progress from the `downloadProgress` event. On iOS the system manages the download and the app cannot trigger it.
- **Chat history**: the Android API has no native multi-turn sessions, so the plugin keeps the history in memory and includes it in each prompt.
- **Cancellation**: best-effort on Android, so a few more chunks may arrive after you cancel.

Gemini Nano runs on a short list of devices, such as the Google Pixel 9 series and the Samsung Galaxy S25 series, and the underlying SDK is still in beta. For the Android side in depth, read the [Capacitor ML Kit GenAI Prompt plugin](../../sdks/capacitor/mlkit/genai-prompt.md) documentation and [Capacitor ML Kit 8.2.0 with Gemini Nano support](./capacitor-mlkit-8-2-0-release.md).

## App Review and Privacy

The plugin calls only public Foundation Models APIs, so there is nothing to explain to App Review beyond the feature itself. No prompt, response, or chat history leaves the device, and the plugin collects nothing, so it adds no data type to your privacy nutrition label. Two things remain your responsibility: the content your app feeds into the model, and Apple's guardrails, which can refuse a prompt or a response. Handle `GENERATION_FAILED` with a message the user understands rather than a silent failure.

## Common Errors and Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `getAvailability()` returns `not-enabled` | Apple Intelligence is off on the device. | Ask the user to enable it under Settings > Apple Intelligence & Siri, then wait for `availabilityChange`. |
| `getAvailability()` returns `device-not-eligible` | The device is older than an iPhone 15 Pro or otherwise not on Apple's list. | Hide the feature or use a server-side model. |
| `getAvailability()` returns `unavailable` on iOS | The device runs iOS 18 or older. | Nothing to do on that OS. Gate the feature on the status. |
| Build fails with an unknown framework | Xcode 25 or older. | Update to Xcode 26 or later. |
| `GENERATION_FAILED` | The chat's context window is full, or a guardrail blocked the content. | Create a new chat. Read the error message for the platform reason. |
| `GENERATION_IN_PROGRESS` | A second generation started in the same chat. | Wait for the first one, or use one chat per task. |
| `CHAT_ALREADY_EXISTS` | `createChat(...)` was called twice with the same id. | Reuse the existing chat or delete it first. |
| Everything rejects on the web | Browsers provide no system model. | Gate the feature on `getAvailability()`, which returns `unavailable` there. |

## FAQ

### Does Apple Intelligence text generation work offline?

Yes. The model runs on the device's Neural Engine, and the plugin makes no network requests. Once Apple Intelligence is enabled and the system has downloaded the model, generation works in Airplane Mode.

### Can I use the Foundation Models framework on iOS 18?

No. Apple Intelligence features exist on iOS 18.1 and later, but the developer framework arrived with iOS 26. On iOS 18 the plugin reports `unavailable`, and every other method rejects.

### Can I test Apple Intelligence in the iOS Simulator?

The most reliable setup is a physical iPhone from the eligibility table with iOS 26 and Apple Intelligence turned on. If you use the Simulator, run it on an Apple silicon Mac with macOS 26 and Apple Intelligence enabled, and check `getAvailability()` first. The status tells you whether the model is usable in that environment before you spend time debugging your own code.

### Do I need a paid plan for the Capacitor LLM plugin?

Yes. The plugin is part of Capawesome Insiders, which is a paid subscription that covers all Insider SDKs. Apple charges nothing for the on-device model itself.

### Does the same code run on Android?

Yes. The plugin uses Gemini Nano on Android through the ML Kit GenAI Prompt API. The availability states, the model download flow, and the parameter limits differ, as described above, but the chat, generate, stream, and cancel calls are identical.

## Stay Up to Date on On-Device AI

Apple and Google are both moving fast on system models, and the plugin tracks their public APIs release by release. The [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"} announces new plugin capabilities, Capacitor releases, and guides like this one.

[Subscribe to the Capawesome Newsletter](https://capawesome.io/newsletter/){ .md-button .md-button--primary }

## Conclusion

Gate the feature on `getAvailability()` and ship it with `generateText(...)` first. Add streaming once responses grow past a sentence, and add a cancel button at the same time, because a user who can watch a response also wants to stop it. Keep one chat per task so the 4,096-token window never fills up unnoticed.

If you want voice prompts on top, [Exploring the Capacitor Speech Recognition API](./exploring-the-capacitor-speech-recognition-api.md) covers on-device transcription that feeds straight into `generateText(...)`. For questions about your setup, join the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"}, and subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"} to hear when the plugin adds guided generation or tool calling.
