Skip to content

Performance and Best Practices

Overview

This guide covers best practices for building efficient, maintainable Live Pages applications.

Performance Optimization

1. Use pt.get() for Single Entities

When you know the entity ID, always use pt.get() for the fastest retrieval:

// ✅ GOOD: Fast primary key lookup
const task = await pt.get(123);

// ❌ AVOID: Slower filtering when ID is known
const tasks = await pt.list({
    entityNames: ['task'],
    filters: { id: 123 }
});
const task = tasks[0];

2. Implement Server-Side Filtering

Always filter on the server rather than loading all data and filtering client-side:

// ✅ GOOD: Server-side filtering
const activeTasks = await pt.list({
    entityNames: ['task'],
    filters: { status: 'active' },
    limit: 50
});

// ❌ AVOID: Client-side filtering of large datasets
const allTasks = await pt.list({
    entityNames: ['task'],
    limit: 10000
});
const activeTasks = allTasks.filter(t => t.data.status === 'active');

3. Use Appropriate Operators

Choose the most efficient operator for your use case:

// ✅ GOOD: Use exact match when possible (fastest)
const task = await pt.list({
    entityNames: ['task'],
    filters: { status: 'active' }
});

// ✅ GOOD: Use $in for multiple values
const tasks = await pt.list({
    entityNames: ['task'],
    filters: { priority: { $in: ['high', 'medium'] } }
});

// ❌ AVOID: Unnecessary $or for same field
const tasks = await pt.list({
    entityNames: ['task'],
    filters: {
        $or: [
            { priority: 'high' },
            { priority: 'medium' }
        ]
    }
});

4. Cache Static Data

Cache data that doesn't change frequently:

// ✅ GOOD: Cache chat members at app initialization
let allMembers = [];

async function initApp() {
    allMembers = await pt.getChatMembers();
    await loadTasks();
}

function getMemberName(userId) {
    const member = allMembers.find(m => m.id === userId);
    return member ? member.name : 'Unknown';
}

// ❌ AVOID: Calling getChatMembers() repeatedly
async function displayTask(task) {
    const members = await pt.getChatMembers(); // Called for every task!
    const creator = members.find(m => m.id === task.creator_user_id);
    return creator.name;
}

5. Use Pagination

For large datasets, always implement pagination:

// ✅ GOOD: Load data in pages
const result = await pt.list({
    entityNames: ['task'],
    filters: { status: 'active' },
    page: 1,
    pageSize: 20,
    returnMetadata: true
});

// ❌ AVOID: Loading thousands of records at once
const allTasks = await pt.list({
    entityNames: ['task'],
    limit: 10000
});

6. Batch Operations

Use Promise.all() for parallel operations:

// ✅ GOOD: Parallel operations
async function batchUpdate(taskIds, updates) {
    const promises = taskIds.map(async id => {
        const task = await pt.get(id);
        return pt.edit(id, { ...task.data, ...updates });
    });

    await Promise.all(promises);
}

// ❌ AVOID: Sequential operations
async function slowBatchUpdate(taskIds, updates) {
    for (const id of taskIds) {
        const task = await pt.get(id);
        await pt.edit(id, { ...task.data, ...updates });
    }
}

7. Provide Immediate Feedback with Processing Status

For file uploads or long-running operations, create database rows immediately with a PROCESSING status to provide instant user feedback, then update them when processing completes.

Pattern: Add-Then-Update

// ✅ GOOD: Create PROCESSING row immediately, update when done
async function uploadFileWithFeedback(file) {
    // 1. Create row immediately - user sees it right away
    const created = await pt.add('document', {
        filename: file.name,
        status: 'PROCESSING',
        topic: null,
        document_id: null
    });

    try {
        // 2. Upload and process
        const formData = new FormData();
        formData.append('files', file);

        const msg = `Process this file and use 'chatdb_edit' with entity_id: ${created.id} to update the row with results.`;
        await pt.addMessage(formData, msg);

        // User will see PROCESSING status immediately, then SUCCESS after AI updates
    } catch (error) {
        // 3. Update to ERROR if something fails
        await pt.edit(created.id, {
            filename: file.name,
            status: 'ERROR',
            error_message: error.message
        });
    }
}

// ❌ AVOID: User waits with no feedback
async function uploadFileNoFeedback(file) {
    const formData = new FormData();
    formData.append('files', file);

    // User sees nothing until AI finishes processing
    const msg = `Process this file and use 'chatdb_add' to create a row.`;
    await pt.addMessage(formData, msg);
}

When to Use Each Approach

Upload-Then-Add (chatdb_add): - Goals and automation (chat, email) - Background tasks - No user waiting for feedback - Simpler with fewer failure modes

Add-Then-Update (pt.add + chatdb_edit): - Interactive Live Page uploads - User is actively waiting - Immediate feedback is critical - Dashboard/real-time applications

Comparison:

Aspect Upload-Then-Add Add-Then-Update
User Feedback Delayed Immediate
Operations 1 (add only) 2 (add + edit)
Complexity Simple More complex
Orphaned Rows None Possible
Best For Automation Interactive UIs

Best Practices for Add-Then-Update

1. Handle Cleanup on Failure:

try {
    const created = await pt.add('document', { status: 'PROCESSING' });
    await processDocument(created.id);
} catch (error) {
    // Option 1: Update to ERROR status
    await pt.edit(created.id, { status: 'ERROR', error: error.message });

    // Option 2: Delete orphaned row
    // await pt.delete(created.id);
}

2. Use Clear Status Values:

const STATUS = {
    PROCESSING: 'PROCESSING',  // Yellow badge, spinner
    SUCCESS: 'SUCCESS',        // Green badge, checkmark
    ERROR: 'ERROR'             // Red badge, x mark
};

3. Add Auto-Refresh:

// Refresh table every 10 seconds to show updated statuses
setInterval(loadData, 10000);

4. Show Processing Indicators:

function renderStatus(status) {
    if (status === 'PROCESSING') {
        return `<span class="text-yellow-600">⏳ Processing...</span>`;
    }
    if (status === 'SUCCESS') {
        return `<span class="text-green-600">✓ Complete</span>`;
    }
    return `<span class="text-red-600">✗ Error</span>`;
}

5. Use waitForMessageReceived for Real-Time Updates:

Instead of polling with setInterval, use pt.waitForMessageReceived() to get notified when AI processing completes:

async function uploadFileWithRealTimeUpdate(file) {
    // 1. Create PROCESSING row immediately
    const created = await pt.add('document', {
        filename: file.name,
        status: 'PROCESSING',
        topic: null,
        document_id: null
    });

    // 2. Refresh UI to show the new row
    await loadData();

    try {
        // 3. Upload and process
        const formData = new FormData();
        formData.append('files', file);

        const msg = `Process this file and use 'chatdb_edit' with entity_id: ${created.id} to update the row with results.`;
        const result = await pt.addMessage(formData, msg);

        // 4. Wait for AI to finish processing
        const response = await pt.waitForMessageReceived(result.task_id);
        console.log('AI finished processing:', response.id);

        // Refresh to show updated status
        await loadData();

    } catch (error) {
        await pt.edit(created.id, {
            filename: file.name,
            status: 'ERROR',
            error_message: error.message
        });
        await loadData();
    }
}

This approach is more efficient than polling because: - Updates appear immediately when AI completes (no 10-second delay) - No unnecessary API calls when nothing has changed - Works well with multiple concurrent uploads - Clean async/await syntax with proper error handling

8. Implement Debouncing

Debounce search inputs to reduce API calls:

let searchTimeout;

document.getElementById('searchInput').addEventListener('input', (e) => {
    clearTimeout(searchTimeout);
    const query = e.target.value.trim();

    if (query.length < 2) {
        clearResults();
        return;
    }

    searchTimeout = setTimeout(async () => {
        const results = await pt.list({
            entityNames: ['task'],
            filters: { text: { $contains: query } },
            limit: 20
        });
        displayResults(results);
    }, 300); // Wait 300ms after user stops typing
});

9. Cache Page Results

Implement caching for pagination:

const pageCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes

async function loadPageWithCache(page, filters) {
    const cacheKey = `${page}-${JSON.stringify(filters)}`;
    const cached = pageCache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
        return cached.data;
    }

    const result = await pt.list({
        entityNames: ['task'],
        filters: filters,
        page: page,
        pageSize: 20,
        returnMetadata: true
    });

    pageCache.set(cacheKey, {
        data: result,
        timestamp: Date.now()
    });

    return result;
}

Error Handling

1. Always Handle Errors

Wrap data operations in try-catch blocks:

async function robustOperation() {
    try {
        const entity = await pt.get(123);
        return entity;
    } catch (error) {
        console.error('Operation failed:', error);
        return null;
    }
}

2. Provide User Feedback

Show meaningful error messages to users:

async function addTask() {
    const text = document.getElementById('taskInput').value.trim();

    if (!text) {
        showError('Please enter a task description');
        return;
    }

    try {
        await pt.add('task', {
            text: text,
            completed: false
        });

        showSuccess('Task added successfully');
        await loadTasks();
    } catch (error) {
        console.error('Error adding task:', error);
        showError('Failed to add task. Please try again.');
    }
}

function showError(message) {
    const alert = document.createElement('div');
    alert.className = 'bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4';
    alert.textContent = message;
    document.getElementById('alerts').appendChild(alert);

    setTimeout(() => alert.remove(), 5000);
}

function showSuccess(message) {
    const alert = document.createElement('div');
    alert.className = 'bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4';
    alert.textContent = message;
    document.getElementById('alerts').appendChild(alert);

    setTimeout(() => alert.remove(), 3000);
}

3. Implement Fallback Strategies

Provide fallbacks when operations fail:

async function robustDataOperation() {
    try {
        const result = await pt.list({
            entityNames: ['task'],
            filters: { text: { $contains: 'important' } },
            limit: 50
        });

        return result;
    } catch (error) {
        console.error('Primary operation failed:', error);

        // Fallback to simpler query
        try {
            return await pt.list({
                entityNames: ['task'],
                limit: 20
            });
        } catch (fallbackError) {
            console.error('Fallback also failed:', fallbackError);
            return [];
        }
    }
}

Code Organization

1. Separate Concerns

Organize code into logical functions:

// ✅ GOOD: Separate concerns
async function loadTasks() {
    const entities = await fetchTasks();
    const tasks = filterTaskEntities(entities);
    displayTasks(tasks);
}

async function fetchTasks() {
    return await pt.list({
        entityNames: ['task'],
        filters: { completed: false }
    });
}

function filterTaskEntities(entities) {
    return entities.filter(e => e.entity_name === 'task');
}

function displayTasks(tasks) {
    document.getElementById('tasksList').innerHTML = tasks.map(renderTask).join('');
}

function renderTask(task) {
    return `
        <div class="task-card">
            <span>${task.data.text}</span>
            <button onclick="deleteTask(${task.id})">Delete</button>
        </div>
    `;
}

// ❌ AVOID: Everything in one function
async function doEverything() {
    const entities = await pt.list({ entityNames: ['task'] });
    const tasks = entities.filter(e => e.entity_name === 'task');
    document.getElementById('tasksList').innerHTML = tasks.map(t =>
        `<div><span>${t.data.text}</span><button onclick="deleteTask(${t.id})">Delete</button></div>`
    ).join('');
}

2. Use Meaningful Names

// ✅ GOOD: Clear variable names
const chatMembers = await pt.getChatMembers();
const humanUsers = chatMembers.filter(m => m.type === 'user');
const aiAgents = chatMembers.filter(m => m.type === 'agent');
const chatOwner = chatMembers.find(m => m.is_chat_owner);
const currentUser = chatMembers.find(m => m.is_logged_user);

// ❌ AVOID: Unclear names
const m = await pt.getChatMembers();
const u = m.filter(x => x.type === 'user');

3. Create Reusable Components

// Reusable task card renderer
function createTaskCard(task) {
    const card = document.createElement('div');
    card.className = 'bg-white rounded-lg shadow p-4 mb-2';

    const isCompleted = task.data.completed === "true";

    card.innerHTML = `
        <div class="flex items-center justify-between">
            <div class="flex items-center gap-3">
                <input
                    type="checkbox"
                    ${isCompleted ? 'checked' : ''}
                    onchange="toggleTask(${task.id})"
                    class="h-4 w-4"
                >
                <span class="${isCompleted ? 'line-through text-gray-500' : ''}">
                    ${escapeHtml(task.data.text)}
                </span>
            </div>
            <button onclick="deleteTask(${task.id})" class="text-red-500">
                Delete
            </button>
        </div>
    `;

    return card;
}

// Usage
function displayTasks(tasks) {
    const container = document.getElementById('tasksList');
    container.innerHTML = '';
    tasks.forEach(task => {
        container.appendChild(createTaskCard(task));
    });
}

Data Management Best Practices

1. Always Merge When Editing

// ✅ GOOD: Preserve existing fields
const task = await pt.get(taskId);
await pt.edit(taskId, {
    ...task.data,
    completed: true
});

// ❌ BAD: Lose all other fields
await pt.edit(taskId, { completed: true });

2. Validate Before Saving

async function addTask() {
    const text = document.getElementById('taskInput').value.trim();

    // Validate
    if (!text) {
        showError('Task description is required');
        return;
    }

    if (text.length > 500) {
        showError('Task description is too long (max 500 characters)');
        return;
    }

    // Save
    await pt.add('task', {
        text: text,
        completed: false
    });
}

3. Handle Missing Members Gracefully

function getMemberName(userId) {
    const member = allMembers.find(m => m.id === userId);
    return member ? member.name : 'Unknown User';
}

function displayTaskCreator(task) {
    const creator = allMembers.find(m => m.id === task.creator_user_id);

    if (!creator) {
        return '<span class="text-gray-400">Unknown</span>';
    }

    const icon = creator.type === 'user' ? '👤' : '🤖';
    return `${icon} ${creator.name}`;
}

4. Use Appropriate Limits

// ✅ GOOD: Reasonable limits
const recentTasks = await pt.list({
    entityNames: ['task'],
    limit: 50
});

// ❌ AVOID: Requesting too much data
const allTasks = await pt.list({
    entityNames: ['task'],
    limit: 10000
});

Using Goals with Live Pages

What are Goals?

Goals are automatic AI instructions that execute when specific conditions are met in a chat. When combined with Live Pages, Goals enable powerful automation workflows where data can be processed and stored in your database without manual intervention.

How Goals Work with Live Pages

When you set up a Goal in your chat settings, the AI automatically: 1. Detects when the goal condition is triggered (e.g., file upload, specific keywords) 2. Executes the instructions you've defined in the Goal 3. Can use chatdb tools to create/update/query database entities 4. Stores results that your Live Page can display and interact with

This creates a seamless integration between: - Direct chat interactions (uploading files, sending messages) - Email forwarding (files sent to chat email address) - API uploads (files uploaded programmatically via PrimeThink API) - Chat mentions (files uploaded when the chat is mentioned in other conversations) - AI processing (extraction, categorization, validation) - Database storage (structured data in entities) - Live Page display (visualization and interaction)

Common Use Cases for Goals with Live Pages

1. Document Processing

Goal Trigger: User uploads a PDF file
Goal Action: Extract key information, categorize, store in database
Live Page: Display categorized documents with search/filter

2. Email Automation

Goal Trigger: Email with attachments forwarded to chat
Goal Action: Extract data, create database records
Live Page: Show processed emails in dashboard

3. Data Entry Shortcuts

Goal Trigger: User sends message with specific format
Goal Action: Parse message, validate, store as entity
Live Page: Display and manage all entries

4. File Analysis

Goal Trigger: Invoice/receipt uploaded
Goal Action: Extract line items, amounts, vendors
Live Page: Financial dashboard showing all invoices

5. Content Categorization

Goal Trigger: Document uploaded
Goal Action: Analyze content, assign categories/tags
Live Page: Browse and filter by categories

6. API Integration

Goal Trigger: File uploaded via API
Goal Action: Process file, extract metadata, store in database
Live Page: Monitor API uploads with status tracking

7. Chat Mention Processing

Goal Trigger: Chat mentioned in another conversation with file attachment
Goal Action: Process file in context of mention, create record
Live Page: Show all processed mentions and their results

8. Multi-Channel Document Inbox

Goal Trigger: File uploaded via any channel (chat, email, API, chat mentions)
Goal Action: Unified processing regardless of source
Live Page: Single dashboard showing all documents from all channels

Best Practices for Writing Goal Prompts

1. Be Explicit About Tool Usage

Always specify which chatdb tool to use:

✅ GOOD: Use the tool 'chatdb_add' to create a new invoice record

❌ AVOID: Store the invoice information

2. Demand Actual Tool Execution (Not Simulation)

The AI might summarize or simulate tool calls instead of actually executing them if your instructions are too high-level. Be explicit that you want actual execution:

❌ BAD: High-level workflow that AI might just simulate
"Analyze the uploaded files and extract IP tasks. Use chatdb_list to check duplicates
by unique_key. If not found, use chatdb_add to insert the task."

Result: AI returns JSON summary but doesn't actually call the tools

✅ GOOD: Explicit tool execution instructions
"Analyze the uploaded files and extract IP tasks. For EACH task:
1. ACTUALLY CALL the tool 'chatdb_list' to check for duplicates by unique_key
2. If not found, ACTUALLY CALL the tool 'chatdb_add' to insert the task
3. Show all tool call results
4. At the end, return a JSON summary"

Result: AI actually executes chatdb_list and chatdb_add for each task

Key Phrases for Actual Execution: - "ACTUALLY CALL the tool 'chatdb_list'" - "ACTUALLY USE the tool 'chatdb_add'" - "For EACH task, use the tool..." - "Show all tool call results" - "Execute the following steps with tool calls"

Why This Matters: - Without explicit execution instructions, AI may treat your prompt as a conceptual workflow - AI might return a summary of what "would happen" instead of actually doing it - Your database won't be updated even though the response looks correct - This is especially critical for multi-step operations with loops

Real-World Example:

❌ AVOID: Conceptual instruction
"Extract tasks from the document. For each task, check if it exists using unique_key.
If not, add it to the database with these fields: [list]. Never modify existing tasks."

✅ PREFER: Explicit execution instruction
"Extract all tasks from the document. Then FOR EACH extracted task:

1. ACTUALLY CALL chatdb_list with filters: {unique_key: "<computed_hash>"}
2. If the list returns empty (no duplicate):
   - ACTUALLY CALL chatdb_add with entity_name 'ip_task' and data: {
       matter_reference: "...",
       application_number: "...",
       due_date: "YYYY-MM-DD",
       task_description: "...",
       unique_key: "<computed_hash>",
       source: "auto",
       source_document_id: <doc_id>
     }
3. If the list returns results (duplicate exists):
   - Skip this task

IMPORTANT: You must ACTUALLY EXECUTE chatdb_list and chatdb_add for each task.
Show the tool call results. Never modify existing tasks.

After processing all tasks, respond with JSON:
{
  "added": <number>,
  "skipped": <number>,
  "items": [{"application_number": "...", "due_date": "...", "task": "..."}]
}

Testing Your Instructions: 1. Run your instruction with a test file 2. Check if chatdb tools were actually called (look for tool execution in chat) 3. Verify database entities were actually created 4. If AI only returns a summary without tool calls, add "ACTUALLY CALL" phrases

3. Specify Entity Structure Clearly

Define exactly what fields to create:

Use the tool 'chatdb_add' with:
- entity_name: "invoice"
- data: {
    "invoice_number": "<extracted_number>",
    "vendor": "<extracted_vendor>",
    "amount": <extracted_amount>,
    "date": "<extracted_date>",
    "status": "pending"
  }

4. Handle Edge Cases

Account for scenarios where data might not be available:

If the document has extracted text:
  - Extract information and use 'chatdb_add' with status: "success"

If the document has no text or processing fails:
  - Use 'chatdb_add' with status: "pending" or "error"

5. Request Structured Responses

Ask for JSON responses for easier validation:

Respond with JUST JSON in this format:
[
  {
    "document_id": <id>,
    "extracted_field": "<value>",
    "status": "<success|error>"
  }
]

6. Create One Record Per Item

Be explicit about quantity:

✅ GOOD: You MUST call 'chatdb_add' exactly once per uploaded file.

❌ AVOID: Create records for each file.

7. Maintain Consistency

Use the same entity names and data structures across goals:

Always use:
- entity_name: "invoice" (not "invoices", "invoice_data", etc.)
- data.status: "pending" | "success" | "error" (consistent values)
- data.created_date: ISO format (consistent format)

Goal Example for Live Pages

Here's a complete example of a Goal that works with a Live Page:

Goal Trigger: If a user uploads a PDF or DOCX file

Goal Instructions:

AI Processing Request

You are given a file uploaded as an attachment to THIS message. Follow EXACTLY:

1) If the document has extracted text:
   - Analyze the content and extract key information
   - Use the tool 'chatdb_add' to create a database record:
     - entity_name: "document"
     - data: {
         "filename": "<actual filename>",
         "category": "<derived category>",
         "summary": "<brief summary, max 200 chars>",
         "document_id": <ID from message attachments>,
         "processing_status": "success"
       }

2) If the document has no text or processing fails:
   - Use the tool 'chatdb_add' to create a database record:
     - entity_name: "document"
     - data: {
         "filename": "<actual filename>",
         "category": "Uncategorized",
         "summary": "Processing pending",
         "document_id": <ID from message attachments>,
         "processing_status": "pending"
       }

3) Respond with JSON:
   {
     "status": "<success|pending|error>",
     "filename": "<filename>",
     "category": "<category>"
   }

Corresponding Live Page: - Displays all documents using pt.list({ entityNames: ['document'] }) - Shows category, summary, processing status - Allows filtering by category or status - Provides "View" button to see document text with pt.getDocumentText() - Shows "Re-process" button for pending items using pt.addMessage()

Benefits of Goals with Live Pages

Unified Experience: - Users can upload files via Live Page, chat message, email, API, or chat mentions - All uploads are processed consistently regardless of source - All results appear in the same Live Page interface - Single Goal handles all four upload channels

Automation: - No manual data entry required - AI handles extraction and categorization - Reduces human error - Zero-touch processing for API and automated uploads

Flexibility: - Live Page: Visual interface for interactive uploads - Chat: Conversational interface for manual uploads - Email: Integration with existing email workflows - API: Programmatic uploads for system integrations - Chat Mentions: Context-aware processing when the chat is mentioned in other conversations

Scalability: - Process single files or batch uploads - Same Goal handles all sources (chat, email, API, chat mentions) - Live Page adapts to any data volume - Supports high-throughput API integrations

Testing Goals with Live Pages

1. Test with Live Page Upload First: - Use pt.addMessage(formData, instructions) for uploads with AI processing - Or use pt.uploadFiles(formData, folder) for silent document uploads - Verify AI creates correct database entities (if using pt.addMessage) - Check that Live Page displays data correctly

2. Test Chat Upload: - Upload file directly in chat message - Verify Goal triggers and runs - Confirm same entity structure is created

3. Test Email Upload: - Forward email with attachment to chat email address - Verify Goal triggers automatically - Confirm consistent entity structure

4. Test API Upload: - Upload file programmatically via PrimeThink API - Verify Goal triggers for API uploads - Confirm API uploads create same entity structure

5. Test Chat Mention Upload: - Mention the chat in another conversation with file attachment - Verify Goal triggers when chat is mentioned - Confirm entity structure matches other channels

6. Test Edge Cases: - Upload file without text - Upload unsupported format - Upload very large file - Upload multiple files at once - Test each channel with edge cases

7. Verify Multi-Channel Consistency: - Compare entities from all four channels (Live Page, chat, email, API, chat mentions) - Ensure entity_name matches exactly across all sources - Confirm data structure is identical regardless of upload method - Verify all uploads appear correctly in Live Page

Goal + Live Page Checklist

  • Goal uses explicit chatdb tool names
  • Entity structure is clearly defined
  • Edge cases are handled (no text, errors)
  • Response format is specified (JSON recommended)
  • Entity names are consistent with Live Page queries
  • Data field names match what Live Page expects
  • Status values are well-defined and consistent
  • Goal tested with Live Page uploads
  • Goal tested with chat uploads
  • Goal tested with email uploads
  • Goal tested with API uploads
  • Goal tested with chat mention uploads
  • Live Page can display all possible status values
  • Error cases have retry mechanisms
  • All channels create identical entity structures

Security Best Practices

1. Escape User Input

Always escape HTML when displaying user-generated content:

function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// Usage
function renderTask(task) {
    return `
        <div class="task-card">
            <span>${escapeHtml(task.data.text)}</span>
        </div>
    `;
}

2. Validate Input Length

function validateTaskInput(text) {
    if (!text || text.trim().length === 0) {
        return { valid: false, error: 'Task description is required' };
    }

    if (text.length > 500) {
        return { valid: false, error: 'Task description is too long (max 500 characters)' };
    }

    return { valid: true };
}

async function addTask() {
    const text = document.getElementById('taskInput').value;
    const validation = validateTaskInput(text);

    if (!validation.valid) {
        showError(validation.error);
        return;
    }

    await pt.add('task', {
        text: text.trim(),
        completed: false
    });
}

3. Confirm Destructive Actions

async function deleteTask(taskId) {
    if (!confirm('Delete this task? This cannot be undone.')) {
        return;
    }

    try {
        await pt.delete(taskId);
        await loadTasks();
    } catch (error) {
        console.error('Error deleting task:', error);
        showError('Failed to delete task');
    }
}

UI/UX Best Practices

1. Show Loading States

async function loadTasks() {
    const loading = document.getElementById('loading');
    const tasksList = document.getElementById('tasksList');

    loading.style.display = 'block';
    tasksList.style.display = 'none';

    try {
        const entities = await pt.list({
            entityNames: ['task'],
            filters: { completed: false }
        });

        const tasks = entities.filter(e => e.entity_name === 'task');
        displayTasks(tasks);
    } finally {
        loading.style.display = 'none';
        tasksList.style.display = 'block';
    }
}

2. Provide Visual Feedback

async function addTask() {
    const button = document.getElementById('addButton');
    const originalText = button.textContent;

    // Show loading state
    button.disabled = true;
    button.textContent = 'Adding...';

    try {
        const text = document.getElementById('taskInput').value.trim();
        await pt.add('task', { text: text, completed: false });

        // Show success
        button.textContent = '✓ Added';
        document.getElementById('taskInput').value = '';

        await loadTasks();

        // Reset button after delay
        setTimeout(() => {
            button.textContent = originalText;
            button.disabled = false;
        }, 1000);
    } catch (error) {
        button.textContent = 'Error';
        button.disabled = false;

        setTimeout(() => {
            button.textContent = originalText;
        }, 2000);
    }
}

3. Handle Empty States

function displayTasks(tasks) {
    const container = document.getElementById('tasksList');

    if (tasks.length === 0) {
        container.innerHTML = `
            <div class="text-center py-12 text-gray-500">
                <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
                </svg>
                <p class="mt-2 text-sm">No tasks found</p>
                <button onclick="clearFilters()" class="mt-4 text-blue-500 text-sm">
                    Clear filters
                </button>
            </div>
        `;
        return;
    }

    container.innerHTML = tasks.map(renderTask).join('');
}

Troubleshooting

Common Issues

Issue: pt.list() returns empty array

Solutions: - Check that entity name is correct - Verify filters are valid - Try without filters to see if entities exist - Check browser console for errors

// Debug filters
async function debugFilters() {
    // Start simple
    console.log('All tasks:', await pt.list({ entityNames: ['task'] }));

    // Add filters incrementally
    console.log('Active tasks:', await pt.list({
        entityNames: ['task'],
        filters: { status: 'active' }
    }));
}

Issue: Data not updating after edit

Solutions: - Ensure you're merging with existing data - Check that you're calling loadTasks() after edit - Verify the edit was successful

// ✅ GOOD: Proper edit
const task = await pt.get(taskId);
await pt.edit(taskId, {
    ...task.data,
    completed: true
});
await loadTasks(); // Refresh display

Issue: Performance is slow

Solutions: - Implement pagination - Use server-side filtering - Cache static data - Reduce data transfer with appropriate limits

// ✅ GOOD: Optimized loading
const result = await pt.list({
    entityNames: ['task'],
    filters: { status: 'active' }, // Server-side filter
    page: 1,
    pageSize: 20, // Pagination
    returnMetadata: true
});

Testing Tips

1. Test with Empty Data

function displayTasks(tasks) {
    // Handle empty state
    if (!tasks || tasks.length === 0) {
        showEmptyState();
        return;
    }

    renderTasks(tasks);
}

2. Test with Large Datasets

// Test pagination with many items
async function testWithLargeDataset() {
    const tasks = await pt.list({
        entityNames: ['task'],
        page: 1,
        pageSize: 20,
        returnMetadata: true
    });

    console.log('Has more pages:', tasks.pagination.has_more);
    console.log('Count:', tasks.count);
}

3. Test Error Scenarios

async function testErrorHandling() {
    try {
        await pt.get(999999); // Non-existent ID
    } catch (error) {
        console.log('Error handled correctly:', error);
    }
}

Performance Checklist

  • Use pt.get() for single entity lookups
  • Implement server-side filtering
  • Use pagination for large datasets
  • Cache chat members and other static data
  • Implement debouncing for search inputs
  • Use batch operations with Promise.all()
  • Provide immediate feedback with PROCESSING status for uploads
  • Choose appropriate filter operators
  • Set reasonable limits on queries
  • Implement caching where appropriate
  • Show loading states
  • Handle errors gracefully
  • Validate user input
  • Escape HTML content
  • Test with empty and large datasets
  • Use Goals for unified upload experience (Live Page + Chat + Email + API + Chat Mentions)
  • Ensure Goal entity structure matches Live Page queries
  • Make Goal prompts explicit about chatdb tool usage
  • Use "ACTUALLY CALL" phrases to ensure tool execution (not simulation)
  • Verify in chat that tools were actually executed (not just summarized)
  • For multi-step operations, explicitly state "FOR EACH item, ACTUALLY CALL..."
  • Choose appropriate pattern: Upload-Then-Add for automation, Add-Then-Update for interactive UIs
  • Use waitForMessageReceived for clean async/await AI response handling
  • Use waitForDocumentReady only for silent uploads (uploadFiles) when you need extracted text
  • Remember: addMessage with attachments processes files immediately - no waitForDocumentReady needed
  • Use onDocumentChanged for batch upload progress tracking
  • Cache generated documents (PDF/DOCX) by entity ID to avoid regenerating on repeated requests

Document Processing Patterns

When to Use waitForDocumentReady

Understanding when you need waitForDocumentReady() depends on your upload method:

Upload Method AI Processes Immediately Need waitForDocumentReady
pt.addMessage(formData, message) ✅ Yes ❌ No
pt.uploadFiles(form) ❌ No ✅ Yes (for text extraction)

Key insight: When using addMessage() with attachments, files are sent directly to the AI with your message. The AI processes them immediately, so you only need waitForMessageReceived() - not waitForDocumentReady().

Upload with AI Analysis (No waitForDocumentReady Needed)

When you want AI to analyze uploaded files immediately:

async function uploadAndAnalyze(file, instructions) {
    const formData = new FormData();
    formData.append('files', file);

    // Files are sent with the message - AI processes them immediately
    const result = await pt.addMessage(formData, instructions);

    // Only wait for AI response - no waitForDocumentReady needed!
    const response = await pt.waitForMessageReceived(result.task_id, {
        timeout: 120000  // 2 minutes for complex analysis
    });

    return response.message;
}

// Usage
const analysis = await uploadAndAnalyze(
    invoiceFile, 
    'Extract line items, total, and vendor from this invoice'
);

Silent Upload with Text Extraction (waitForDocumentReady Required)

When uploading files silently (without AI) and you need the extracted text:

async function uploadAndExtractText(file) {
    const formData = new FormData();
    formData.append('files', file);

    // Silent upload - no AI processing
    const result = await pt.uploadFiles(formData);
    const docId = result.documents[0].id;

    // Must wait for document processing to complete
    try {
        const doc = await pt.waitForDocumentReady(docId, {
            timeout: 60000  // 1 minute
        });

        // Now safe to get extracted text
        const text = await pt.getDocumentText(docId);
        return { success: true, text: text.text };
    } catch (error) {
        return { success: false, error: error.message };
    }
}

Track Batch Upload Progress

For multiple file uploads with progress tracking (silent uploads):

async function uploadWithProgress(files) {
    const formData = new FormData();
    files.forEach(f => formData.append('files', f));

    const result = await pt.uploadFiles(formData);
    const docIds = result.documents.map(d => d.id);
    const total = docIds.length;

    return new Promise((resolve) => {
        const processed = new Set();

        const unsubscribe = pt.onDocumentChanged((doc) => {
            if (doc.status === 'Ready' || doc.status === 'Error') {
                processed.add(doc.id);
                updateProgressBar(processed.size, total);

                if (processed.size === total) {
                    unsubscribe();
                    resolve({ processed: processed.size });
                }
            }
        }, { documentIds: docIds });
    });
}

Cache Generated Documents by Entity ID

When generating PDFs, DOCX, or other files from entity data, save them with a predictable filename based on the entity ID. This allows you to check for cached versions before regenerating, saving processing time and API calls.

Pattern: 1. Save generated documents to a dedicated folder (e.g., /translations/) 2. Use a consistent naming convention: {type}_{entityId}.{format} 3. Before generating, check if a cached version exists using pt.getDocumentInfo() 4. If cached, download directly from the existing URL 5. If not cached, generate and save to the folder

// Configuration
const CACHE_FOLDER = '/translations';

/**
 * Get or generate a document for an entity
 * Returns cached version if available, otherwise generates new one
 */
async function getOrGenerateDocument(entityId, format = 'pdf') {
    const filename = `translation_${entityId}.${format}`;
    const filepath = `${CACHE_FOLDER}/${filename}`;

    // Check if cached version exists
    try {
        const docInfo = await pt.getDocumentInfo(filepath);

        if (docInfo && docInfo.document && docInfo.document.download_url) {
            console.log('Using cached document:', filename);
            return {
                cached: true,
                url: docInfo.document.download_url,
                documentId: docInfo.document.id
            };
        }
    } catch (error) {
        // Document doesn't exist, will generate new one
        console.log('No cached version found, generating new document');
    }

    // Generate new document
    const entity = await pt.get(entityId);
    const generatedDoc = await generateDocument(entity, format);

    // Save to cache folder
    const formData = new FormData();
    formData.append('files', generatedDoc.blob, filename);

    const result = await pt.uploadFiles(formData, CACHE_FOLDER);
    const savedDoc = result.documents[0];

    return {
        cached: false,
        url: savedDoc.download_url,
        documentId: savedDoc.id
    };
}

/**
 * Download button handler with caching
 */
async function handleDownload(entityId, format) {
    const downloadBtn = document.getElementById('downloadBtn');
    downloadBtn.disabled = true;
    downloadBtn.textContent = 'Preparing...';

    try {
        const result = await getOrGenerateDocument(entityId, format);

        // Trigger download
        const link = document.createElement('a');
        link.href = result.url;
        link.download = `translation_${entityId}.${format}`;
        link.click();

        if (result.cached) {
            showSuccess('Downloaded from cache');
        } else {
            showSuccess('Document generated and downloaded');
        }
    } catch (error) {
        showError('Failed to generate document: ' + error.message);
    } finally {
        downloadBtn.disabled = false;
        downloadBtn.textContent = 'Download';
    }
}

Benefits: - Faster downloads for repeated requests (no regeneration needed) - Reduced server load and API usage - Consistent file organization - Easy to find and manage generated documents

When to Use: - Translation exports - Report generation - Invoice/receipt PDFs - Any document generated from entity data that doesn't change frequently

Cache Invalidation: If entity data changes and you need to regenerate, delete the cached document first:

async function regenerateDocument(entityId, format = 'pdf') {
    const filename = `translation_${entityId}.${format}`;
    const filepath = `${CACHE_FOLDER}/${filename}`;

    // Delete cached version if exists
    try {
        const docInfo = await pt.getDocumentInfo(filepath);
        if (docInfo && docInfo.document) {
            await pt.deleteDocument(docInfo.document.id);
        }
    } catch (error) {
        // No cached version to delete
    }

    // Generate fresh document
    return await getOrGenerateDocument(entityId, format);
}

Downloading Documents Generated with pt.saveDocument()

When using pt.saveDocument() to generate PDF, DOCX, or other document formats, the function saves the document to PrimeThink's storage but doesn't automatically trigger a browser download. To enable automatic downloads, extract the download_url from the response and trigger the download manually.

Helper Functions:

// Extract download URL from various response structures
function extractDownloadUrl(result) {
    if (result?.result?.documents?.[0]?.download_url) return result.result.documents[0].download_url;
    if (result?.documents?.[0]?.download_url) return result.documents[0].download_url;
    if (result?.download_url) return result.download_url;
    // Fallback: construct relative URL from UUID (works within PrimeThink platform)
    if (result?.uuid) return `/api/v1/documents/uuid/${result.uuid}/download`;
    return null;
}

// Trigger browser download from URL
function triggerDownload(url, filename) {
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
}

Usage Example:

async function downloadAsPdf() {
    try {
        showToast('Generating PDF...', 'info');
        const filename = 'my-document.pdf';

        // Save document to PrimeThink storage
        const result = await pt.saveDocument(
            filename, 
            'PDF', 
            'application/pdf', 
            content, 
            'exports'
        );

        // Extract and trigger download
        const downloadUrl = extractDownloadUrl(result);
        if (downloadUrl) {
            triggerDownload(downloadUrl, filename);
            showToast('PDF downloaded', 'success');
        } else {
            showToast('PDF saved to documents', 'success');
        }
    } catch (error) {
        console.error('PDF generation failed:', error);
        showToast('Failed to generate PDF', 'error');
    }
}

Key Points: - pt.saveDocument() returns a result object containing the document's download_url - The response structure can vary, so extractDownloadUrl() checks multiple paths - Relative URLs work since Live Pages run within the PrimeThink platform - Use triggerDownload() to programmatically click a download link - Always provide a fallback message if the download URL isn't available

Next Steps