---
title: Message types | Letta Docs
description: Understanding the different message types in the Letta API
---

When you interact with a Letta agent and retrieve its message history using `client.agents.messages.list()`, you’ll receive various types of messages that represent different aspects of the agent’s execution. This guide explains all message types and how to work with them.

## Message Type Categories

### User and System Messages

#### `user_message`

Messages sent by the user or system events packaged as user input.

**Structure:**

```
{
  id: string;
  date: datetime;
  message_type: "user_message";
  content: string | Array<TextContent | ImageContent>;
  name?: string;
  otid?: string;
  sender_id?: string;
}
```

**Special User Message Subtypes:** User messages can contain JSON with a `type` field indicating special message subtypes:

- **`login`** - User login events

  ```
  {
    "type": "login",
    "last_login": "Never (first login)",
    "time": "2025-10-03 12:34:56 PM PDT-0700"
  }
  ```

- **`user_message`** - Standard user messages

  ```
  {
    "type": "user_message",
    "message": "Hello, agent!",
    "time": "2025-10-03 12:34:56 PM PDT-0700"
  }
  ```

- **`system_alert`** - System notifications and alerts

  ```
  {
    "type": "system_alert",
    "message": "System notification text",
    "time": "2025-10-03 12:34:56 PM PDT-0700"
  }
  ```

#### `system_message`

Messages generated by the system, typically used for internal context.

**Structure:**

```
{
  id: string;
  date: datetime;
  message_type: "system_message";
  content: string;
  name?: string;
}
```

**Note:** System messages are never streamed back in responses; they’re only visible when paginating through message history.

### Agent Reasoning and Responses

#### `reasoning_message`

Represents the agent’s internal reasoning or “chain of thought.”

**Structure:**

```
{
  id: string;
  date: datetime;
  message_type: "reasoning_message";
  reasoning: string;
  source: "reasoner_model" | "non_reasoner_model";
  signature?: string;
}
```

**Fields:**

- `reasoning` - The agent’s internal thought process
- `source` - Whether this was generated by a model with native reasoning (like o1) or via prompting
- `signature` - Optional cryptographic signature for reasoning verification (for models that support it)

#### `hidden_reasoning_message`

Represents reasoning that has been hidden from the response.

**Structure:**

```
{
  id: string;
  date: datetime;
  message_type: "hidden_reasoning_message";
  state: "redacted" | "omitted";
  hidden_reasoning?: string;
}
```

**Fields:**

- `state: "redacted"` - The provider redacted the reasoning content
- `state: "omitted"` - The API chose not to include reasoning (e.g., for o1/o3 models)

#### `assistant_message`

The actual message content sent by the agent.

**Structure:**

```
{
  id: string;
  date: datetime;
  message_type: "assistant_message";
  content: string | Array<TextContent>;
  name?: string;
}
```

### Tool Execution Messages

#### `tool_call_message`

A request from the agent to execute a tool.

**Structure:**

```
{
  id: string;
  date: datetime;
  message_type: "tool_call_message";
  tool_call: {
    name: string;
    arguments: string; // JSON string
    tool_call_id: string;
  }
}
```

**Example:**

```
{
  message_type: "tool_call_message",
  tool_call: {
    name: "archival_memory_search",
    arguments: '{"query": "user preferences", "page": 0}',
    tool_call_id: "call_abc123"
  }
}
```

#### `tool_return_message`

The result of a tool execution.

**Structure:**

```
{
  id: string;
  date: datetime;
  message_type: "tool_return_message";
  tool_return: string;
  status: "success" | "error";
  tool_call_id: string;
  stdout?: string[];
  stderr?: string[];
}
```

**Fields:**

- `tool_return` - The formatted return value from the tool
- `status` - Whether the tool executed successfully
- `stdout`/`stderr` - Captured output from the tool execution (useful for debugging)

### Human-in-the-Loop Messages

#### `approval_request_message`

A request for human approval before executing a tool.

**Structure:**

```
{
  id: string;
  date: datetime;
  message_type: "approval_request_message";
  tool_call: {
    name: string;
    arguments: string;
    tool_call_id: string;
  }
}
```

See [Human-in-the-Loop](/guides/core-concepts/tools/human-in-the-loop/index.md) for more information on this experimental feature.

#### `approval_response_message`

The user’s response to an approval request.

**Structure:**

```
{
  id: string;
  date: datetime;
  message_type: "approval_response_message";
  approve: boolean;
  approval_request_id: string;
  reason?: string;
}
```

## Working with Messages

### Listing Messages

- [TypeScript](#tab-panel-98)
- [Python](#tab-panel-99)

```
import Letta from "@letta-ai/letta-client";


const client = new Letta({
  apiKey: process.env.LETTA_API_KEY,
});


// List recent messages
const messages = await client.agents.messages.list("agent-id", {
  limit: 50,
  useAssistantMessage: true,
});


// Iterate through message types
for (const message of messages) {
  switch (message.message_type) {
    case "user_message":
      console.log("User:", message.content);
      break;
    case "assistant_message":
      console.log("Agent:", message.content);
      break;
    case "reasoning_message":
      console.log("Reasoning:", message.reasoning);
      break;
    case "tool_call_message":
      console.log("Tool call:", message.tool_calls[0].name);
      break;
    // ... handle other types
  }
}
```

```
from letta_client import Letta
import os


client = Letta(api_key=os.getenv("LETTA_API_KEY"))


# List recent messages
messages = client.agents.messages.list(
    agent_id="agent-id",
    limit=50,
    use_assistant_message=True
)


# Iterate through message types
for message in messages:
    if message.message_type == "user_message":
        print(f"User: {message.content}")
    elif message.message_type == "assistant_message":
        print(f"Agent: {message.content}")
    elif message.message_type == "reasoning_message":
        print(f"Reasoning: {message.reasoning}")
    elif message.message_type == "tool_call_message":
        print(f"Tool call: {message.tool_call.name}")
    # ... handle other types
```

### Filtering Messages by Type

- [TypeScript](#tab-panel-100)
- [Python](#tab-panel-101)

```
// Get only assistant messages (what the agent said to the user)
const agentMessages = messages.filter(
  (msg) => msg.message_type === "assistant_message",
);


// Get all tool-related messages
const toolMessages = messages.filter(
  (msg) =>
    msg.message_type === "tool_call_message" ||
    msg.message_type === "tool_return_message",
);


// Get conversation history (user + assistant messages only)
const conversation = messages.filter(
  (msg) =>
    msg.message_type === "user_message" ||
    msg.message_type === "assistant_message",
);
```

```
# Get only assistant messages (what the agent said to the user)
agent_messages = [
    msg for msg in messages
    if msg.message_type == "assistant_message"
]


# Get all tool-related messages
tool_messages = [
    msg for msg in messages
    if msg.message_type in ["tool_call_message", "tool_return_message"]
]


# Get conversation history (user + assistant messages only)
conversation = [
    msg for msg in messages
    if msg.message_type in ["user_message", "assistant_message"]
]
```

### Pagination

Messages support cursor-based pagination:

- [TypeScript](#tab-panel-102)
- [Python](#tab-panel-103)

```
// Get first page
let messages = await client.agents.messages.list("agent-id", {
  limit: 100,
});


// Get next page using the last message ID
const lastMessageId = messages[messages.length - 1].id;
const nextPage = await client.agents.messages.list("agent-id", {
  limit: 100,
  before: lastMessageId,
});
```

```
# Get first page
messages = client.agents.messages.list(
    agent_id="agent-id",
    limit=100
)


# Get next page using the last message ID
last_message_id = messages[-1].id
next_page = client.agents.messages.list(
    agent_id="agent-id",
    limit=100,
    before=last_message_id
)
```

## Message Metadata Fields

All message types include these common fields:

- **`id`** - Unique identifier for the message
- **`date`** - ISO 8601 timestamp of when the message was created
- **`message_type`** - The discriminator field identifying the message type
- **`name`** - Optional name field (varies by message type)
- **`otid`** - Offline threading ID for message correlation
- **`sender_id`** - The ID of the sender (identity or agent ID)
- **`step_id`** - The step ID associated with this message
- **`is_err`** - Whether this message is part of an error step (debugging only)
- **`seq_id`** - Sequence ID for ordering
- **`run_id`** - The run ID associated with this message

## Best Practices

### 1. Use Type Discriminators

Always check the `message_type` field to safely access type-specific fields:

- [TypeScript](#tab-panel-104)
- [Python](#tab-panel-105)

```
if (message.message_type === "tool_call_message") {
  // TypeScript now knows message has a toolCall field
  console.log(message.tool_calls[0].name);
}
```

```
if message.message_type == "tool_call_message":
    # Safe to access tool_call
    print(message.tool_call.name)
```

### 2. Handle Special User Messages

When displaying conversations to end users, filter out internal messages:

```
def is_internal_message(msg):
    """Check if a user message is internal (login, system_alert, etc.)"""
    if msg.message_type != "user_message":
        return False


    if not isinstance(msg.content, str):
        return False


    try:
        parsed = json.loads(msg.content)
        return parsed.get("type") in ["login", "system_alert"]
    except:
        return False


# Get user-facing messages only
display_messages = [
    msg for msg in messages
    if not is_internal_message(msg)
]
```

### 3. Track Tool Execution

Match tool calls with their returns using `tool_call_id`:

```
# Build a map of tool calls to their returns
tool_calls = {
    msg.tool_call.tool_call_id: msg
    for msg in messages
    if msg.message_type == "tool_call_message"
}


tool_returns = {
    msg.tool_call_id: msg
    for msg in messages
    if msg.message_type == "tool_return_message"
}


# Find failed tool calls
for call_id, call_msg in tool_calls.items():
    if call_id in tool_returns:
        return_msg = tool_returns[call_id]
        if return_msg.status == "error":
            print(f"Tool {call_msg.tool_call.name} failed:")
            print(f"  {return_msg.tool_return}")
```

## See Also

- [Human-in-the-Loop](/guides/core-concepts/tools/human-in-the-loop/index.md) - Using approval messages
- [Streaming Responses](/guides/core-concepts/messages/streaming/index.md) - Receiving messages in real-time
- [API Reference](/api-reference/agents/messages/list/index.md) - Full API documentation
