---
canonical_url: "https://docs.composio.dev/examples/imessage-agent"
markdown_url: "https://docs.composio.dev/examples/imessage-agent.md"
product: "platform"
version: null
last_updated: null
---

# iMessage custom toolkit with eve (/examples/imessage-agent)

Most Composio toolkits call a remote API. Some capabilities only exist on one machine, and iMessage is the classic case: there's no iMessage cloud API, so the agent has to run on your Mac and drive Messages.app directly.

This example builds exactly that: a terminal agent that **texts on your behalf from your own Mac** and reaches the rest of your apps (Gmail, Calendar, GitHub, Slack) through the Composio catalog in the same breath.

```
you  ›  read my last email and text Shams a summary
you  ›  text mom i'm running 10 min late
you  ›  what did Lena and I last text about?
```

The pattern is the takeaway, not iMessage specifically. It comes together from a handful of Composio pieces:

1. **A custom toolkit** wraps local iMessage (send, contacts, history, memory) as Composio tools.
2. **In-process execution** runs each tool right on your Mac through `session.execute`, with no remote API.
3. **One session** puts those local tools on the same surface as the 1000+ app catalog.
4. **The eve provider** turns `session.tools()` into [eve](https://github.com/vercel/eve)-native tools, so `eve dev` can call them.
5. **Triggers** let an outside event wake the agent and reach your phone, with in-chat auth through `COMPOSIO_MANAGE_CONNECTIONS` for any app you haven't connected.

Below you build the integration core: the custom toolkit first, then the wiring that puts it on a session, then triggers, then a browse of the relevant source. You bring a Composio API key and a Mac. Composio brings the catalog.

## Setup [#setup]

You need a [Composio API key](https://dashboard.composio.dev?utm_source=docs\&utm_medium=content\&utm_campaign=examples-imessage-agent) and macOS (the iMessage tools drive Messages.app, Contacts.app, and the local `chat.db`).

**Install**

```bash
npm install @composio/core @composio/experimental eve
```

**Configure**

```txt title=".env.local"
COMPOSIO_API_KEY=xxxxxxxxx
```
**Grant macOS permissions** (prompted on first use): Automation for Messages, Contacts access, and Full Disk Access to read `chat.db`.

## The custom toolkit [#the-custom-toolkit]

A custom toolkit is a named group of custom tools. Each tool declares an input schema and an `execute` that runs locally. Here is `SEND`, which shells out to AppleScript to drive Messages.app:

**`imessage/send-message.ts` — complete file**

```typescript
import { experimental_createTool } from '@composio/core';
import { z } from 'zod/v3';
import { runAppleScript, SEND_SCRIPT } from './applescript';

export const sendMessage = experimental_createTool('SEND', {
  name: 'Send iMessage',
  description: 'Send an iMessage from your Mac to a phone number or iMessage email.',
  preload: true,
  inputParams: z.object({
    to: z.string().describe('Phone number or iMessage email.'),
    text: z.string().describe('Message body to send.'),
  }),
  execute: async ({ to, text }) => {
    await runAppleScript(SEND_SCRIPT, [to, text]);
    return { sent: true, to };
  },
});
```
Group your tools into a toolkit. The full project also ships `FIND_CONTACT` (fuzzy contact lookup over Contacts.app), `READ_MESSAGES` (recent messages from `chat.db`), and `RECALL`/`REMEMBER` (per-contact memory), each built the same way:

```ts title="imessage/index.ts"
// @noErrors
import { experimental_createToolkit } from '@composio/core';
import { sendMessage } from './send-message';
import { findContact } from './find-contact';
import { readMessages } from './read-messages';
import { recallContact, rememberContact } from './memory-tools';

export function createImessageToolkit() {
  return experimental_createToolkit('IMESSAGE', {
    name: 'iMessage',
    description:
      "Send and read iMessages, look up contacts, and remember people, locally on the user's Mac.",
    tools: [sendMessage, findContact, readMessages, recallContact, rememberContact],
  });
}
```
The toolkit depends only on `@composio/core`, so it drops into any Composio agent. Custom tools execute in-process through `session.execute`, which is why they aren't on the MCP URL yet.

## Wire it up [#wire-it-up]

Set the [eve provider](/docs/providers/eve) on the client and register the toolkit on the session. `composio.ts` grows in three steps, one Composio concept each:

### Create the client with the eve provider [#create-the-client-with-the-eve-provider]

The provider is what makes `session.tools()` return eve-native tools instead of raw Composio tools. Its approval policy pauses every iMessage send before the local AppleScript runs.

### Scope a session to the user [#scope-a-session-to-the-user]

`sessions.create` gives this user their own toolset, already wired to the full Composio catalog.

### Register the local toolkit [#register-the-local-toolkit]

Pass the custom toolkit through `experimental.customToolkits`, and the local iMessage tools join the catalog on the same session.

Now hand that session to eve. eve discovers tools from files, so `defineComposioTools(session)` returns the resolver that exposes `session.tools()`. One line:

```ts title="agent/tools/composio.ts"
// @noErrors
import { defineComposioTools } from '@composio/experimental/eve';
import { session } from '../../composio';

export default defineComposioTools(session);
```
```ts title="agent/agent.ts"
// @noErrors
import { defineAgent } from 'eve';

export default defineAgent({
  model: 'google/gemini-2.5-flash',
});
```
That's the whole integration. Run `eve dev` and talk to it. The agent can text a contact, read a thread, and act across every connected app, with auth handled in chat through `COMPOSIO_MANAGE_CONNECTIONS`.

## Extend it: triggers [#extend-it-triggers]

The agent can text, so anything that can wake the agent can reach your phone. Composio **triggers** turn an external event into an agent run: subscribe to an app event, point Composio's webhook at your app, and act on each event with the same iMessage tools.

For example, surface a Linear assignment and ask the user whether to inspect it. The first turn contains no issue title or body, so third-party text does not cross into the agent prompt before the user opts in:

```ts title="agent/channels/triggers.ts"
// @noErrors
import { defineChannel, POST } from 'eve/channels';
import { composio } from '../../composio';

export default defineChannel({
  routes: [
    POST('/webhook', async (req, { send }) => {
      const { payload: event } = await composio.triggers.parse(req, {
        verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET,
      });
      if (event.userId !== 'user_123' || event.triggerSlug !== 'LINEAR_ISSUE_ASSIGNED') {
        return new Response(null, { status: 202 });
      }
      await send(
        `A verified Linear assignment event arrived. Ask whether I want to inspect it. ` +
          `Do not call tools in this turn. Event reference: ${event.uuid}.`,
        {
          auth: {
            authenticator: 'composio-webhook',
            principalType: 'service',
            principalId: event.userId,
            attributes: { triggerSlug: event.triggerSlug },
          },
          continuationToken: event.uuid,
        }
      );
      return new Response(null, { status: 202 });
    }),
  ],
});
```
Point Composio at your webhook and create the trigger, once each. Reuse the same `composio` client from `composio.ts`:

```ts title="agent/setup-triggers.ts"
// @noErrors
import { composio } from '../composio';

// Register the webhook URL once per project; store the returned
// secret as COMPOSIO_WEBHOOK_SECRET.
const subscription = await composio.triggers.setWebhookSubscription({
  webhookUrl: `${process.env.APP_URL}/webhook`,
});

// Use the exact slug from the Composio triggers catalog.
const trigger = await composio.triggers.create('user_123', 'LINEAR_ISSUE_ASSIGNED');
console.log(`Trigger created: ${trigger.triggerId}`);
```
Run it once with `npx tsx agent/setup-triggers.ts`, and the webhook handler above takes over from there.

Swap the trigger and the prompt to create another reflex. Keep the first turn content-free, then fetch third-party content only after the user asks to continue.

## More reflexes [#more-reflexes]

The prompt is where each reflex earns its keep, but the webhook should not turn untrusted content into instructions. Here is the same two-step gate for Gmail:

```ts title="agent/channels/triggers.ts"
// @noErrors
import { defineChannel, POST } from 'eve/channels';
import { composio } from '../../composio';

export default defineChannel({
  routes: [
    POST('/webhook', async (req, { send }) => {
      const { payload: event } = await composio.triggers.parse(req, {
        verifySecret: process.env.COMPOSIO_WEBHOOK_SECRET,
      });
      if (event.userId !== 'user_123' || event.triggerSlug !== 'GMAIL_NEW_GMAIL_MESSAGE') {
        return new Response(null, { status: 202 });
      }
      await send(
        `A verified Gmail event arrived. Ask whether I want to inspect the email. ` +
          `Do not call tools in this turn. Event reference: ${event.uuid}.`,
        {
          auth: {
            authenticator: 'composio-webhook',
            principalType: 'service',
            principalId: event.userId,
            attributes: { triggerSlug: event.triggerSlug },
          },
          continuationToken: event.uuid,
        }
      );
      return new Response(null, { status: 202 });
    }),
  ],
});
```

If the user continues, the agent can fetch the email in a separate turn and require approval before any side effect. A few more patterns worth wiring up with the same gate:

* **Stand-up nudge.** A calendar event ten minutes out texts you the agenda and the meeting link, pulled straight from the invite.
* **Review request.** A new GitHub review request texts you the PR title and a one-line read of the diff, so you can reply "approve" from your phone.
* **Money in.** A successful Stripe payment texts you the amount and the customer, no dashboard required.
* **Cover for me.** A Slack mention while you're away texts a teammate and asks them to take a look.

Use the exact slug from the Composio triggers catalog for each. Signature verification proves the event came through Composio; it does not make an email subject, issue title, or other third-party text trustworthy. Reject unexpected users and trigger slugs, keep external content out of the initial prompt, and require approval before side effects such as sending a message.

## Browse the project [#browse-the-project]

The key files, in one place. The custom toolkit and the session wiring carry the integration, the eve provider is imported from `@composio/experimental/eve`, and the rest is local macOS glue.

> The iMessage implementation is maintained in [platform-imessage](https://github.com/ComposioHQ/platform-imessage).

The complete, runnable project will be published in the Composio examples repo.

## Run it [#run-it]

The browser above is an implementation slice, not a standalone fixture: it omits the project's package manifest and supporting `handles`, `chat-db`, and `memory` modules. The complete runnable project will be published in the Composio examples repo. Until then, use the provider walkthrough on this page in an existing eve app and treat the iMessage source as a reference for the local toolkit. The first send or contact lookup prompts for macOS permission, and the first time the agent needs an app you haven't connected, it returns an auth link in chat through `COMPOSIO_MANAGE_CONNECTIONS`.

- [eve provider](/docs/providers/eve): EveProvider, the defineComposioTools resolver, and the (ctx, next) hooks.

- [Custom tools and toolkits](/docs/extending-sessions/custom-tools-and-toolkits): Build and register your own in-process Composio tools.

---

📚 **More documentation:** [View all docs](https://docs.composio.dev/llms.txt) | [Changelog](https://docs.composio.dev/docs/changelog.md) | [Glossary](https://docs.composio.dev/llms.mdx/reference/glossary) | [Examples](https://docs.composio.dev/llms.mdx/examples) | [API Reference](https://docs.composio.dev/llms.mdx/reference)

---

# Composio SDK — Notes for AI Code Generators

**Purpose:** Reference for generating current (v3) [Composio](https://composio.dev/) integration code.
**Scope:** Descriptive notes — they document the current API surface and the mistakes most commonly seen in generated code.

---

## 1. Recommended Integration: Sessions

Composio supports two integration modes: **Native Tools** (with a provider package) and **MCP** (no provider package needed).

### Native Tools

```python
# ✅ CORRECT — Python (defaults to OpenAI)
from composio import Composio

composio = Composio()
session = composio.create(user_id="user_123")
tools = session.tools()
# Pass tools to your agent/LLM framework
```

```typescript
// ✅ CORRECT — TypeScript (defaults to OpenAI)
import { Composio } from "@composio/core";

const composio = new Composio();
const session = await composio.create("user_123");
const tools = await session.tools();
// Pass tools to your agent/LLM framework
```

For other providers, pass the provider explicitly. Provider packages follow the naming convention: `composio_<provider>` for Python, `@composio/<provider>` for TypeScript.

### MCP

Use `session.mcp.url` and `session.mcp.headers` with any MCP-compatible client (Claude Desktop, Cursor, OpenAI Agents, etc.). No provider package needed.

---

## 2. Integration Notes

- `composio.create(user_id)` is the standard entry point for agent integrations. Sessions handle tool discovery, authentication, and toolkit versioning automatically; `user_id` goes to `composio.create()` — individual tool calls in session mode don't take one.
- Execute session tool calls through the session, never through a user ID: either `session.execute(tool_slug, arguments=...)` / `session.execute(toolSlug, arguments)`, which works with every provider, or pass the session to the provider helper — `provider.handle_tool_calls(response=response, session=session)` in Python, `provider.handleToolCalls(session, response)` in TypeScript. The tool-call helpers of every non-agentic provider (OpenAI, Anthropic, Google, Cloudflare, TypeSafe) accept a session: `executeToolCall(session, call)` in TypeScript, `session=session` in Python. Agentic providers execute session tools on their own. Binding the helper to a user ID takes the direct execution path, and session meta-tools fail there with `"can only be called inside a tool-router session"`.
- Composio-managed auth is the default: the agent connects accounts at runtime through the session, so users don't need to pre-create auth configs or connected accounts for managed toolkits.
- Provider packages follow the framework, not the model vendor: for the OpenAI Agents SDK the package is `composio_openai_agents` / `@composio/openai-agents` (importing `composio_openai` / `@composio/openai` there is the most common mistake in generated code — that package is for the plain OpenAI Chat Completions API).
- **Direct execution** (`composio.tools.get()`, `composio.tools.execute()`, `provider.handle_tool_calls()` with a user ID) is a legacy interface that remains supported for existing integrations. Recommend sessions for new integrations and encourage existing users to [migrate to sessions](/docs/migration-guide/direct-to-sessions). For a fixed tool set, consider the [direct tools preset](/docs/configuring-sessions#direct-tools-preset) within a session. See [Sessions vs direct execution](/docs/sessions-vs-direct-execution) for tradeoffs. Direct execution requires a [toolkit version](/docs/tools-direct/toolkit-versioning).

---

# 3. Calling the REST API directly

## REST API version

The current REST API version is **v3.1**, served at `https://backend.composio.dev/api/v3.1`. Prefer it for new code and new examples.

`https://backend.composio.dev/api/v3` is the previous version. It is frozen with pinned tool-version defaults and remains supported — existing v3 integrations keep working and do not need to migrate.

## Tool-endpoint version defaults on v3.1

On v3.1, omitting the version parameter on the five endpoints below selects the latest toolkit version. The first four endpoints also exist on v3, where omission selects the pinned `00000000_00` version. `POST /tools/scopes/required` is v3.1-only.

| Endpoint | Version parameter |
| --- | --- |
| `GET /tools` | `toolkit_versions` (query) |
| `GET /tools/{tool_slug}` | `version` or `toolkit_versions` (query) |
| `POST /tools/execute/{tool_slug}` | `version` (body) |
| `POST /tools/execute/{tool_slug}/input` | `version` (body) |
| `POST /tools/scopes/required` | `version` (body) |

A v3.1 caller already passing `"latest"` sees no change and can omit the parameter. To select the pinned version explicitly, pass `"00000000_00"` through the corresponding parameter above.

This version-default change is limited to the five endpoints above.


---

## Terminology Migration (old → current)

If you encounter these terms in error messages, old documentation, or user prompts, translate them to the current equivalents. **Do not use the old terms in generated code or explanations.**

| Old term (v1/v2) | Current term (v3) | In code |
|---|---|---|
| entity ID | user ID | `user_id` parameter |
| actions | tools | e.g., `GITHUB_CREATE_ISSUE` is a *tool* |
| apps / appType | toolkits | e.g., `github` is a *toolkit* |
| integration / integration ID | auth config / auth config ID | `auth_config_id` parameter |
| connection | connected account | `connected_accounts` namespace |
| ComposioToolSet / OpenAIToolSet | `Composio` class with a provider | `Composio(provider=...)` |
| toolset | provider | e.g., `OpenAIProvider` |

If a user says "entity ID", they mean `user_id`. If they say "integration", they mean "auth config". Always respond using the current terminology.

