The Developer Hub has been hidden from the menu. If you have previously created scripts that you need to adjust, you can do so by following this link.
The miniExtensions Developer Hub allows users to create powerful automations. By leveraging TypeScript, you can manipulate data, create custom interfaces, and extend the functionality of your Airtable bases. This guide will walk you through the various components of the scripting environment, providing detailed explanations and examples along the way.
Overview
Within the miniExtensions Developer Hub, you have access to several global objects and functions:
record
: The current Airtable record being processedcontext
: Information about the current script execution contextairtable
: Functions for interacting with Airtable, like fetching recordscache
: Functions for caching data across script runsstorage
: Functions for uploading temporary filesui
: Namespace for creating user interfaces for your scripts
We will go into each of these elements in the sections below. You can use the navigation on the right or the links above to jump straight to the corresponding sections! At the very end of this article you will find further sections on handling of Airtable field types, advanced techniques and best practices as well as a comprehensive example which combines many of the concepts explained in this guide.
The Record Object
The record
object represents the current Airtable record being processed. It provides methods to get and set field values.
Key Methods:
record.get(fieldName: string): any
Retrieves the value of a specified field
Throws an error if the field is not found
record.set(fieldName: string, value: any): void
Sets the value of a specified field
Throws an error if the field is not found or is a computed field
Properties:
record.id: string
The unique identifier of the record
Examples:
// Get the value of the 'Name' field
const name = record.get('Name');
// Set the value of the 'Status' field
record.set('Status', 'Completed');
// Get the record's ID
const recordId = record.id;
The Context Object
The context
object provides information about the current script execution context. It includes details about the table, view, and fields being used.
Properties:
context.tableId: string
The ID of the table used by the automation
context.viewId?: string
The ID of the view used by the automation (if any)
context.runId: string
A unique identifier for the current run of the automation
context.feedbackField?: string
The field used for feedback by the automation (if any)
context.fields: AirtableField[]
An array of all fields in the Airtable table
context.fieldsMap: Record<string, AirtableField>
A map of field names to their corresponding Airtable fields
Examples:
// Get the table ID
console.log(`Current table ID: ${context.tableId}`);
// Check if a view is being used
if (context.viewId) {
console.log(`Running in view: ${context.viewId}`);
}
// Access all fields in the table
const allFields = context.fields;
console.log(`Number of fields: ${allFields.length}`);
// Get a specific field
const nameField = context.fieldsMap['Name'];
console.log(`Name field type: ${nameField.type}`);
Airtable Object: Fetching Records
The airtable
object allows you to interact with your Airtable base, primarily for fetching additional records.
Key Method:
airtable.fetchRecords(tableId: string, options: AirtableSelectOptions): Promise<AirtableRecordsResult>
Fetches records from a specified table
Returns a promise that resolves to an object containing the fetched records and an optional offset
Examples:
async function fetchActiveUsers() {
try {
const result = await airtable.fetchRecords('tblUsers', {
fields: ['Name', 'Email', 'Status'],
filterByFormula: "Status='Active'",
maxRecords: 100
});
console.log(`Fetched ${result.records.length} active users`);
for (const user of result.records) {
console.log(`User: ${user.getCellValue('Name')}, Email: ${user.getCellValue('Email')}`);
}
if (result.offset) {
console.log('More records available. Use the offset for pagination.');
}
} catch (error) {
console.error('Error fetching records:', error);
}
}
Caching with the Cache Namespace
The cache
namespace provides functions for caching data across script runs, which can significantly improve performance for frequently accessed data.
Key Methods:
cache.get(key: string): Promise<string | null>
Retrieves the value of a cache key
cache.multipleGet(keys: string[]): Promise<(string | null)[]>
Retrieves values for multiple cache keys
cache.set(key: string, value: string, expiration?: number): Promise<void>
Sets the value of a cache key with an optional expiration time (in seconds)
cache.del(key: string): Promise<void>
Deletes a cache key
Examples:
async function cacheExample() {
// Set a cache value
await cache.set('lastUpdateTime', Date.now().toString(), 3600); // Expires in 1 hour
// Get a cache value
const lastUpdateTime = await cache.get('lastUpdateTime');
if (lastUpdateTime) {
console.log(`Last update was at: ${new Date(parseInt(lastUpdateTime))}`);
} else {
console.log('No previous update recorded');
}
// Get multiple cache values
const [value1, value2] = await cache.multipleGet(['key1', 'key2']);
// Delete a cache key
await cache.del('oldKey');
}
File Storage
The storage object allows you to upload temporary files, which can be useful for generating reports or processing data.
Key Method:
storage.uploadTempFile(filename: string, content: string | Buffer, mimeType: string): Promise<string>
Uploads a file to temporary storage
Returns a promise that resolves to the URL of the uploaded file
Example:
async function generateAndUploadReport() {
const reportContent = 'This is the content of the report...';
try {
const fileUrl = await storage.uploadTempFile('report.txt', reportContent, 'text/plain');
console.log(`Report uploaded successfully. URL: ${fileUrl}`);
// You can now use this URL to reference the file in your Airtable record
record.set('Report URL', fileUrl);
} catch (error) {
console.error('Error uploading file:', error);
}
}
Define User Inputs with the UI Namespace
The ui
namespace provides functions for creating user interfaces for your scripts. This can be used instead of the user input fields provided in the script settings and allows for more flexibility.
Key Functions:
ui.createField(type: string, propertyKey: string, config: object): MetaBulkRunnerUIFields
Creates a field for the UI
ui.createSection(name: string, config: { fields: MetaBulkRunnerUIFields[] }): MetaBulkRunnerUISection
Creates a section in the UI containing multiple fields
ui.buildSections(sections: MetaBulkRunnerUISection[]): object
Builds the final UI configuration from the defined sections
Field Types:
'text'
: For text input'number'
: For numeric input'airtableFields'
: For selecting Airtable fields'workspaceSecrets'
: For accessing workspace secrets
Example:
const sections = [
ui.createSection('User Details', {
fields: [
ui.createField('text', 'userName', {
title: 'User Name',
required: true
}),
ui.createField('number', 'userAge', {
title: 'User Age',
required: true
}),
ui.createField('airtableFields', 'fieldsToUpdate', {
title: 'Fields to Update',
inputOrOutput: 'input',
selectSingle: false
})
]
}),
ui.createSection('Additional Settings', {
fields: [
ui.createField('workspaceSecrets', 'apiKey', {
title: 'API Key',
required: true
})
]
})
];
const config = ui.buildSections(sections);
function run() {
const { userName, userAge, fieldsToUpdate, apiKey } = config;
// Use these values in your script...
}
Working with Airtable Fields
When working with Airtable fields, it's important to understand the different field types and how to interact with them.
Common Field Types:
singleLineText
multilineText
number
checkbox
singleSelect
multipleSelects
date
dateTime
attachment
multipleRecordLinks
Handling Different Field Types:
function handleField(fieldName: string) {
const fieldType = context.fieldsMap[fieldName].type;
const value = record.get(fieldName);
switch (fieldType) {
case 'number':
return value as number;
case 'checkbox':
return value as boolean;
case 'multipleSelects':
return (value as string[]).join(', ');
case 'date':
case 'dateTime':
return new Date(value as string).toLocaleString();
default:
return value as string;
}
}
Advanced Techniques and Best Practices
Error Handling: Always use try-catch blocks to handle potential errors gracefully.
try {
// Your code here
} catch (error) {
console.error('An error occurred:', error);
// Handle the error appropriately
}Asynchronous Operations: Use async/await for cleaner asynchronous code.
async function processRecord() {
try {
const result = await someAsyncOperation();
// Process the result
} catch (error) {
console.error('Error in async operation:', error);
}
}Modularization: Break your code into smaller, reusable functions for better maintainability.
Type Safety: Leverage TypeScript's type system to catch errors early and improve code quality.
interface UserData {
name: string;
age: number;
email: string;
}
function processUser(user: UserData) {
// Process user data
}Performance Optimization: Use caching for expensive operations and limit the number of API calls.
Consistent Naming: Use clear and consistent naming conventions for variables, functions, and UI elements.
Documentation: Add comments to explain complex logic or non-obvious decisions in your code.
Comprehensive Example: User Data Updater
Let's put everything together in a comprehensive example that updates user data and logs the changes, demonstrating the following concepts:
Creating a user interface for input
Using the cache to store and retrieve the last update time
Updating multiple fields in the record
Generating and uploading a report file
Error handling and logging
Happy scripting!
// Define UI
const sections = [
ui.createSection('User Update', {
fields: [
ui.createField('text', 'newStatus', {
title: 'New Status',
required: true
}),
ui.createField('airtableFields', 'fieldsToUpdate', {
title: 'Fields to Update',
inputOrOutput: 'input',
selectSingle: false
})
]
})
];
const config = ui.buildSections(sections);
// Helper function to get last update time
async function getLastUpdateTime(recordId: string): Promise<string | null> {
return await cache.get(`lastUpdate_${recordId}`);
}
// Helper function to set last update time
async function setLastUpdateTime(recordId: string): Promise<void> {
await cache.set(`lastUpdate_${recordId}`, new Date().toISOString(), 86400); // Cache for 24 hours
}
// Main function
async function run() {
const { newStatus, fieldsToUpdate } = config;
try {
// Check last update time
const lastUpdateTime = await getLastUpdateTime(record.id);
if (lastUpdateTime) {
console.log(`Last updated: ${lastUpdateTime}`);
}
// Update fields
for (const field of fieldsToUpdate) {
record.set(field, newStatus);
}
// Log the change
console.log(`Updated ${fieldsToUpdate.length} fields to "${newStatus}"`);
// Set new update time
await setLastUpdateTime(record.id);
// Generate and upload report
const reportContent = `User Update Report\\n\\nRecord ID: ${record.id}\\nUpdated Fields: ${fieldsToUpdate.join(', ')}\\nNew Status: ${newStatus}\\nUpdate Time: ${new Date().toISOString()}`;
const reportUrl = await storage.uploadTempFile('update_report.txt', reportContent, 'text/plain');
record.set('Last Update Report', reportUrl);
} catch (error) {
console.error('Error updating user data:', error);
}
}