> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# User API

> Persistent per-user storage for state, preferences, and custom data

## Overview

The User API is a **persistent, per-user key-value store** that survives across conversations and sessions. Use it to store any data tied to a user — onboarding progress, workflow state, preferences, cart contents, verification status, or any custom fields your agent needs.

Think of it as a **schemaless user database**: read and write any property, and it persists automatically. This makes it ideal for **multi-step flows** where your agent needs to remember where a user left off.

```typescript theme={null}
import { User } from 'lua-cli';

// Get user instance
const user = await User.get();

// Access the read-only user profile (system-provided identity)
console.log(user._luaProfile.fullName);
console.log(user._luaProfile.emailAddresses); // A simple array of strings

// Store ANY custom data — it persists across conversations and sessions
user.onboardingStep = 'verified';
user.lastProductViewed = 'SKU-123';
user.collectedData = { company: 'Acme', plan: 'enterprise' };
await user.save();
```

## Read-Only User Profile (`_luaProfile`)

A new, read-only property `user._luaProfile` is now available on the user object. This provides a secure and reliable way to access core user identity information.

* **`_luaProfile`** (Read-Only): Contains essential user data like `userId`, `fullName`, `mobileNumbers`, and `emailAddresses`.
  * The `_lua` prefix indicates this is a special, system-provided property.
  * This data is **immutable**; any attempts to change it will be silently ignored.
* **`user.*`** (Mutable): Continue to use the main `user` object to store and manage any custom data your agent needs, such as preferences, shopping carts, or game scores.

<CardGroup>
  <Card title="Core User Identity" icon="lock">
    Access read-only data like `user._luaProfile.userId` and `user._luaProfile.fullName`.
  </Card>

  <Card title="Persistent Storage" icon="database">
    Store any data on the user object — onboarding state, workflow progress, preferences, cart contents. Persists across all conversations and sessions.
  </Card>
</CardGroup>

### Deprecated `user.userId`

To centralize core user information, `user.userId` is now deprecated. Please update your code to use `user._luaProfile.userId`. The old property will be removed in a future version.

```typescript theme={null}
// ✅ New & Recommended
const userId = user._luaProfile.userId;

// ⚠️ Deprecated
const oldUserId = user.userId;
```

## User as a State Store

The User object is not just for profile data — it is a **persistent state store** for building multi-step, stateful agent workflows. Any property you write to the user object persists across conversations, sessions, and even days or months.

<Note>
  **Key insight for AI agents and developers:** The User object is the primary way to maintain state across conversations. Use it to track onboarding progress, accumulate data across tool calls, and resume workflows exactly where the user left off.
</Note>

### Onboarding State Machine Example

```typescript theme={null}
import { LuaTool, User } from 'lua-cli';
import { z } from 'zod';

export class OnboardingTool implements LuaTool {
  name = 'handle_onboarding';
  description = 'Guide user through onboarding steps, resuming where they left off';

  inputSchema = z.object({
    data: z.record(z.any()).optional().describe('Data collected in this step')
  });

  async execute(input: any) {
    const user = await User.get();

    // Read persisted state — survives across conversations!
    const step = user.onboardingStep || 'not_started';

    switch (step) {
      case 'not_started':
        user.onboardingStep = 'collecting_info';
        user.onboardingStartedAt = new Date().toISOString();
        await user.save();
        return { message: "Let's get you set up! What's your company name?" };

      case 'collecting_info':
        user.companyName = input.data?.companyName;
        user.onboardingStep = 'awaiting_verification';
        user.completedSteps = ['welcome', 'personal_info'];
        await user.save();
        return { message: 'Great! Now let\'s verify your identity.' };

      case 'awaiting_verification':
        user.verified = true;
        user.onboardingStep = 'choosing_plan';
        user.completedSteps = [...(user.completedSteps || []), 'verification'];
        await user.save();
        return { message: 'Verified! Which plan works best for you?' };

      case 'choosing_plan':
        user.plan = input.data?.plan;
        user.onboardingStep = 'complete';
        user.onboardingCompletedAt = new Date().toISOString();
        user.completedSteps = [...(user.completedSteps || []), 'plan_selection'];
        await user.save();
        return { message: `You're all set on the ${user.plan} plan!` };

      case 'complete':
        return {
          message: `Welcome back! You completed onboarding on ${user.onboardingCompletedAt}.`,
          plan: user.plan,
          company: user.companyName
        };
    }
  }
}
```

### Common State Storage Patterns

| Pattern                   | What to store on `user.*`                               | Example                                                 |
| ------------------------- | ------------------------------------------------------- | ------------------------------------------------------- |
| **Onboarding flow**       | `onboardingStep`, `completedSteps`, `collectedData`     | Track which step the user is on, resume across sessions |
| **Multi-step form**       | `formData`, `currentSection`, `validationErrors`        | Accumulate form data across multiple tool calls         |
| **Verification workflow** | `verificationStatus`, `documentsUploaded`, `verifiedAt` | Track identity/document verification progress           |
| **Feature adoption**      | `featuresUsed`, `tutorialStep`, `firstActionAt`         | Guide users through product discovery                   |
| **Conversation context**  | `lastIntent`, `pendingAction`, `conversationTopic`      | Help the agent maintain context between sessions        |

## Features

<CardGroup cols={2}>
  <Card title="Direct Access" icon="bolt">
    Access properties with `user.name` instead of `user.data.name`
  </Card>

  <Card title="Auto Sanitization" icon="shield">
    Removes sensitive fields automatically
  </Card>

  <Card title="Built-in Methods" icon="wrench">
    `update()`, `save()`, `send()`, and `clear()` included
  </Card>

  <Card title="Messaging" icon="message">
    Send text, images, and files to users
  </Card>
</CardGroup>

## get(identifier?)

Retrieve user data as a UserDataInstance. Supports lookup by userId, email, or phone number.

```typescript theme={null}
User.get(identifier?: string | UserLookupOptions): Promise<UserDataInstance | null>
```

<ParamField path="identifier" type="string | UserLookupOptions">
  One of:

  * **No parameter**: Returns current user from conversation context
  * **string (userId)**: Retrieve a specific user by ID
  * **`{ email: string }`**: Look up user by email address
  * **`{ phone: string }`**: Look up user by phone number (with or without `+` prefix)

  **Required in:** Webhooks, pre-defined LuaJob (no conversational context)

  **Optional in:** Tools, dynamic jobs (has conversational context)
</ParamField>

**Returns:** `UserDataInstance` with proxy-based property access, or `null` if user not found (for email/phone lookup)

<Note>
  Look up users by email or phone — especially useful in webhooks where you receive contact info from external systems but don't have the internal userId.
</Note>

### Shortcut: `User.getChatHistory()`

If you only need chat history for the **current** user (in a tool with conversational context), the top-level static `User.getChatHistory()` skips the `User.get()` step:

```typescript theme={null}
import { User } from 'lua-cli';

const history = await User.getChatHistory();
```

This is equivalent to `(await User.get()).getChatHistory()` — see the [instance method](#getchathistory) below for the full return shape and examples.

## When to Use userId Parameter

Understanding when userId is required vs optional:

| Context      | Method                                      | Identifier Required? | Why                        |
| ------------ | ------------------------------------------- | -------------------- | -------------------------- |
| **Tools**    | `User.get()`                                | ❌ Optional           | Has conversational context |
| **Webhooks** | `User.get(userId)` or `User.get({ email })` | ✅ **REQUIRED**       | No conversational context  |
| **LuaJob**   | `User.get(userId)` or `User.get({ phone })` | ✅ **REQUIRED**       | No conversational context  |
| **Jobs API** | `jobInstance.user()`                        | ❌ N/A                | Automatic user context     |

<Warning>
  **Context Matters:**

  * **Tools:** identifier is optional - defaults to current user in conversation
  * **Webhooks:** identifier is REQUIRED - use userId, email, or phone lookup
  * **LuaJob (pre-defined):** identifier is REQUIRED - use userId, email, or phone lookup
  * **Jobs API (dynamic):** Use `jobInstance.user()` instead - automatic context captured!
</Warning>

<Tip>
  **New!** In webhooks, you can now look up users by email or phone if you don't have the userId:

  ```typescript theme={null}
  const user = await User.get({ email: 'customer@example.com' });
  const user = await User.get({ phone: '+1234567890' });
  ```
</Tip>

**Examples:**

<Tabs>
  <Tab title="Current User">
    **Get the current user from conversation context:**

    ```typescript theme={null}
    import { User } from 'lua-cli';

    // In your tool
    async execute(input: any) {
      const user = await User.get();
      
      // Direct property access
      console.log(user.name);    // "John Doe"
      console.log(user.email);   // "john@example.com"
      console.log(user.phone);   // "555-0123"
      
      return { userName: user.name };
    }
    ```
  </Tab>

  <Tab title="Specific User (Webhooks/LuaJob)">
    **Get a specific user by ID (REQUIRED in webhooks and pre-defined jobs):**

    ```typescript theme={null}
    import { User } from 'lua-cli';

    // In a webhook (NO conversational context)
    execute: async (event) => {
      // ⚠️ MUST provide userId in webhooks
      const customerId = event.data.object.metadata?.customerId;
      
      if (!customerId) {
        return { error: 'No customer ID provided' };
      }
      
      const user = await User.get(customerId);
      
      // Send message to that specific user
      await user.send([{
        type: 'text',
        text: `✅ Payment received: $${event.data.object.amount/100}`
      }]);
      
      return { notified: true };
    }
    ```

    **Required in:**

    * ✅ **Webhooks** - No conversational context
    * ✅ **LuaJob (pre-defined)** - No conversational context
    * ✅ **Admin tools** - Managing other users

    **NOT needed in:**

    * ❌ **Tools** - Use `User.get()` without userId
    * ❌ **Jobs API (dynamic)** - Use `jobInstance.user()` instead
  </Tab>

  <Tab title="Dynamic Jobs (Special Case)">
    **Dynamic jobs have automatic user context:**

    ```typescript theme={null}
    import { Jobs } from 'lua-cli';

    // Creating a dynamic job from a tool
    await Jobs.create({
      name: 'user-reminder',
      execute: async (jobInstance) => {
        // ✅ Use jobInstance.user() - automatic context!
        const user = await jobInstance.user();
        
        await user.send([{
          type: 'text',
          text: 'Reminder: Your meeting starts in 5 minutes!'
        }]);
      }
    });
    ```

    **Why it works:** Dynamic jobs created from tools automatically capture the user context, so you don't need to provide a userId.
  </Tab>

  <Tab title="Email/Phone Lookup">
    **Look up a user by email or phone number:**

    ```typescript theme={null}
    import { User } from 'lua-cli';

    // In a webhook receiving customer contact info
    execute: async (event) => {
      const { email, phone } = event.body;
      
      // Look up by email
      const userByEmail = await User.get({ email: 'customer@example.com' });
      
      // Look up by phone (both formats work)
      const userByPhone = await User.get({ phone: '+1234567890' });
      const userByPhone2 = await User.get({ phone: '1234567890' });
      
      // Handle not found gracefully
      if (!userByEmail) {
        return { error: 'User not found' };
      }
      
      // Update the user's data
      userByEmail.routingEnabled = true;
      await userByEmail.save();
      
      return { 
        success: true, 
        userId: userByEmail._luaProfile.userId 
      };
    }
    ```

    **Use cases:**

    * ✅ **Webhooks** - External systems send email/phone, not userId
    * ✅ **Admin tools** - Operators search by contact info
    * ✅ **Integrations** - Third-party systems don't have Lua user IDs

    <Warning>
      **Returns `null` if not found:** Unlike `User.get(userId)` which throws on not found, email/phone lookup returns `null`. Always check for null before using the user object.
    </Warning>
  </Tab>
</Tabs>

## UserDataInstance API

### Property Access (Direct)

Access any user property directly:

```typescript theme={null}
// Reading properties
const name = user.name;
const email = user.email;
const preferences = user.preferences;
const customField = user.customField;

// Setting properties (local only - call update() to persist)
user.name = "Jane Doe";
user.email = "jane@example.com";
user.preferences = { theme: "dark" };

// Checking existence
if ('name' in user) {
  console.log('User has a name');
}
```

### update()

Update user data on the server and locally.

```typescript theme={null}
user.update(data: Record<string, any>): Promise<any>
```

<ParamField path="data" type="object" required>
  Object containing fields to update or add
</ParamField>

**Returns:** Promise resolving to updated sanitized user data

**Examples:**

```typescript theme={null}
// Update single field
await user.update({ name: "John Doe" });

// Update multiple fields
await user.update({
  name: "John Doe",
  email: "john@example.com",
  phone: "555-1234"
});

// Update nested objects
await user.update({
  preferences: {
    theme: "dark",
    notifications: true,
    language: "en"
  }
});

// Access updated data immediately
console.log(user.name);  // "John Doe"
console.log(user.preferences.theme);  // "dark"
```

### save()

Save the current state of user data to the server. This is a convenience method that persists all changes made to the user instance.

```typescript theme={null}
user.save(): Promise<boolean>
```

**Returns:** Promise resolving to `true` if successful

**Examples:**

```typescript theme={null}
const user = await User.get();

// Modify properties
user.name = "John Doe";
user.email = "john@example.com";
user.phone = "555-1234";

// Save all changes at once
await user.save();

// Much cleaner than multiple update calls!
```

<Note>
  **New in Latest Version:** The `save()` method provides a simpler workflow - modify properties then save, rather than passing data to `update()`.
</Note>

### send()

Send messages to the user conversation. Supports text, images, and file attachments.

```typescript theme={null}
user.send(messages: Message[]): Promise<any>
```

<ParamField path="messages" type="Message[]" required>
  Array of messages to send (text, image, or file)
</ParamField>

**Message Types:**

```typescript theme={null}
// Text message
type TextMessage = {
  type: "text";
  text: string;
};

// Image message
type ImageMessage = {
  type: "image";
  image: string;      // Base64 encoded image data
  mediaType: string;   // e.g., "image/png", "image/jpeg"
};

// File message
type FileMessage = {
  type: "file";
  data: string;       // Base64 encoded file data
  mediaType: string;   // e.g., "application/pdf"
};
```

**Examples:**

```typescript theme={null}
const user = await User.get();

// Send a text message
await user.send([
  { type: "text", text: "Your order has been shipped!" }
]);

// Send multiple messages
await user.send([
  { type: "text", text: "Here is your receipt:" },
  { 
    type: "image", 
    image: base64ImageData, 
    mediaType: "image/png" 
  }
]);

// Send a file
await user.send([
  { type: "text", text: "Your invoice is attached:" },
  {
    type: "file",
    data: base64PdfData,
    mediaType: "application/pdf"
  }
]);
```

<Note>
  **New in Latest Version:** Send proactive messages to users for notifications, updates, and automated workflows.
</Note>

### getChatHistory()

Retrieve the conversation history for the current user with the active agent. Returns the last 40 user/assistant messages, transformed for display: hidden text is filtered out and embedded media (audio, video, files) is surfaced as structured `content` parts you can render directly.

```typescript theme={null}
user.getChatHistory(): Promise<ChatHistoryMessage[]>
```

**Return shape:**

```typescript theme={null}
interface ChatHistoryMessage {
  id: string;
  role: 'user' | 'assistant';
  createdAt: string; // ISO-8601
  content: ChatHistoryContent[];
}

interface ChatHistoryContent {
  type: 'text' | 'image' | 'video' | 'audio' | 'file';
  // For text:
  text?: string;
  // For media (depending on type):
  image?: string;       // image URL or base64
  video?: string;       // video URL or base64
  data?: string;        // file/audio URL or base64
  mediaType?: string;   // e.g. "image/png", "audio/mpeg", "application/pdf"
}
```

**Example:**

```typescript theme={null}
const user = await User.get();
const history = await user.getChatHistory();

console.log(history.length); // Number of messages

for (const msg of history) {
  for (const part of msg.content) {
    if (part.type === 'text') {
      console.log(`${msg.role}: ${part.text}`);
    } else if (part.type === 'image') {
      console.log(`${msg.role} sent image (${part.mediaType})`);
    }
  }
}
```

### clear()

Clear all user data for the current user.

```typescript theme={null}
user.clear(): Promise<boolean>
```

**Returns:** Promise resolving to `true` if successful

**Example:**

```typescript theme={null}
try {
  const success = await user.clear();
  if (success) {
    console.log('User data cleared successfully');
  }
} catch (error) {
  console.error('Failed to clear user data:', error);
}
```

<Warning>
  **Destructive operation!** This removes all user data. Use with caution.
</Warning>

## Data Sanitization

UserDataInstance separates system identity from custom data:

**System identity (read-only via `_luaProfile`):**

* `userId`, `fullName`, `mobileNumbers`, `emailAddresses`
* Extracted from response and made immutable — access via `user._luaProfile`

**Custom data (read/write via direct properties):**

* `name`, `email`, `phone`
* Custom fields you set
* Preferences, settings, and any other data

```typescript theme={null}
const user = await User.get();

// System identity — read-only
user._luaProfile.userId;        // "12345"
user._luaProfile.fullName;      // "John Doe"

// Custom data — read/write
user.name;                      // "John Doe"
user.email;                     // "john@example.com"
user.preferences;               // { theme: "dark" }
```

## Complete Examples

### Example 1: User Preferences

```typescript theme={null}
import { LuaTool } from 'lua-cli';
import { z } from 'zod';

export class ManagePreferencesTool implements LuaTool {
  name = "manage_preferences";
  description = "Manage user preferences";
  
  inputSchema = z.object({
    theme: z.enum(['light', 'dark']).optional(),
    notifications: z.boolean().optional(),
    language: z.string().optional()
  });

  async execute(input: any) {
    const user = await User.get();

    // Direct property access
    const currentPrefs = user.preferences || {};
    
    // Merge with new preferences
    const newPrefs = {
      ...currentPrefs,
      ...input
    };
    
    // Update on server
    await user.update({ preferences: newPrefs });
    
    return {
      message: 'Preferences updated successfully',
      preferences: user.preferences  // Direct access to updated data
    };
  }
}
```

### Example 2: Shopping Cart Persistence

```typescript theme={null}
export class CartTool implements LuaTool {
  name = "manage_cart";
  description = "Manage shopping cart";
  
  inputSchema = z.object({
    action: z.enum(['add', 'remove', 'get']),
    productId: z.string().optional(),
    quantity: z.number().optional()
  });

  async execute(input: any) {
    const user = await User.get();

    // Get current cart (direct access)
    let cart = user.cart || [];
    
    if (input.action === 'add') {
      cart.push({
        productId: input.productId,
        quantity: input.quantity
      });
      await user.update({ cart });
    }
    
    if (input.action === 'remove') {
      cart = cart.filter(item => item.productId !== input.productId);
      await user.update({ cart });
    }
    
    return {
      cart: user.cart,  // Direct access
      itemCount: cart.length
    };
  }
}
```

### Example 3: User Profile Management

```typescript theme={null}
export class ProfileTool implements LuaTool {
  name = "update_profile";
  description = "Update user profile information";
  
  inputSchema = z.object({
    firstName: z.string().optional(),
    lastName: z.string().optional(),
    phone: z.string().optional(),
    bio: z.string().optional()
  });

  async execute(input: any) {
    const user = await User.get();

    // Update user data
    await user.update(input);
    
    // Return updated profile (direct access)
    return {
      success: true,
      profile: {
        firstName: user.firstName,
        lastName: user.lastName,
        phone: user.phone,
        bio: user.bio
      },
      message: 'Profile updated successfully'
    };
  }
}
```

### Example 4: Personalized Greeting

```typescript theme={null}
export class GreetUserTool implements LuaTool {
  name = "greet_user";
  description = "Greet user with personalized message";
  
  inputSchema = z.object({});

  async execute(input: any) {
    const user = await User.get();

    // Direct property access
    const hour = new Date().getHours();
    const timeGreeting = hour < 12 ? 'Good morning'
      : hour < 18 ? 'Good afternoon'
      : 'Good evening';
    
    return {
      message: `${timeGreeting}, ${user.name}! How can I help you today?`
    };
  }
}
```

### Example 5: Order Notification with Messaging

```typescript theme={null}
export class SendOrderNotificationTool implements LuaTool {
  name = "send_order_notification";
  description = "Send order status notification to user";
  
  inputSchema = z.object({
    orderId: z.string(),
    status: z.enum(['shipped', 'delivered']),
    trackingNumber: z.string().optional(),
    receiptUrl: z.string().optional()
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const user = await User.get();

    // Build notification message
    const messages = [
      {
        type: "text" as const,
        text: `Hi ${user.name}! Your order #${input.orderId} has been ${input.status}.`
      }
    ];
    
    // Add tracking info if shipped
    if (input.status === 'shipped' && input.trackingNumber) {
      messages.push({
        type: "text" as const,
        text: `Tracking number: ${input.trackingNumber}`
      });
    }
    
    // Send messages to user
    await user.send(messages);
    
    // Update user's notification preferences
    user.lastNotificationSent = new Date().toISOString();
    await user.save();
    
    return {
      success: true,
      message: "Notification sent successfully"
    };
  }
}
```

### Example 6: Send Receipt with Image

```typescript theme={null}
export class SendReceiptTool implements LuaTool {
  name = "send_receipt";
  description = "Send order receipt with QR code";
  
  inputSchema = z.object({
    orderId: z.string(),
    amount: z.number(),
    qrCodeBase64: z.string()
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const user = await User.get();

    // Send receipt with QR code
    await user.send([
      {
        type: "text",
        text: `Thank you for your order! Total: $${input.amount.toFixed(2)}`
      },
      {
        type: "text",
        text: "Here's your receipt QR code for easy access:"
      },
      {
        type: "image",
        image: input.qrCodeBase64,
        mediaType: "image/png"
      }
    ]);
    
    return {
      success: true,
      message: "Receipt sent to user"
    };
  }
}
```

## Best Practices

<AccordionGroup>
  <Accordion title="✅ Use Direct Property Access">
    ```typescript theme={null}
    // ✅ Recommended
    const name = user.name;
    const email = user.email;

    // 🟡 Still works but verbose
    const name = user.data.name;
    const email = user.data.email;
    ```
  </Accordion>

  <Accordion title="✅ Batch Updates">
    Combine multiple updates into one call:

    ```typescript theme={null}
    // ✅ Good - Single update call
    await user.update({
      name: "John Doe",
      email: "john@example.com",
      phone: "555-1234"
    });

    // ❌ Bad - Multiple update calls
    await user.update({ name: "John Doe" });
    await user.update({ email: "john@example.com" });
    await user.update({ phone: "555-1234" });
    ```
  </Accordion>

  <Accordion title="✅ Handle Missing Fields">
    Not all fields may be present:

    ```typescript theme={null}
    const phone = user.phone || 'Not provided';
    const name = user.name || 'Guest';
    const preferences = user.preferences || {};
    ```
  </Accordion>

  <Accordion title="✅ Use for Personalization">
    ```typescript theme={null}
    // Personalized responses
    return {
      message: `Welcome back, ${user.name}!`,
      savedItems: user.cart?.length || 0,
      lastVisit: user.lastSeen
    };
    ```
  </Accordion>

  <Accordion title="✅ Use save() for Multiple Changes">
    The new `save()` method is perfect for multiple changes:

    ```typescript theme={null}
    // ✅ Recommended - Modify then save
    user.name = "John Doe";
    user.email = "john@example.com";
    user.preferences = { theme: "dark" };
    await user.save();

    // 🟡 Still works - Update method
    await user.update({
      name: "John Doe",
      email: "john@example.com",
      preferences: { theme: "dark" }
    });
    ```
  </Accordion>

  <Accordion title="✅ Use send() for Proactive Notifications">
    Send messages to users for order updates, alerts, and more:

    ```typescript theme={null}
    // Send order shipped notification
    await user.send([
      { type: "text", text: "Your order has been shipped!" },
      { type: "text", text: "Tracking: TRACK123456" }
    ]);

    // Send with image
    await user.send([
      { type: "text", text: "Your boarding pass:" },
      { type: "image", image: qrCodeBase64, mediaType: "image/png" }
    ]);
    ```
  </Accordion>

  <Accordion title="⚠️ Message Format Requirements">
    Ensure correct message format:

    ```typescript theme={null}
    // ✅ Correct
    await user.send([
      { type: "text", text: "Hello!" }
    ]);

    // ✅ Multiple messages
    await user.send([
      { type: "text", text: "Message 1" },
      { type: "text", text: "Message 2" }
    ]);

    // ❌ Wrong - Must be array
    await user.send({ type: "text", text: "Hello!" });
    ```
  </Accordion>
</AccordionGroup>

## TypeScript Support

```typescript theme={null}
// User lookup options for email/phone lookup
interface UserLookupOptions {
  /** Email address to look up */
  email?: string;
  /** Phone number to look up (with or without + prefix) */
  phone?: string;
}

interface UserDataInstance {
  // Properties (dynamic based on stored data)
  name?: string;
  email?: string;
  phone?: string;
  [key: string]: any;
  
  // Methods
  update(data: Record<string, any>): Promise<any>;
  save(): Promise<boolean>;
  send(messages: Message[]): Promise<any>;
  clear(): Promise<boolean>;
  toJSON(): Record<string, any>;
}

// Message types
type TextMessage = {
  type: "text";
  text: string;
};

type ImageMessage = {
  type: "image";
  image: string;
  mediaType: string;
};

type FileMessage = {
  type: "file";
  data: string;
  mediaType: string;
};

type Message = TextMessage | ImageMessage | FileMessage;
```

### Usage with Types

```typescript theme={null}
// Define your user data shape
interface MyUserData {
  name: string;
  email: string;
  preferences: {
    theme: 'light' | 'dark';
    notifications: boolean;
  };
}

// Access with type safety
const user = await User.get();
const name: string = user.name;
const theme = user.preferences?.theme || 'light';
```

<Note title="Debugging tip">
  If user properties aren't what you expect, log the user object to see what's actually stored:

  ```typescript theme={null}
  const user = await User.get();
  console.log('User data:', JSON.stringify(user.data, null, 2));
  console.log('User profile fields:', JSON.stringify({ name: user.name, email: user.email }, null, 2));
  ```

  Then run `lua logs --type skill --limit 5` after a test message. See the [Debugging Skills guide](/cli/debugging) for the full workflow.
</Note>

## Next Steps

<CardGroup cols={2}>
  <Card title="Data API" icon="database" href="/api/data">
    Store custom user data
  </Card>

  <Card title="User Data Examples" icon="layer-group" href="/examples/user-data">
    See working examples
  </Card>

  <Card title="Debugging Skills" icon="bug" href="/cli/debugging">
    Inspect runtime return values
  </Card>
</CardGroup>
