Input
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
Returns: Promise<string>
- A promise that resolves to the text entered by the user
Example:
// Ask for user's name
const name = await input.textAsync('What is your name?');
output.text(`Hello, ${name}!`);
// Ask for an email subject
const subject = await input.textAsync('Enter email subject:');
output.text(`Email subject: ${subject}`);
input.buttonsAsync()
Presents the user with buttons to choose from.
Parameters:
label
(string
): The prompt text to display to the useroptions
(Array<string | ButtonOption>
): An array of options, either as strings or button option objects
Button Option Properties:
label
(string
): Text to display on the buttonvalue
(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:
// 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}`);
Example with button options:
// 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.');
}
input.selectAsync()
Presents a dropdown menu of options for the user to choose from.
Parameters:
label
(string
): The prompt text to display to the useroptions
(Array<string | { label: string, value: any }>
): An array of options, either as strings or objects with label and value properties
Returns: Promise<any>
- A promise that resolves to the value of the selected option
Example with simple strings:
// 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}`);
Example with label/value objects:
// 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}`);
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 headersuseRawValues
(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:
// 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]]);
}
}
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:
// 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})`);
}
input.viewAsync()
Prompts the user to select a view from a specified table.
Parameters:
label
(string
): The prompt text to display to the usertableOrTableNameOrTableId
(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:
// 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.`);
input.fieldAsync()
Prompts the user to select a field from a specified table.
Parameters:
label
(string
): The prompt text to display to the usertableOrTableNameOrTableId
(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:
// 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.`);
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 usersource
(Table | View | RecordQueryResult | Array<NocoDBRecord>
): The source to select fromoptions
(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:
// 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.');
}
Practical Examples
Form-Based Data Entry
// 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
// 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(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
// 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
-
Use descriptive prompts - Provide clear instructions in prompt labels so users know what input is expected.
-
Handle null/cancelled responses - Always check if the user provided a value, especially for optional inputs.
-
Validate user input - Validate text inputs to ensure they meet your requirements before using them.
-
Show confirmation for destructive actions - Use buttons with confirmation prompts before performing actions that modify data.
-
Provide progress updates - Show progress information between input prompts, especially for multi-step processes.
-
Use appropriate input methods - Choose the right input method for the data type (e.g., buttons for boolean choices, select for enumerated values).
-
Limit choices when possible - Use select or buttons instead of free-form text when the valid inputs are known.
-
Handle errors gracefully - Catch and handle errors that might occur when processing user input.
-
Provide default values - When possible, suggest sensible defaults to make the script easier to use.
-
Break complex workflows into steps - For scripts with many inputs, organize them into logical sections with appropriate headings.