# Input

> Part of the NocoDB documentation (Workflows > Scripts > API Reference). Index of all pages: https://nocodb.com/llms.txt. Any docs page is available as Markdown by adding `.md` to its URL.

URL: https://nocodb.com/docs/workflows/scripts/api-reference/input
Last updated: 2026-09-25

Documentation for the input object and its methods in NocoDB Scripts

The `input` object provides methods for collecting user input during script execution. These methods allow you to create interactive scripts that can prompt users for information, choices, and file uploads.

## Overview

Input methods are asynchronous and pause script execution until the user provides a response. This enables you to:

* Collect text input from users
* Present users with choices using buttons or dropdown menus
* Allow users to select tables, views, fields, or records
* Collect file uploads for data processing

All input methods return a Promise that resolves to the value provided by the user, so they must be used with `await` to get the result.

## Available Methods

### input.textAsync()

Prompts the user to enter text.

**Parameters:**

* `label` (`string`): The prompt text to display to the user
* `options` (`object`, optional): Additional options for the text input
  * `defaultValue` (`string`, optional): Default value for the text input

**Returns:** `Promise<string>` - A promise that resolves to the text entered by the user

**Example:**

```javascript
// Ask for user's name
const name = await input.textAsync('What is your name?');
output.text(`Hello, ${name}!`);

// Ask for an email subject with a default value
const subject = await input.textAsync('Enter email subject:', {defaultValue: 'Monthly Newsletter'});
output.text(`Email subject: ${subject}`);

// Ask for user's name with a default
const userName = await input.textAsync('What is your name?', {defaultValue: 'Anonymous'});
output.text(`Hello, ${userName}!`);
```

**Output:**
<img alt="Text Input Example" src={__img0} placeholder="blur" />

### input.buttonsAsync()

Presents the user with buttons to choose from.

**Parameters:**

* `label` (`string`): The prompt text to display to the user
* `options` (`Array<string | ButtonOption>`): An array of options, either as strings or button option objects

**Button Option Properties:**

* `label` (`string`): Text to display on the button
* `value` (any): Value to return when the button is clicked (defaults to the label if using strings)
* `variant` (`'default' | 'primary' | 'secondary' | 'danger'`, optional): Visual style of the button

**Returns:** `Promise<any>` - A promise that resolves to the value of the selected option

**Example with simple strings:**

```javascript
// Ask user to choose a status
const status = await input.buttonsAsync(
  'Select a status for this record:',
  ['Active', 'Pending', 'Completed', 'Cancelled']
);
output.text(`Selected status: ${status}`);
```

**Output:**
<img alt="Button Input Example" src={__img1} placeholder="blur" />

**Example with button options:**

```javascript
// Ask for confirmation with styled buttons
const confirmation = await input.buttonsAsync(
  'Are you sure you want to delete these records?',
  [
    { label: 'Yes, delete them', value: true, variant: 'danger' },
    { label: 'Cancel', value: false, variant: 'secondary' }
  ]
);

if (confirmation) {
  output.text('Deleting records...');
  // Delete operation would go here
} else {
  output.text('Operation cancelled.');
}
```

**Output:**
<img alt="Button Input Example" src={__img2} placeholder="blur" />

### input.selectAsync()

Presents a dropdown menu of options for the user to choose from.

**Parameters:**

* `label` (`string`): The prompt text to display to the user
* `options` (`Array<string | { label: string, value: any }>`): An array of options, either as strings or objects with label and value properties
* `defaultValue` (any, optional): Default selected value

**Returns:** `Promise<any>` - A promise that resolves to the value of the selected option

**Example with simple strings:**

```javascript
// Ask user to select a department
const department = await input.selectAsync(
  'Select a department:',
  ['Marketing', 'Sales', 'Engineering', 'Customer Support', 'Finance', 'HR']
);
output.text(`Selected department: ${department}`);

// Ask user to select a department with a default value
const dept = await input.selectAsync(
  'Select a department:',
  ['Marketing', 'Sales', 'Engineering', 'Customer Support', 'Finance', 'HR'],
  'Engineering' // default value
);
output.text(`Selected department: ${dept}`);
```

**Output:**
<img alt="Select Input Example" src={__img3} placeholder="blur" />

**Example with label/value objects:**

```javascript
// Ask user to select a priority level
const priority = await input.selectAsync(
  'Select priority level:',
  [
    { label: 'High (Urgent)', value: 3 },
    { label: 'Medium (Normal)', value: 2 },
    { label: 'Low (When time permits)', value: 1 }
  ]
);
output.text(`Selected priority level: ${priority}`);

// Ask user to select a priority level with a default value
const priorityLevel = await input.selectAsync(
  'Select priority level:',
  [
    { label: 'High (Urgent)', value: 3 },
    { label: 'Medium (Normal)', value: 2 },
    { label: 'Low (When time permits)', value: 1 }
  ],
  2 // default value (Medium)
);
output.text(`Selected priority level: ${priorityLevel}`);
```

**Output:**
<img alt="Select Input Example" src={__img4} placeholder="blur" />

### input.fileAsync()

Prompts the user to upload a file.

**Parameters:**

* `label` (`string`, optional): The prompt text to display to the user (defaults to " --Select a file-- ")
* `options` (`object`, optional): Additional options for file upload:
  * `allowedFileTypes` (`Array<string>`): File extensions or MIME types to accept (e.g., '.csv', '.xlsx', 'application/json')
  * `hasHeaderRow` (`boolean`): For spreadsheets, whether to treat the first row as headers
  * `useRawValues` (`boolean`): For spreadsheets, whether to return raw string values vs. parsed types

**Returns:** `Promise<any>` - A promise that resolves to the parsed file contents. The data is automatically parsed from JSON if possible, otherwise returned as-is

**Example:**

```javascript
// Ask user to upload a CSV file
const result = await input.fileAsync('Please upload a CSV file of contacts', {
  allowedFileTypes: ['.csv'],
  hasHeaderRow: true
});

if (result && result.parsedContents) {
  const contacts = result.parsedContents;
  output.text(`Uploaded ${contacts.length} contacts.`);
  
  // Display the first contact as an example
  if (contacts.length > 0) {
    output.text('First contact:');
    output.table([contacts[0]]);
  }
}
```

**Output:**
<img alt="File Input Example" src={__img5} placeholder="blur" />

### input.tableAsync()

Prompts the user to select a table from the base.

**Parameters:**

* `label` (`string`): The prompt text to display to the user

**Returns:** `Promise<Table>` - A promise that resolves to the selected Table object

**Example:**

```javascript
// Ask user to select a table
const table = await input.tableAsync('Select a table to process:');
output.text(`Selected table: ${table.name}`);

// Get the table's fields
output.text(`Fields in ${table.name}:`);
for (const field of table.fields) {
  output.text(`- ${field.name} (${field.type})`);
}
```

**Output:**
<img alt="Table Input Example" src={__img6} placeholder="blur" />

### input.viewAsync()

Prompts the user to select a view from a specified table.

**Parameters:**

* `label` (`string`): The prompt text to display to the user
* `tableOrTableNameOrTableId` (`Table | string`): The table, table name, or table ID to select from

**Returns:** `Promise<View>` - A promise that resolves to the selected View object

**Example:**

```javascript
// First select a table
const table = await input.tableAsync('Select a table:');

// Then select a view from that table
const view = await input.viewAsync('Select a view from this table:', table);
output.text(`Selected view: ${view.name} (type: ${view.type})`);

// Query records from the view
const records = await view.selectRecordsAsync();
output.text(`This view contains ${records.records.length} records.`);
```

**Output:**
<img alt="View Input Example" src={__img7} placeholder="blur" />

### input.fieldAsync()

Prompts the user to select a field from a specified table.

**Parameters:**

* `label` (`string`): The prompt text to display to the user
* `tableOrTableNameOrTableId` (`Table | string`): The table, table name, or table ID to select from

**Returns:** `Promise<Field>` - A promise that resolves to the selected Field object

**Example:**

```javascript
// First select a table
const table = await input.tableAsync('Select a table:');

// Then select a field from that table
const field = await input.fieldAsync('Select a field to analyze:', table);
output.text(`Selected field: ${field.name} (${field.type})`);

// Now use the field for something
const records = await table.selectRecordsAsync({
  fields: [field.name]
});

// Count non-empty values in the field
let nonEmptyCount = 0;
for (const record of records.records) {
  if (record.getCellValue(field.name) !== null) {
    nonEmptyCount++;
  }
}

output.text(`The field "${field.name}" has values in ${nonEmptyCount} out of ${records.records.length} records.`);
```

**Output:**
<img alt="Field Input Example" src={__img8} placeholder="blur" />

### input.recordAsync()

Prompts the user to select a record from a table, view, or set of records.

**Parameters:**

* `label` (`string`): The prompt text to display to the user
* `source` (`Table | View | RecordQueryResult | Array<NocoDBRecord>`): The source to select from
* `options` (`object`, optional): Additional options:
  * `fields` (`Array<Field | string>`): Specific fields to display. Primary key and primary value fields are automatically included.

**Returns:** `Promise<NocoDBRecord | null>` - A promise that resolves to the selected NocoDBRecord object, or null if no record is selected

**Example:**

```javascript
// First select a table
const table = await input.tableAsync('Select a table:');

// Then select a record from that table
const record = await input.recordAsync('Select a record to update:', table);

if (record) {
  output.text(`Selected record: ${record.name}`);
  
  // Ask for a new value
  const newValue = await input.textAsync('Enter a new value:');
  
  // Update the record
  const fieldToUpdate = await input.fieldAsync('Select a field to update:', table);
  
  await table.updateRecordAsync(record.id, {
    [fieldToUpdate.name]: newValue
  });
  
  output.text(`Updated "${fieldToUpdate.name}" to "${newValue}".`);
} else {
  output.text('No record selected.');
}
```

**Output:**
<img alt="Record Input Example" src={__img9} placeholder="blur" />

## Practical Examples

### Form-Based Data Entry

```javascript
// Interactive form for creating a new project
async function createNewProject() {
  const projectsTable = base.getTable('Projects');
  const tasksTable = base.getTable('Tasks');
  
  output.markdown('# Create New Project');
  
  // Just the basics
  const projectName = await input.textAsync('Project Name:');
  const projectDescription = await input.textAsync('Description:');
  const dueDate = await input.textAsync('Due Date (YYYY-MM-DD):');
  
  // Add a few tasks
  output.markdown('## Add Tasks');
  const tasks = [];
  let addingTasks = true;
  
  while (addingTasks) {
    const taskName = await input.textAsync('Task Name:');
    tasks.push({ name: taskName });
    
    const addAnother = await input.buttonsAsync(
      'Add another task?',
      ['Yes', 'No, create project']
    );
    
    addingTasks = (addAnother === 'Yes');
  }
  
  // Create everything
  try {
    output.text('Creating project...');
    
    const projectId = await projectsTable.createRecordAsync({
      'Project Name': projectName,
      'Description': projectDescription,
      'Due Date': dueDate
    });
    
    // Create tasks
    for (const task of tasks) {
      await tasksTable.createRecordAsync({
        'Task Name': task.name,
        'Projects': [{id: projectId}],
        'Status': 'To Do'
      });
    }
    
    output.markdown('# ✅ Project Created!');
    output.text(`Created "${projectName}" with ${tasks.length} tasks`);
    
  } catch (error) {
    output.text(`Error: ${error.message}`);
  }
}

await createNewProject();
```

### File Upload and Processing

```javascript
// Example of using file upload to import data
async function importCSVData(): Promise<void> {
  try {
    // Step 1: Select destination table
    const table: Table = await input.tableAsync('Select a table to import data into:');
    
    // Step 2: Upload CSV file
    const result: any = await input.fileAsync('Upload CSV file:', {
      allowedFileTypes: ['.csv'],
      hasHeaderRow: true,
      useRawValues: false
    });
    
    if (!result || !result.parsedContents) {
      output.text('No file uploaded or file could not be parsed.');
      return;
    }
    
    const csvData: Record<string, any>[] = result.parsedContents;
    
    if (csvData.length === 0) {
      output.text('CSV file is empty.');
      return;
    }
    
    // Step 3: Show file preview
    output.markdown('# CSV Import Preview');
    output.text(`File contains ${csvData.length} rows and ${Object.keys(csvData[0] || {}).length} columns.`);
    
    if (csvData.length > 0) {
      output.text('Sample data (first 3 rows):');
      output.table(csvData.slice(0, 3));
    }
    
    // Step 4: Configure duplicate handling
    const duplicateHandling: 'skip' | 'update' | 'allow' | 'cancel' = await input.selectAsync(
      'How should duplicate records be handled?',
      [
        { label: 'Skip duplicates', value: 'skip' },
        { label: 'Update existing records', value: 'update' },
        { label: 'Allow duplicates', value: 'allow' },
        { label: '-- Cancel Import --', value: 'cancel' }
      ]
    );
    
    if (duplicateHandling === 'cancel') {
      output.text('Import cancelled by user.');
      return;
    }
    
    // Step 5: Select unique field if handling duplicates
    let uniqueField: Field | undefined;
    let uniqueColumnName: string | null = null;
    
    if (duplicateHandling !== 'allow') {
      // Create field options with cancel option
      const fieldOptions = [
        { label: '-- Cancel Import --', value: 'cancel' },
        ...table.fields
          .filter(field => !field.isComputed && !field.is_system_field)
          .map(field => ({
            label: `${field.name} (${field.type})`,
            value: field.name
          }))
      ];
      
      const selectedFieldName: string = await input.selectAsync(
        'Select a field to use for identifying duplicates:', 
        fieldOptions
      );
      
      if (selectedFieldName === 'cancel') {
        output.text('Import cancelled by user.');
        return;
      }      
      uniqueField = table.getField(selectedFieldName);

      if(uniqueField) {
      const csvColumns: string[] = Object.keys(csvData[0] || {});
      uniqueColumnName = csvColumns.find(col => 
        col.toLowerCase().trim() === uniqueField!.name.toLowerCase().trim()
      ) || null;
      
      if (!uniqueColumnName) {
        const availableColumns = [
          { label: '-- Cancel Import --', value: 'cancel' },
          ...csvColumns.map(col => ({ label: col, value: col }))
        ];
        
        uniqueColumnName = await input.selectAsync(
          `No CSV column matches "${uniqueField.name}". Select the CSV column to use:`,
          availableColumns
        );
        
        if (uniqueColumnName === 'cancel') {
          output.text('Import cancelled by user.');
          return;
        }
      }

      }
    }
    
    // Step 6: Map CSV columns to table fields
    output.markdown('## Field Mapping');
    output.text('Map each CSV column to a field in the table:');
    
    const csvColumns: string[] = Object.keys(csvData[0] || {});
    const fieldMap: Record<string, string> = {};
    
    // Get available fields (exclude computed and system fields)
    const availableFields: Field[] = table.fields.filter(field => 
      !field.isComputed && 
      !field.primary_key && 
      !field.is_system_field
    );
    
    for (const column of csvColumns) {
      // Find best match for auto-suggestion
      const bestMatchField: Field | undefined = availableFields.find(field => 
        field.name.toLowerCase().trim() === column.toLowerCase().trim()
      );
      
      const fieldOptions: Array<{ label: string; value: string }> = [
        { label: '-- Skip this column --', value: 'skip' },
        { label: '-- Cancel Import --', value: 'cancel' },
        ...availableFields.map(field => ({
          label: `${field.name} (${field.type})`,
          value: field.name
        }))
      ];
      
      let selectedField: string;
      if (bestMatchField) {
        // Auto-confirm perfect matches, but give option to change
        const autoConfirm: string = await input.buttonsAsync(
          `Auto-mapped: "${column}" → "${bestMatchField.name}". Accept this mapping?`,
          [
            { label: 'Accept', value: 'accept', variant: 'primary' },
            { label: 'Change Mapping', value: 'change', variant: 'secondary' },
            { label: 'Skip Column', value: 'skip', variant: 'secondary' },
            { label: 'Cancel Import', value: 'cancel', variant: 'danger' }
          ]
        );
        
        if (autoConfirm === 'cancel') {
          output.text('Import cancelled by user.');
          return;
        } else if (autoConfirm === 'accept') {
          selectedField = bestMatchField.name;
          output.text(`✓ Mapped: "${column}" → "${bestMatchField.name}"`);
        } else if (autoConfirm === 'skip') {
          selectedField = 'skip';
        } else {
          selectedField = await input.selectAsync(
            `Map CSV column "${column}" to:`,
            fieldOptions
          );
        }
      } else {
        selectedField = await input.selectAsync(
          `Map CSV column "${column}" to:`,
          fieldOptions
        );
      }
      
      if (selectedField === 'cancel') {
        output.text('Import cancelled by user.');
        return;
      } else if (selectedField && selectedField !== 'skip') {
        fieldMap[column] = selectedField;
      }
    }
    
    // Ensure at least one field is mapped
    if (Object.keys(fieldMap).length === 0) {
      output.text('No fields mapped. Import cancelled.');
      return;
    }
    
    // Step 7: Show mapping preview
    output.markdown('## Import Configuration');
    output.text('The following columns will be imported:');
    
    const mappingTable = Object.entries(fieldMap).map(([csvCol, fieldName]) => {
      const field = table.getField(fieldName);
      return {
        'CSV Column': csvCol,
        'Table Field': fieldName,
        'Field Type': field?.type || 'Unknown'
      };
    });
    
    output.table(mappingTable);
    
    // Step 8: Final confirmation
    const confirmation: boolean = await input.buttonsAsync(
      `Ready to import ${csvData.length} records with ${duplicateHandling} duplicate handling. Continue?`,
      [
        { label: 'Import Data', value: true, variant: 'primary' },
        { label: 'Cancel', value: false, variant: 'secondary' }
      ]
    );
    
    if (!confirmation) {
      output.text('Import cancelled.');
      return;
    }
    
    // Step 9: Begin import process
    output.markdown('# Importing Data');
    
    // Initialize counters
    let created: number = 0;
    let updated: number = 0;
    let skipped: number = 0;
    let failed: number = 0;
    let processed: number = 0;
    const failedRows: Array<{ Row: number | string; Error: string; 'Data Sample': string }> = [];
    
    const batchSize: number = 10;
    const totalBatches: number = Math.ceil(csvData.length / batchSize);
    
    // Process data in batches
    for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
      const startIndex: number = batchIndex * batchSize;
      const endIndex: number = Math.min(startIndex + batchSize, csvData.length);
      const batch: Record<string, any>[] = csvData.slice(startIndex, endIndex);
      
      // Prepare records for this batch
      const recordsToCreate: Record<string, any>[] = [];
      const recordsToUpdate: Array<{ id: string; fields: Record<string, any> }> = [];
      
      for (let rowIndex = 0; rowIndex < batch.length; rowIndex++) {
        const row: Record<string, any> = batch[rowIndex];
        const actualRowIndex: number = startIndex + rowIndex;
        
        try {
          // Transform CSV data according to field mapping and types
          const recordData: Record<string, any> = {};
          
          for (const [csvColumn, fieldName] of Object.entries(fieldMap)) {
            const rawValue: any = row[csvColumn];
            
            if (rawValue !== undefined && rawValue !== null && rawValue !== '') {
              const field: Field | undefined = table.getField(fieldName);
              if (field) {
                recordData[fieldName] = transformValue(rawValue, field);
              }
            }
          }
          
          // Skip empty records
          if (Object.keys(recordData).length === 0) {
            skipped++;
          } else {
            // Handle duplicates
            let shouldCreateRecord = true;
            
            if (duplicateHandling !== 'allow' && uniqueField && uniqueColumnName) {
              const uniqueValue: any = row[uniqueColumnName];
              
              if (uniqueValue !== undefined && uniqueValue !== null && uniqueValue !== '') {
                // Check for existing record using efficient server-side filtering
                const existingRecords: RecordQueryResult = await table.selectRecordsAsync({
                  fields: [uniqueField.name],
                  where: `(${uniqueField.name},eq,${uniqueValue})`,
                  pageSize: 1
                });
                
                if (existingRecords.records.length > 0) {
                  const existingRecord: NocoDBRecord = existingRecords.records[0];
                  
                  if (duplicateHandling === 'update') {
                    recordsToUpdate.push({
                      id: existingRecord.id,
                      fields: recordData
                    });
                    shouldCreateRecord = false;
                  } else if (duplicateHandling === 'skip') {
                    skipped++;
                    shouldCreateRecord = false;
                  }
                }
              }
            }
            
            // Add to creation queue if not handled as duplicate
            if (shouldCreateRecord) {
              recordsToCreate.push({fields: recordData});
            }
          }
          
        } catch (error: any) {
          console.error(`Error processing row ${actualRowIndex + 1}:`, error);
          failedRows.push({
            'Row': actualRowIndex + 1,
            'Error': error.message || String(error),
            'Data Sample': JSON.stringify(row).substring(0, 100) + '...'
          });
          failed++;
        }
      }
      
      // Execute batch operations
      try {
        // Bulk create new records
        if (recordsToCreate.length > 0) {
          const createdIds: string[] = await table.createRecordsAsync(recordsToCreate);
          created += createdIds.length;
        }
        
        // Bulk update existing records
        if (recordsToUpdate.length > 0) {
          await table.updateRecordsAsync(recordsToUpdate);
          updated += recordsToUpdate.length;
        }
        
      } catch (batchError: any) {
        console.error(`Error in batch ${batchIndex + 1}:`, batchError);
        failed += recordsToCreate.length + recordsToUpdate.length;
        
        // Log batch failure
        failedRows.push({
          'Row': `Batch ${batchIndex + 1}`,
          'Error': `Batch operation failed: ${batchError.message || String(batchError)}`,
          'Data Sample': `${recordsToCreate.length} creates, ${recordsToUpdate.length} updates`
        });
      }
      
      processed = endIndex;
    }
    
    // Step 10: Show final results
    output.markdown('# Import Complete! 🎉');
    
    const resultsTable: Array<{ Metric: string; Count: number }> = [
      { 'Metric': 'Total Records Processed', 'Count': csvData.length },
      { 'Metric': 'Records Created', 'Count': created },
      { 'Metric': 'Records Updated', 'Count': updated },
      { 'Metric': 'Records Skipped', 'Count': skipped },
      { 'Metric': 'Failed Records', 'Count': failed }
    ];
    
    output.table(resultsTable);
    
    // Show failed records if any
    if (failedRows.length > 0) {
      output.markdown('## Failed Records');
      output.text(`${failedRows.length} records failed to import. First 10 failures:`);
      output.table(failedRows.slice(0, 10));
    }
    
    // Success summary
    const successRate: number = Math.round(((created + updated + skipped) / csvData.length) * 100);
    output.text(`Import completed with ${successRate}% success rate.`);
    
  } catch (error: any) {
    console.error('Import failed:', error);
    output.text(`Import failed: ${error.message || String(error)}`);
  }
}

/**
 * Transform CSV value to appropriate type based on field definition
 * @param {any} value - Raw CSV value
 * @param {Field} field - NocoDB field definition
 * @returns {any} Transformed value
 */
function transformValue(value: unknown, field: Field) {
  if (value === null || value === undefined || value === '') {
    return null;
  }
  
  const stringValue = String(value).trim();
  
  switch (field.type) {
    case UITypes.Number:
    case UITypes.Decimal:
    case UITypes.Currency:
    case UITypes.Percent:
    case UITypes.Rating:
      const numValue = parseFloat(stringValue.replace(/[^\d.-]/g, ''));
      return isNaN(numValue) ? null : numValue;
      
    case UITypes.Checkbox:
      return ['true', '1', 'yes', 'on', 'checked'].includes(stringValue.toLowerCase());
      
    case UITypes.Time:
      // Try to parse time format
      const timeMatch = stringValue.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
      if (timeMatch) {
        const [, hours, minutes, seconds = '00'] = timeMatch;
        return `${hours.padStart(2, '0')}:${minutes}:${seconds}`;
      }
      return stringValue;
      
    case UITypes.Year:
      const yearValue = parseInt(stringValue);
      return (yearValue >= 1900 && yearValue <= 2100) ? yearValue : null;
      
    case UITypes.Duration:
      // Try to parse duration format (h:mm:ss or seconds)
      if (stringValue.includes(':')) {
        const parts = stringValue.split(':');
        if (parts.length >= 2) {
          const hours = parseInt(parts[0]) || 0;
          const minutes = parseInt(parts[1]) || 0;
          const seconds = parseInt(parts[2]) || 0;
          return hours * 3600 + minutes * 60 + seconds; // Convert to seconds
        }
      }
      // Try parsing as seconds
      const durationSeconds = parseFloat(stringValue);
      return isNaN(durationSeconds) ? null : durationSeconds;
      
    case UITypes.MultiSelect:
      // Split on common delimiters
      if (stringValue.includes(',')) {
        return stringValue.split(',').map(s => s.trim()).filter(s => s);
      } else if (stringValue.includes(';')) {
        return stringValue.split(';').map(s => s.trim()).filter(s => s);
      } else if (stringValue.includes('|')) {
        return stringValue.split('|').map(s => s.trim()).filter(s => s);
      }
      return [stringValue];
      
    case UITypes.SingleSelect:
      return stringValue;
      
    case UITypes.JSON:
      try {
        return JSON.parse(stringValue);
      } catch {
        return stringValue; // Keep as string if not valid JSON
      }
      
    case UITypes.Email:
      // Basic email validation
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      return emailRegex.test(stringValue) ? stringValue : null;
      
    case UITypes.URL:
      // Basic URL validation
      try {
        new URL(stringValue);
        return stringValue;
      } catch {
        // Try adding https:// if it looks like a domain
        if (stringValue.includes('.') && !stringValue.includes(' ')) {
          try {
            new URL(`https://${stringValue}`);
            return `https://${stringValue}`;
          } catch {
            return null;
          }
        }
        return null;
      }
      
    case UITypes.PhoneNumber:
      // Clean phone number - keep digits, +, parentheses, hyphens, spaces
      return stringValue.replace(/[^\d+()-\s]/g, '');
      
    case UITypes.GeoData:
      // Try to parse as lat,lng or JSON
      if (stringValue.includes(',')) {
        const [lat, lng] = stringValue.split(',').map(s => parseFloat(s.trim()));
        if (!isNaN(lat) && !isNaN(lng)) {
          return { latitude: lat, longitude: lng };
        }
      }
      try {
        const geoData = JSON.parse(stringValue);
        if (geoData.latitude !== undefined && geoData.longitude !== undefined) {
          return geoData;
        }
      } catch {
        // Not valid JSON
      }
      return null;
      
    case UITypes.Attachment:
      // For attachments, we can only handle URLs
      try {
        new URL(stringValue);
        return [{
          url: stringValue,
          title: stringValue.split('/').pop() || 'attachment'
        }];
      } catch {
        return null;
      }
      
    case UITypes.User:
      // Try to find user by email
      const collaborator = base.getCollaborator(stringValue);
      if (collaborator) {
        return [collaborator.id]; // Users are stored as arrays of IDs
      }
      return null;
    case UITypes.Barcode:
    case UITypes.QrCode:
      return { value: stringValue };
      
    case UITypes.SingleLineText:
    case UITypes.LongText:
    case UITypes.SpecificDBType:
    default:
      return stringValue;
  }
}

// Execute the import
await importCSVData();
```

### Interactive Record Selection and Update

```javascript
// Example of selecting and updating records
async function updateRecords() {
  // Select a table
  const table = await input.tableAsync('Select a table:');
  
  // Select a view (optional)
  const useView = await input.buttonsAsync(
    'Do you want to select a specific view?',
    ['Yes', 'No, use all records']
  );
  
  let records;
  
  if (useView === 'Yes') {
    const view = await input.viewAsync('Select a view:', table);
    records = await view.selectRecordsAsync();
    output.text(`Selected view "${view.name}" with ${records.records.length} records.`);
  } else {
    records = await table.selectRecordsAsync();
    output.text(`Selected table "${table.name}" with ${records.records.length} records.`);
  }
  
  // No records found
  if (records.records.length === 0) {
    output.text('No records found. Nothing to update.');
    return;
  }
  
  // Ask which field to update
  const field = await input.fieldAsync('Select a field to update:', table);
  
  // Check if field is computed
  if (field.isComputed) {
    output.text(`Cannot update computed field "${field.name}". Please select a different field.`);
    return;
  }
  
  // Different update strategies based on field type
  let newValue;
  
  switch (field.type) {
    case 'SingleSelect':
      // Get options from field
      const options = field.options?.choices?.map(choice => choice.title) || [];
      newValue = await input.selectAsync(`Select new value for "${field.name}":`, options);
      break;
      
    case 'MultiSelect':
      // Get options from field
      const multiOptions = field.options?.choices?.map(choice => choice.title) || [];
      
      // Let user select multiple options (workaround since there's no direct multi-select)
      const selectedOptions = [];
      let selecting = true;
      
      while (selecting) {
        const option = await input.selectAsync(
          `Select an option to add (${selectedOptions.length} selected so far):`,
          [
            ...multiOptions.filter(opt => !selectedOptions.includes(opt)),
            '-- Done selecting --'
          ]
        );
        
        if (option === '-- Done selecting --') {
          selecting = false;
        } else {
          selectedOptions.push(option);
          output.text(`Added "${option}". Selected: ${selectedOptions.join(', ')}`);
        }
      }
      
      newValue = selectedOptions;
      break;
      
    case 'Checkbox':
      newValue = await input.buttonsAsync(
        `Set "${field.name}" to:`,
        [
          { label: 'Checked', value: true },
          { label: 'Unchecked', value: false }
        ]
      );
      break;
      
    case 'Date':
    case 'DateTime':
      newValue = await input.textAsync(
        `Enter new date for "${field.name}" (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS):`
      );
      break;
      
    case 'Number':
    case 'Currency':
    case 'Percent':
    case 'Rating':
      const numValue = await input.textAsync(`Enter new value for "${field.name}":`);
      // Convert to number
      newValue = numValue ? parseFloat(numValue) : null;
      break;
      
    default:
      // Default to text input for most field types
      newValue = await input.textAsync(`Enter new value for "${field.name}":`);
  }
  
  // Ask which records to update
  const updateTarget = await input.selectAsync(
    'Which records do you want to update?',
    [
      { label: 'All records in the current view/table', value: 'all' },
      { label: 'Select specific records', value: 'select' },
      { label: 'Records matching certain criteria', value: 'filter' }
    ]
  );
  
  let recordsToUpdate = [];
  
  if (updateTarget === 'all') {
    // Update all records
    recordsToUpdate = records.records;
  } else if (updateTarget === 'select') {
    // Let user select records one by one
    const selectedIds = [];
    let selecting = true;
    
    // Show a simplified list of records for selection
    const recordOptions = records.records.map(record => ({
      label: record.name,
      value: record.id
    }));
    
    while (selecting && recordOptions.length > 0) {
      const record = await input.selectAsync(
        `Select a record to update (${selectedIds.length} selected so far):`,
        [
          ...recordOptions.filter(opt => !selectedIds.includes(opt.value)),
          { label: '-- Done selecting --', value: 'done' }
        ]
      );
      
      if (record === 'done') {
        selecting = false;
      } else {
        selectedIds.push(record);
        output.text(`Added record "${recordOptions.find(opt => opt.value === record)?.label}"`);
      }
    }
    
    // Get the full records for the selected IDs
    recordsToUpdate = records.records.filter(record => selectedIds.includes(record.id));
  } else if (updateTarget === 'filter') {
    // Let user select a field to filter on
    const filterField = await input.fieldAsync('Select a field to filter on:', table);
    
    // Get filter value
    const filterValue = await input.textAsync(`Enter value to match in "${filterField.name}":`);
    
    // Filter records (case-insensitive for strings)
    recordsToUpdate = records.records.filter(record => {
      const value = record.getCellValue(filterField.name);
      
      if (typeof value === 'string' && typeof filterValue === 'string') {
        return value.toLowerCase().includes(filterValue.toLowerCase());
      } else {
        return value == filterValue; // Use loose equality for other types
      }
    });
  }
  
  // Confirm the update
  output.markdown('# Update Preview');
  output.text(`You are about to update the field "${field.name}" to "${newValue}" for ${recordsToUpdate.length} records.`);
  
  if (recordsToUpdate.length > 0) {
    output.text('Records to be updated:');
    // Show first few records
    const previewRecords = recordsToUpdate.slice(0, 5).map(record => ({
      'Name': record.name,
      'Current Value': record.getCellValueAsString(field.name)
    }));
    
    output.table(previewRecords);
    
    if (recordsToUpdate.length > 5) {
      output.text(`...and ${recordsToUpdate.length - 5} more records.`);
    }
  }
  
  const confirmation = await input.buttonsAsync(
    'Proceed with the update?',
    [
      { label: 'Update Records', value: true, variant: 'primary' },
      { label: 'Cancel', value: false, variant: 'secondary' }
    ]
  );
  
  if (!confirmation) {
    output.text('Update cancelled.');
    return;
  }
  
  // Perform the update
  output.text(`Updating ${recordsToUpdate.length} records...`);
  
  let successCount = 0;
  let failCount = 0;
  
  for (const record of recordsToUpdate) {
    try {
      await table.updateRecordAsync(record.id, {
        [field.name]: newValue
      });
      successCount++;
    } catch (error) {
      console.error(`Error updating record ${record.id}:`, error);
      failCount++;
    }
  }
  
  // Report results
  output.markdown('# Update Complete');
  output.text(`Successfully updated ${successCount} records.`);
  
  if (failCount > 0) {
    output.text(`Failed to update ${failCount} records. Check the console for details.`);
  }
}

// Run the update function
await updateRecords();
```

## Best Practices

1. **Use descriptive prompts** - Provide clear instructions in prompt labels so users know what input is expected.

2. **Handle null/cancelled responses** - Always check if the user provided a value, especially for optional inputs.

3. **Validate user input** - Validate text inputs to ensure they meet your requirements before using them.

4. **Show confirmation for destructive actions** - Use buttons with confirmation prompts before performing actions that modify data.

5. **Provide progress updates** - Show progress information between input prompts, especially for multi-step processes.

6. **Use appropriate input methods** - Choose the right input method for the data type (e.g., buttons for boolean choices, select for enumerated values).

7. **Limit choices when possible** - Use select or buttons instead of free-form text when the valid inputs are known.

8. **Handle errors gracefully** - Catch and handle errors that might occur when processing user input.

9. **Provide default values** - When possible, suggest sensible defaults to make the script easier to use.

10. **Break complex workflows into steps** - For scripts with many inputs, organize them into logical sections with appropriate headings.

## Related

* [Output Methods](/docs/workflows/scripts/api-reference/output)
* [Table Object](/docs/workflows/scripts/api-reference/table)
* [View Object](/docs/workflows/scripts/api-reference/view)
* [Field Object](/docs/workflows/scripts/api-reference/field)
* [Record Object](/docs/workflows/scripts/api-reference/record)

---

## Related pages

- [Base](https://nocodb.com/docs/workflows/scripts/api-reference/base.md): Documentation for the base object in NocoDB Scripts
- [Cursor](https://nocodb.com/docs/workflows/scripts/api-reference/cursor.md): Documentation for the Cursor object in NocoDB Scripts
- [Table](https://nocodb.com/docs/workflows/scripts/api-reference/table.md): Documentation for the Table object in NocoDB Scripts
- [View](https://nocodb.com/docs/workflows/scripts/api-reference/view.md): Documentation for the View object in NocoDB Scripts
- [Field](https://nocodb.com/docs/workflows/scripts/api-reference/field.md): Documentation for the Field object and Field Types in NocoDB Scripts
- [RecordQueryResult](https://nocodb.com/docs/workflows/scripts/api-reference/record-query-result.md): Documentation for the RecordQueryResult object in NocoDB Scripts
- [Record](https://nocodb.com/docs/workflows/scripts/api-reference/record.md): Documentation for the NocoDBRecord object in NocoDB Scripts
- [Session](https://nocodb.com/docs/workflows/scripts/api-reference/session.md): Documentation for the Session object in NocoDB Scripts
