---
title: Message
description: Normalized message format with text, AST, author, and metadata.
type: reference
---

Incoming messages are normalized across all platforms into a consistent `Message` object.

```typescript
import { Message } from "chat";
```

## Properties

<TypeTable
  type={{
    id: {
      description: 'Platform-specific message ID.',
      type: 'string',
    },
    threadId: {
      description: 'Thread ID in adapter:channel:thread format.',
      type: 'string',
    },
    text: {
      description: 'Plain text content with all formatting stripped.',
      type: 'string',
    },
    formatted: {
      description: 'mdast AST representation — the canonical format for processing.',
      type: 'Root',
    },
    raw: {
      description: 'Original platform-specific payload (escape hatch).',
      type: 'unknown',
    },
    author: {
      description: 'Message author info.',
      type: 'Author',
    },
    metadata: {
      description: 'Timestamps and edit status.',
      type: 'MessageMetadata',
    },
    attachments: {
      description: 'File attachments.',
      type: 'Attachment[]',
    },
    links: {
      description: 'Links found in the message, with optional preview metadata.',
      type: 'LinkPreview[]',
    },
    isMention: {
      description: 'Whether the bot was @-mentioned in this message.',
      type: 'boolean | undefined',
    },
  }}
/>

## Author

<TypeTable
  type={{
    userId: {
      description: 'Platform-specific user ID.',
      type: 'string',
    },
    userName: {
      description: 'Username/handle for @-mentions.',
      type: 'string',
    },
    fullName: {
      description: 'Display name.',
      type: 'string',
    },
    isBot: {
      description: 'Whether the author is a bot.',
      type: 'boolean | "unknown"',
    },
    isMe: {
      description: 'Whether the author is this bot.',
      type: 'boolean',
    },
  }}
/>

### How `isMe` works

Each adapter detects whether a message came from the bot itself. The detection logic varies by platform:

| Platform | Detection method |
|----------|-----------------|
| Slack | Checks `event.user === botUserId` (primary), then `event.bot_id === botId` (for `bot_message` subtypes). Both IDs are fetched during initialization via `auth.test`. |
| Teams | Checks `activity.from.id === appId` (exact match), then checks if `activity.from.id` ends with `:{appId}` (handles `28:{appId}` format). |
| Google Chat | Checks `message.sender.name === botUserId`. The bot user ID is learned dynamically from message annotations when the bot is first @-mentioned. |

<Callout type="info">
All adapters return `false` if the bot ID isn't known yet. This is a safe default that prevents the bot from ignoring messages it should process.
</Callout>

## MessageMetadata

<TypeTable
  type={{
    dateSent: {
      description: 'When the message was sent.',
      type: 'Date',
    },
    edited: {
      description: 'Whether the message has been edited.',
      type: 'boolean',
    },
    editedAt: {
      description: 'When the message was last edited.',
      type: 'Date | undefined',
    },
  }}
/>

## Attachment

<TypeTable
  type={{
    type: {
      description: 'Attachment type.',
      type: '"image" | "file" | "video" | "audio"',
    },
    url: {
      description: 'URL to the file.',
      type: 'string | undefined',
    },
    data: {
      description: 'Binary data (if already fetched).',
      type: 'Buffer | Blob | undefined',
    },
    name: {
      description: 'Filename.',
      type: 'string | undefined',
    },
    mimeType: {
      description: 'MIME type.',
      type: 'string | undefined',
    },
    size: {
      description: 'File size in bytes.',
      type: 'number | undefined',
    },
    'fetchData()': {
      description: 'Fetch the attachment data. Handles platform auth automatically.',
      type: '() => Promise<Buffer> | undefined',
    },
  }}
/>

## LinkPreview

Links found in incoming messages are extracted and exposed as `LinkPreview` objects. On platforms that support it (currently Slack), links pointing to other chat messages include a `fetchMessage()` callback to retrieve the full linked message.

<TypeTable
  type={{
    url: {
      description: 'The URL.',
      type: 'string',
    },
    title: {
      description: 'Title from unfurl metadata (if available).',
      type: 'string | undefined',
    },
    description: {
      description: 'Description from unfurl metadata (if available).',
      type: 'string | undefined',
    },
    imageUrl: {
      description: 'Preview image URL (if available).',
      type: 'string | undefined',
    },
    siteName: {
      description: 'Site name, e.g. "Vercel" (if available).',
      type: 'string | undefined',
    },
    'fetchMessage()': {
      description: 'Fetch the linked chat message. Available when the URL points to a message on the same platform (e.g. a Slack message link).',
      type: '() => Promise<Message> | undefined',
    },
  }}
/>

<Callout type="info">
When using [`toAiMessages()`](/docs/api/to-ai-messages), link metadata is automatically appended to the message content. Embedded message links are labeled as `[Embedded message: ...]` so the AI model understands the context.
</Callout>

### Platform support

| Platform | Link extraction | `fetchMessage()` |
|----------|----------------|-------------------|
| Slack | URLs from `rich_text` blocks or `<url>` text patterns | Slack message links (`*.slack.com/archives/...`) |
| Others | Not yet — `links` is always `[]` | — |

## Serialization

Messages can be serialized for workflow engines and external systems.

```typescript
// Serialize
const json = message.toJSON();

// Deserialize
const restored = Message.fromJSON(json);
```

The serialized format converts `Date` fields to ISO strings and omits non-serializable fields like `data` buffers and `fetchData` functions.
