PrimeThink Documentation Help

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.uploadFiles(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.uploadFiles(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>`; }

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_owner); // ❌ 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. 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" }

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

4. 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>" } ]

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

6. 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.uploadFiles() with instructions

  • Verify AI creates correct database entities

  • 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)

  • [ ] Ensure Goal entity structure matches Live Page queries

  • [ ] Make Goal prompts explicit about chatdb tool usage

  • [ ] Choose appropriate pattern: Upload-Then-Add for automation, Add-Then-Update for interactive UIs

Next Steps

09 November 2025