Skip to main content

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.

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.
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.

Core User Identity

Access read-only data like user._luaProfile.userId and user._luaProfile.fullName.

Persistent Storage

Store any data on the user object — onboarding state, workflow progress, preferences, cart contents. Persists across all conversations and sessions.

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.
// ✅ 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.
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.

Onboarding State Machine Example

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

PatternWhat to store on user.*Example
Onboarding flowonboardingStep, completedSteps, collectedDataTrack which step the user is on, resume across sessions
Multi-step formformData, currentSection, validationErrorsAccumulate form data across multiple tool calls
Verification workflowverificationStatus, documentsUploaded, verifiedAtTrack identity/document verification progress
Feature adoptionfeaturesUsed, tutorialStep, firstActionAtGuide users through product discovery
Conversation contextlastIntent, pendingAction, conversationTopicHelp the agent maintain context between sessions

Features

Direct Access

Access properties with user.name instead of user.data.name

Auto Sanitization

Removes sensitive fields automatically

Built-in Methods

update(), save(), send(), and clear() included

Messaging

Send text, images, and files to users

get(identifier?)

Retrieve user data as a UserDataInstance. Supports lookup by userId, email, or phone number.
User.get(identifier?: string | UserLookupOptions): Promise<UserDataInstance | null>
identifier
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)
Returns: UserDataInstance with proxy-based property access, or null if user not found (for email/phone lookup)
New in Latest Version: Look up users by email or phone! This is especially useful in webhooks where you receive contact info from external systems but don’t have the internal userId.

When to Use userId Parameter

Understanding when userId is required vs optional:
ContextMethodIdentifier Required?Why
ToolsUser.get()❌ OptionalHas conversational context
WebhooksUser.get(userId) or User.get({ email })REQUIREDNo conversational context
LuaJobUser.get(userId) or User.get({ phone })REQUIREDNo conversational context
Jobs APIjobInstance.user()❌ N/AAutomatic user context
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!
New! In webhooks, you can now look up users by email or phone if you don’t have the userId:
const user = await User.get({ email: '[email protected]' });
const user = await User.get({ phone: '+1234567890' });
Examples:
Get the current user from conversation context:
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);   // "[email protected]"
  console.log(user.phone);   // "555-0123"
  
  return { userName: user.name };
}

UserDataInstance API

Property Access (Direct)

Access any user property directly:
// 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 = "[email protected]";
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.
user.update(data: Record<string, any>): Promise<any>
data
object
required
Object containing fields to update or add
Returns: Promise resolving to updated sanitized user data Examples:
// Update single field
await user.update({ name: "John Doe" });

// Update multiple fields
await user.update({
  name: "John Doe",
  email: "[email protected]",
  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.
user.save(): Promise<boolean>
Returns: Promise resolving to true if successful Examples:
const user = await User.get();

// Modify properties
user.name = "John Doe";
user.email = "[email protected]";
user.phone = "555-1234";

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

// Much cleaner than multiple update calls!
New in Latest Version: The save() method provides a simpler workflow - modify properties then save, rather than passing data to update().

send()

Send messages to the user conversation. Supports text, images, and file attachments.
user.send(messages: Message[]): Promise<any>
messages
Message[]
required
Array of messages to send (text, image, or file)
Message Types:
// 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:
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"
  }
]);
New in Latest Version: Send proactive messages to users for notifications, updates, and automated workflows.

getChatHistory()

Retrieve the conversation history for the current user.
user.getChatHistory(): Promise<ChatHistoryMessage[]>
Returns: Promise resolving to an array of chat history messages Example:
const user = await User.get();
const history = await user.getChatHistory();
console.log(history.length); // Number of messages

clear()

Clear all user data for the current user.
user.clear(): Promise<boolean>
Returns: Promise resolving to true if successful Example:
try {
  const success = await user.clear();
  if (success) {
    console.log('User data cleared successfully');
  }
} catch (error) {
  console.error('Failed to clear user data:', error);
}
Destructive operation! This removes all user data. Use with caution.

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
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;                     // "[email protected]"
user.preferences;               // { theme: "dark" }

Complete Examples

Example 1: User Preferences

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

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

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

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

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

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

// ✅ Recommended
const name = user.name;
const email = user.email;

// 🟡 Still works but verbose
const name = user.data.name;
const email = user.data.email;
Combine multiple updates into one call:
// ✅ Good - Single update call
await user.update({
  name: "John Doe",
  email: "[email protected]",
  phone: "555-1234"
});

// ❌ Bad - Multiple update calls
await user.update({ name: "John Doe" });
await user.update({ email: "[email protected]" });
await user.update({ phone: "555-1234" });
Not all fields may be present:
const phone = user.phone || 'Not provided';
const name = user.name || 'Guest';
const preferences = user.preferences || {};
// Personalized responses
return {
  message: `Welcome back, ${user.name}!`,
  savedItems: user.cart?.length || 0,
  lastVisit: user.lastSeen
};
The new save() method is perfect for multiple changes:
// ✅ Recommended - Modify then save
user.name = "John Doe";
user.email = "[email protected]";
user.preferences = { theme: "dark" };
await user.save();

// 🟡 Still works - Update method
await user.update({
  name: "John Doe",
  email: "[email protected]",
  preferences: { theme: "dark" }
});
Send messages to users for order updates, alerts, and more:
// 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" }
]);
Ensure correct message format:
// ✅ 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!" });

TypeScript Support

// 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

// 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';
If user properties aren’t what you expect, log the user object to see what’s actually stored:
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 for the full workflow.

Next Steps

Data API

Store custom user data

User Data Examples

See working examples

Debugging Skills

Inspect runtime return values