+1 (415) 612-6492

Building a Custom Airtable Extension with the Blocks SDK: Local Dev, Batched Writes, and Release

Airtable's Interface Designer and Omni cover most of what teams need on top of a base. But every so often a client asks for something the built-in surfaces genuinely cannot do: a seating-plan canvas, a route optimiser that draws on a map, a bulk reconciliation screen that edits 400 records in one pass. That is where custom extensions come in: React apps, written by you, that run inside the Airtable UI with full access to the base they are installed in.

This tutorial walks through building and shipping one, and — just as importantly — sets out when you should not.

Extension, Interface, or script?

Work down this list and stop at the first thing that fits:

  1. Interface Designer. Role-based layouts, record detail pages, dashboards, buttons that trigger automations. No code, no maintenance.
  2. An automation script. Server-side logic that runs on a trigger or a button press. Great for batch operations with no UI.
  3. The scripting extension. Ad-hoc scripts with a simple input/output console, for admin tasks you run by hand.
  4. A custom extension. Only when you need a bespoke interface — canvas drawing, drag-and-drop, a third-party visualisation library, a multi-record editing grid, or an embedded external service.

A custom extension is real software. It has dependencies, a build step, and an upgrade path. Reach for it when the alternative is worse, not because it is more fun.

What you need

  • Node.js installed locally (a current LTS release).
  • Editor or creator permission on the base, plus the right to add extensions to the workspace.
  • A base to develop against. Use a copy, never the production base — an extension in development can write to real records.

Install the CLI:

npm install -g @airtable/blocks-cli

Step 1: scaffold the extension

In Airtable, open the extensions panel in your base, choose to build a custom extension, and Airtable gives you an init command containing your block and base identifiers. It looks roughly like this:

block init appXXXXXXXXXXXXXX/blkYYYYYYYYYYYYYY \
  --template https://github.com/Airtable/apps-hello-world \
  reconciler
cd reconciler
block run

block run starts a local dev server and prints a URL. Paste that URL into the extension's dev panel inside Airtable and your local code renders inside the base, hot-reloading as you save. Everything runs in your browser against your real session — there is no separate authentication step to wire up.

Your project has a block.json (the base and block IDs, plus the frontend entry point) and a frontend/index.js. That entry point is the whole app.

Step 2: read data the reactive way

The SDK is built around hooks that subscribe to the base. Do not fetch and cache by hand; let the hooks re-render when data changes.

import {
  initializeBlock, useBase, useRecords, useGlobalConfig,
  TablePicker, Button, Box, Text,
} from '@airtable/blocks/ui';
import React from 'react';

function Reconciler() {
  const base = useBase();
  const globalConfig = useGlobalConfig();
  const tableId = globalConfig.get('tableId');
  const table = base.getTableByIdIfExists(tableId);
  const records = useRecords(table ? table.selectRecords() : null);

  if (!table) {
    return (
      <Box padding={3}>
        <Text marginBottom={2}>Choose a table to reconcile:</Text>
        <TablePicker
          onChange={(t) => globalConfig.setAsync('tableId', t.id)}
        />
      </Box>
    );
  }

  const unmatched = records.filter((r) => !r.getCellValue('Matched'));

  return (
    <Box padding={3}>
      <Text>{unmatched.length} unmatched rows in {table.name}</Text>
    </Box>
  );
}

initializeBlock(() => <Reconciler />);

Three things to notice:

  • useBase, useRecords, useGlobalConfig are subscriptions. If someone edits a record in another tab, your component re-renders.
  • globalConfig is shared, per-installation settings storage — the table the user picked, a threshold, an API endpoint. It syncs to every collaborator, so treat it as configuration, not as a scratchpad, and never put secrets in it.
  • Field and table lookups should be by ID, not name. getTableByIdIfExists survives a rename; getTableByName does not. This single habit prevents most "the extension broke and nobody touched it" support tickets.

Step 3: write data in batches

The SDK enforces the same batching limit as the API: 50 records per call. Chunk your updates and check permissions before you attempt them.

async function markMatched(table, records) {
  const updates = records.map((record) => ({
    id: record.id,
    fields: { Matched: true, 'Matched at': new Date().toISOString() },
  }));

  if (!table.hasPermissionToUpdateRecords(updates)) {
    throw new Error('You do not have permission to update these records.');
  }

  for (let i = 0; i < updates.length; i += 50) {
    await table.updateRecordsAsync(updates.slice(i, i + 50));
  }
}

Always call the hasPermissionTo… check first. A read-only collaborator can open your extension, and an unchecked write throws an ugly error instead of a disabled button. The SDK exposes these checks precisely so your UI can grey things out.

For anything above a few thousand records, do not do it in the browser at all. Move the work into an automation script or a server job hitting the Web API, and let the extension trigger it.

Step 4: settings, state, and the unhappy paths

Real extensions spend most of their code on edge cases:

  • No table selected yet. Ship a settings view, keyed off globalConfig, that renders on first install.
  • A field was deleted. Every getFieldByIdIfExists can return null. Render a "this extension needs reconfiguring" message rather than crashing the panel.
  • Empty state. Zero records is not an error; say so plainly.
  • Loading. Use the SDK's Loader component while async work runs, and disable the action button so nobody double-fires a 50-record write.
  • Viewport size. Extensions run in a small panel or full screen. Call useViewport and set a sensible minimum size rather than letting your layout collapse.

Use the SDK's own UI components (Button, Select, FormField, Dialog) wherever you can. They inherit Airtable's styling, so the extension looks native and you write less CSS.

Step 5: release it

When it works locally:

block release --comment "v1: bulk reconciliation screen"

That bundles the frontend and uploads it to the block record in your base. Collaborators on the base see the released version, not your dev server. To use the same extension in another base, use the CLI's option to add a new installation rather than copying the source tree — one codebase, several installations, each with its own globalConfig.

Keep the source in Git. The released bundle is not a backup; if the repository disappears, so does your ability to fix a bug.

Step 6: hand it over properly

An extension you built and never documented becomes the client's problem in eighteen months. Before you call it done:

  • A README in the repo: what it does, which fields it depends on (by name and ID), how to run it locally, how to release.
  • A note in the base's documentation table listing the extension, its owner, and the fields it writes.
  • A short list of what will break it — renaming is safe if you used IDs; deleting the Matched field is not.
  • A named owner on the client side who knows the extension exists.

Security notes

Extension code runs client-side. That means:

  • No secrets in the bundle. An API key in your frontend is readable by every collaborator. Route third-party calls through a small proxy you control, or through an Airtable automation script, and keep the key server-side.
  • globalConfig is visible to collaborators. Configuration only.
  • Respect the permission model. The SDK cannot give a user more access than their base permissions allow, which is exactly right — do not try to design around it.

Is it worth it?

A custom extension is a sensible investment when the workflow is used daily by several people, the alternative is a manual process measured in hours, and the shape of the UI is genuinely unusual. It is a poor investment when an Interface with a button and an automation script gets you 80% of the way there, or when the requirement is really "we want it to look prettier".

BaseBrainers builds, audits, and adopts orphaned custom extensions as part of our extensions and custom apps work. If you have an extension nobody can maintain, or a workflow that has outgrown Interface Designer, tell us about it.