# Write back to Airtable

Learn how to create, update, and delete records in your base and learn about permissions and asynchronous updates.

## Getting started

Interface extensions support updating existing records, creating new records, and deleting records.
All three operations are supported in both single and batch formats:

```js
// Update cell values in existing records
table.updateRecordAsync(record, {[textField.id]: 'updated value'});
table.updateRecordsAsync([
    {id: record1.id, fields: {[textField.id]: 'updated value 1'}},
    {id: record2.id, fields: {[textField.id]: 'updated value 2'}},
]);

// Create records
table.createRecordAsync();
table.createRecordAsync({[textField.id]: 'new value'});
table.createRecordsAsync([
    {fields: {[textField.id]: '1'}},
    {fields: {[textField.id]: '2'}},
    {fields: {[textField.id]: '3'}},
]);

// Delete records
table.deleteRecordAsync(record);
table.deleteRecordsAsync([record1, record2]);
```

Whenever you perform one of these actions, you should check whether the extension has
permission to perform the action first. In many cases, a particular write might not be
permitted. For example:

-   the toggles for "Edit records inline" or "Add/delete records inline" might not be
    turned on in the properties panel in interface designer
-   the user of the extension might only have read-only access to the interface
-   you could be trying to update a cell value in a field that cannot be updated (e.g. formula field)

An error will be thrown if you attempt to perform such a write. Use the
`hasPermissionTo[Action]` helpers to determine whether an action is possible:

```js
function shouldShowRecordCreationButton() {
    return table.hasPermissionToCreateRecord();
}

function isFieldValidForEditing(field) {
    // undefined can be used as a placeholder for unknown values (eg record being
    // edited, new cell value)
    return table.hasPermissionToUpdateRecord(undefined, {
        [field.id]: undefined,
    });
}
```

There are also `checkPermissionsFor[Action]` variants of each permission check that return an object which
not only has a `hasPermission` boolean, but also a `reasonDisplayString` if `hasPermission` is false:

```js
function updateRecordIfPossible(record, fieldValue) {
    const fieldsToUpdate = {
        'My field name': fieldValue,
    };
    const checkResult = table.checkPermissionsForUpdateRecord(record, fieldsToUpdate);

    if (checkResult.hasPermission) {
        table.updateRecordAsync(record, fieldsToUpdate);
    } else {
        // if the user does not have permission to perform this action, we
        // provide a message explaining why
        alert(`You could not update the record: ${checkResult.reasonDisplayString}`);
    }
}
```

That's all you need to get started! See the [API reference](https://airtable.com/developers/interface-extensions/api) for per-function
documentation and examples, or read on for more specific details about how writes work.

## Asynchronous updates

All write methods are asynchronous: they return a
[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
that resolves when the update has been persisted to Airtable's servers. (You'll notice there's a
`Saving...` spinner in the top left corner of the Airtable base while this is happening!)

However, we also optimistically apply updates locally: this means that (assuming use of `useRecords`
or similar to watch for updates) your **changes will be reflected within the extension and within your
base immediately**, before the updates are saved to Airtable's servers.

In other words: you'll see the changes immediately, but other users may not have received them yet.

This means that you can choose whether or not to wait for the update to be complete based on your
use case. For example, updating cell values may have side effects (e.g. if a formula field relies on
it, you need to wait for the update to be complete to get the updated values for the formula field).

The examples in our API documentation use
[async/await syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function),
but you can also use
[`.then`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then):

```js
async function createRecordWithAsyncAwait(fieldValue) {
    const recordId = await table.createRecordAsync({
        'My field name': fieldValue,
    });
    alert(`new record created! ID: ${recordId}`);
}

function createRecordWithCallback(record, fieldValue) {
    table.createRecordAsync({'My field name': fieldValue}).then(function (recordId) {
        alert(`new record created! ID: ${recordId}`);
    });
}
```

## Size limits & rate limits

Two size limits apply to writes:

-   Batch methods (`updateRecordsAsync`, `createRecordsAsync`, `deleteRecordsAsync`) may only
    update/create/delete up to 50 records in one call.
    -   Similarly, `globalConfig.setPathsAsync` can only set 50 paths in one call.
-   Any individual write cannot have a payload (e.g. `fields` content for `updateRecordAsync`)
    exceeding 1.9MB in size

An error will be thrown if these limits are exceeded.

Additionally, writes are rate-limited (maximum of 15 writes per second). Your extension will crash if
this limit is exceeded.

When performing writes to a lot of records, we recommend splitting your updates into batches of 50
and `await`ing individual calls to satisfy these limits:

```js
const BATCH_SIZE = 50;
async function deleteLotsOfRecords(records) {
    let i = 0;
    while (i < records.length) {
        const recordBatch = records.slice(i, i + BATCH_SIZE);
        // awaiting the delete means that next batch won't be deleted until the current
        // batch has been fully deleted, keeping you under the rate limit
        await table.deleteRecordsAsync(recordBatch);
        i += BATCH_SIZE;
    }
}
```

### Special behavior for updating records

For updating records in real time, `await`ing each write can appear laggy (for example, an input
field that syncs the user's typing to an Airtable record in real time).

In order to support these interactions, consecutive `updateRecordAsync` and `updateRecordsAsync`
calls to the same table within a short time period are _merged_ into one request. This means that
they won't be rate limited, as they count as one write request.

If the writes are too large to merge (would exceed the 50 record limit or the 1.9MB size limit) they
will not be merged, so we recommend only relying on this behavior for small updates.

## Updating specific field types

You can find the accepted cell write format for each field type in the [API reference for FieldType](https://airtable.com/developers/interface-extensions/api/fieldtype.md).

_**NOTE:**_ When updating array-type fields (attachment, linked record, multiple select, multiple
collaborators), you must set a new array of items. Whatever value you set will override the previous
value. If you wish to append to the existing values in the cell, you can spread the current cell's
items. For example:

```js
updateRecordAsync(recordToUpdate, {
    'Linked record field': [
        ...recordToUpdate.getCellValue('Linked record field'),
        {id: 'recABC123xyz'}
    ]
});
```

## Working with linked records

Linked record fields are one of the most powerful features in Airtable; there are some additional things to know when working with linked record fields in your interface extension. Let's start by looking at the properties panel.

### Properties panel configurations

The properties panel in interface designer allows you to configure some additional properties for linked record fields compared to other fields:

- **Selection constraints**: Linked record fields can have selection constraints that limit which records can be linked.
- **Permissions**: All fields can be configured to either be read-only or allow edits, but linked record fields can _additionally_ be configured to allow creating new records in the linked table or not.

### Linking to existing records

To link to existing records in the linked table, it is useful to leverage [Record#fetchForeignRecordsAsync](https://airtable.com/developers/interface-extensions/api/models/record.md#fetchForeignRecordsAsync). This method returns a list of records from the linked table that are available to be linked to the current record. The records returned by [Record#fetchForeignRecordsAsync](https://airtable.com/developers/interface-extensions/api/models/record.md#fetchForeignRecordsAsync) will adhere to the selection constraints set in the properties panel.

Pass a filter string to narrow down the available records. The method will return the same set of records unless you change the filter string, which enables you to build an autocomplete UI to allow a user to select a record, like the experience in a native List element shown here:

![ ](https://static.airtable.com/blocks/images/autocomplete_ux_example.gif)

Here's an example of how to implement this pattern:

```jsx
const [filterString, setFilterString] = useState('');
const [availableForeignRecords, setAvailableForeignRecords] = useState([]);

useEffect(() => {
    // Debounce the fetchForeignRecordsAsync call with a 300ms delay
    const timeoutId = setTimeout(() => {
        record.fetchForeignRecordsAsync(filterString).then((response) =>
            setAvailableForeignRecords(response.records)
        );
    }, 300);
    return () => clearTimeout(timeoutId);
}, [record, filterString]);

// ...

<Combobox>
    <ComboboxInput value={filterString} onChange={setFilterString}>
    <ComboboxOptions>
        {availableForeignRecords.map((foreignRecord) => (
            <ComboboxOption
                key={foreignRecord.id}
                onSelect={async () => {
                    await table.updateRecordAsync(record, {
                        'Linked record field': [
                            ...recordToUpdate.getCellValue('Linked record field'),
                            foreignRecord
                        ]
                    });
                }}
            >
                {foreignRecord.name}
            </ComboboxOption>
        ))}
    </ComboboxOptions>
</Combobox>
```

### Creating new records in the linked table
Sometimes, instead of linking to an existing record in the linked table, you may want to create a new record in the linked table and immediately link to it. This is done by providing a `name` property and no `id`:

```js
// Create a new record in the linked table and link to it
table.updateRecordAsync(record, {
    'Linked record field': [{name: 'New linked record name'}]
});

// Create multiple new records and link to them
table.updateRecordAsync(record, {
    'Linked record field': [
        {name: 'First new record'},
        {name: 'Second new record'}
    ]
});
```

As always, you should be checking `table.hasPermissionToUpdateRecord(..)` or `table.checkPermissionsForUpdateRecord(..)` before attempting to call `table.updateRecordAsync(..)`. If the toggle for "Enable record creation" on the linked record field is off and you pass a cell value that would create a new record in the linked table, the permission check will inform you that it will not be allowed.


## Working across multiple tables

### Setting up multiple tables with custom properties

The recommended way to work with multiple tables is to use [custom properties](https://airtable.com/developers/interface-extensions/guides/builders-custom-properties.md) to let builders configure (within Interface Designer) which tables your extension will use:

```js
import {useCustomProperties} from '@airtable/blocks/interface/ui';

function getCustomProperties(base) {
    return [
        {
            key: 'projectsTable',
            label: 'Projects Table',
            type: 'table',
            defaultValue: base.tables.find((table) => table.name.toLowerCase().includes('projects')),
        },
        {
            key: 'tasksTable',
            label: 'Tasks Table',
            type: 'table',
            defaultValue: base.tables.find((table) => table.name.toLowerCase().includes('tasks')),
        },
    ];
}

function MyExtension() {
    const {customPropertyValueByKey} = useCustomProperties(getCustomProperties);
    const projectsTable = customPropertyValueByKey.projectsTable;
    const tasksTable = customPropertyValueByKey.tasksTable;

    // Now you can work with both tables...
}
```

### Writing to multiple tables

Once you have references to multiple tables, you can perform write operations on each table independently. The same write methods (`createRecordAsync`, `updateRecordAsync`, `deleteRecordAsync`, and their batch variants) work the same way on all tables:

```js
async function createProjectWithTasks(projectName, taskNames) {
    // Create a record in the projects table
    const projectRecordId = await projectsTable.createRecordAsync({
        [projectNameField.id]: projectName,
    });

    // Create multiple records in the tasks table
    const taskRecords = taskNames.map(taskName => ({
        fields: {
            [taskNameField.id]: taskName,
            [projectLinkField.id]: [{id: projectRecordId}], // Link back to the project
        },
    }));

    await tasksTable.createRecordsAsync(taskRecords);
}
```

### Batch operations across multiple tables

When performing batch operations across multiple tables, the [size limits](#size-limits--rate-limits) discussed above apply **per table** (50 records per batch, 1.9MB per write), but the rate limit (15 writes per second) is global across all tables. You can optimize performance by processing multiple tables in parallel while still respecting these limits:

```js
async function batchUpdateMultipleTables(projectUpdates, taskUpdates) {
    const BATCH_SIZE = 50;

    // Process updates for each table
    async function processBatches(table, updates) {
        for (let i = 0; i < updates.length; i += BATCH_SIZE) {
            const batch = updates.slice(i, i + BATCH_SIZE);
            await table.updateRecordsAsync(batch);
        }
    }

    // Process both tables in parallel
    await Promise.all([
        processBatches(projectsTable, projectUpdates),
        processBatches(tasksTable, taskUpdates),
    ]);
}
```

**Important**: While you can write to multiple tables in parallel, be mindful that the rate limit of 15 writes per second applies to your extension as a whole, regardless of which table(s) you're writing to. If you're making many writes to multiple tables simultaneously, you may need to add throttling to avoid hitting this limit.

### Permission checks for multiple tables

Each table can have different permission settings. Always check permissions for each table independently:

```js
function canCreateInBothTables() {
    const canCreateProject = projectsTable.hasPermissionToCreateRecord();
    const canCreateTask = tasksTable.hasPermissionToCreateRecord();

    if (!canCreateProject) {
        alert('You do not have permission to create projects');
        return false;
    }

    if (!canCreateTask) {
        alert('You do not have permission to create tasks');
        return false;
    }

    return true;
}

async function createWithPermissionChecks() {
    if (canCreateInBothTables()) {
        await projectsTable.createRecordAsync({...});
        await tasksTable.createRecordAsync({...});
    }
}
```
