Airtable Screenshots: Fill an Attachment Field from URLs

An Airtable automation script that turns a URL field into screenshot attachments, works through a queue inside rate limits and marks dead links.

By Simon Chiu · Updated

This page gives you an Airtable automation script that fills an attachment field with a screenshot of each record's website. It works through the table as a queue. It stops before the API's per-minute limit and continues on the next run. It marks URLs that can never render, so they are not tried again. It is part of our integrations hub.

Last tested: 2026-10-18. We ran the script with Node 22.18 in a harness that gives it fake input, base and output objects with the same methods as Airtable's scripting API, against a local stub of our API that returns the documented JSON and errors and enforces a per-minute limit. We did not run it inside an Airtable base for this page, so the page has no screenshots of the Airtable UI.

What you can build

  • A directory with thumbnails. Each listing has a website. The automation adds a preview image that Airtable Interfaces and gallery views show on the record card. The website thumbnails use case covers sizes and layouts.
  • A lead list you can scan. Imported companies get a homepage capture and the page title in a status field, so a person can sort out parked domains without opening each site.
  • A content log. Editors paste the URL of each published article, and the base keeps a dated image of how it looked.

If you only need to see the images and not store them, a spreadsheet is simpler: the Google Sheets version shows each capture with IMAGE() and needs no automation.

Prerequisites

  • An Airtable base with a table named Sites that has three fields: Website (URL), Screenshot (Attachment) and Shot status (Single line text). You can change the names at the top of the script.
  • A RenderScreenshot API key that starts with rs_live_. Create a free account to get one.
  • An Airtable plan that includes the Run a script action in automations.

Step by step

  1. Open Automations and create an automation. As the trigger, choose At scheduled time and pick an interval, for example every hour.
  2. Add the action Run a script.
  3. In the script sidebar, open Secrets, choose Add existing secret (or create one) and name it RS_API_KEY. The script reads it with input.secret(), so the key never appears in the script text. Airtable also lets only people with access to the secret edit the script.
  4. Paste the script below and click Test. The console shows a line like captured 3 of 7; stopped: rate limit window used.
  5. Turn the automation on.
// Airtable automation: "At scheduled time" -> "Run a script".
// Fills an attachment field with a screenshot of each record's URL.
// Secret: RS_API_KEY (rs_live_...). No input variables needed.
const TABLE = 'Sites';
const URL_FIELD = 'Website';
const SHOT_FIELD = 'Screenshot';     // attachment field
const STATUS_FIELD = 'Shot status';  // single line text
const BUDGET_MS = 25000;             // stop starting new captures after 25 s
const MAX_PER_RUN = 20;              // stays well under the 50 fetch calls per run

const apiKey = input.secret('RS_API_KEY');
const started = Date.now();
const table = base.getTable(TABLE);
const query = await table.selectRecordsAsync({ fields: [URL_FIELD, SHOT_FIELD, STATUS_FIELD] });

// Rows with a URL, no screenshot yet, and no permanent error.
const todo = query.records.filter((r) =>
  r.getCellValueAsString(URL_FIELD) &&
  !(r.getCellValue(SHOT_FIELD) || []).length &&
  !r.getCellValueAsString(STATUS_FIELD).startsWith('failed')
).slice(0, MAX_PER_RUN);

// Airtable scripts may not have URL(), so take the host name with a regex.
const hostOf = (u) => u.replace(/^https?:\/\//i, '').split(/[/?#:]/)[0] || 'screenshot';

let done = 0;
let stopReason = 'queue empty';
for (const record of todo) {
  if (Date.now() - started > BUDGET_MS) { stopReason = 'time budget'; break; }

  let target = record.getCellValueAsString(URL_FIELD).trim();
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) target = `https://${target}`; // acme.com -> https://acme.com

  const res = await fetch('https://api.renderscreenshot.com/v1/screenshot', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify({
      url: target,
      preset: 'link_preview',
      output: { format: 'jpeg', quality: 80 },
      block: { ads: true, cookie_banners: true },
      wait: { timeout: 20 },          // inside Airtable's 30 s fetch timeout
      cache: { available: true },     // a re-run reuses the render for 0 credits
    }),
  });
  const data = await res.json();

  if (res.status === 429) { stopReason = `rate limited, next run continues`; break; }
  if (res.status === 403 && data.error.code === 'insufficient_credits') { stopReason = 'out of credits'; break; }

  if (res.ok) {
    await table.updateRecordAsync(record, {
      [SHOT_FIELD]: [{ url: data.image.url, filename: `${hostOf(target)}.jpg` }],
      [STATUS_FIELD]: `ok ${data.metadata.title || ''}`.trim(),
    });
    done++;
  } else if (!data.error.retryable) {
    // Permanent: bad URL, blocked target, bad key. Mark it so it is not retried every run.
    await table.updateRecordAsync(record, { [STATUS_FIELD]: `failed ${data.error.code}` });
  } else {
    // Retryable (timeout, navigation): leave the status empty, the next run tries again.
    console.log(`retry later: ${target} ${data.error.code}`);
  }

  if (res.headers.get('x-ratelimit-remaining') === '0') { stopReason = 'rate limit window used'; break; }
}

output.set('captured', done);
output.set('stopReason', stopReason);
console.log(`captured ${done} of ${todo.length}; stopped: ${stopReason}`);

Why a queue and not a record trigger

Airtable starts one run per record for record triggers. A CSV import of 300 rows would start 300 runs at about the same moment. Every account has one per-minute request limit for all its keys, so most of those runs would get 429. The scheduled script asks for at most 20 records, sends one request at a time, and stops in three cases: the time budget is used, the API returns 429, or X-RateLimit-Remaining reaches 0. The rows it did not reach still have no screenshot, so the next run takes them.

For a table that gets one new row now and then, a When record matches conditions trigger works too. Use the same request, but read the record ID from an input variable.

Parameter mapping

Script setting API field Value in the script Change it to
URL_FIELD value url acme.com becomes https://acme.com Any page URL
(body) preset link_preview (800 × 600) og_card or desktop_hd. See presets
(body) output.format, output.quality JPEG at 80 png for sharp text
(body) block.ads, block.cookie_banners true Keep them on for clean thumbnails
(body) wait.timeout 20 seconds Keep it below Airtable's 30-second fetch timeout
(body) cache.available true A re-run of the same URL costs 0 credits
STATUS_FIELD metadata.title ok Example Domain Any text field

The JSON response also has metadata.description and metadata.favicon, if you want more fields. The POST endpoint reference lists all options.

Errors and retries

The script sorts every failure by error.retryable from the API's error body:

Result Example codes What the script does
Success none Writes the attachment and ok <page title>
Permanent failure invalid_url, blocked, missing_credentials Writes failed <code>. The filter skips the row from then on.
Retryable failure timeout, navigation_failed, ssl_error Leaves the status empty. The next run tries again.
429 too_many_requests none Stops the run. The next run continues.
403 insufficient_credits none Stops the run until credits reset

In our harness with a limit of 4 requests per minute, five rows gave this result in the first run: two attachments, one failed invalid_url for an ftp:// link, one retryable timeout left for later, and a stop when the window was used. A second test, where another client had already used the window, stopped at the first 429 with no row changed. To try a failed row again, clear its status.

The error handling guide explains each code.

Limits and costs

  • Airtable script limits. Airtable documents 50 fetch calls per run, a 30-second fetch timeout and a script timeout that is normally 30 seconds (checked on Airtable's support site in October 2026). The script stays under these with 20 records and a 25-second budget.
  • Credits. Each new screenshot uses one credit. Failed renders use none. Re-runs of a URL that was rendered before are free while the render is in cache. The free plan gives 150 credits each month. See pricing.
  • Throughput. The free plan allows 10 requests per minute. A run of 25 seconds also limits the count, because each render takes a few seconds. For thousands of rows, run the automation more often or use the batch endpoint from a script outside Airtable.
  • Storage. Attachments count toward your Airtable plan's attachment space. JPEG at quality 80 keeps the files small.

Troubleshooting

  • input.secret() fails. Add the secret to this script in the Secrets section, not only to the base.
  • Every row gets failed missing_credentials. The secret holds a wrong or revoked key. Fix it, then clear the status of those rows.
  • The attachment field stays empty but the status says ok. Airtable downloads attachments a short time after the write. Refresh the view.
  • The images show a cookie wall. Most banners go with block.cookie_banners. For the rest, see blocking ads and cookie banners.
  • The run stops with time budget after one or two rows. The sites are slow. Lower MAX_PER_RUN, or run the automation more often.

Frequently asked questions

Can Airtable take a screenshot of a website automatically?
Not by itself. An automation with a Run a script action can call a screenshot API and write the returned image link into an attachment field. Airtable then downloads the file and keeps its own copy.
Why use a scheduled trigger and not 'When a record is created'?
A CSV import or a form burst creates many records at once, and each one would start its own run at the same moment. That exceeds a per-minute API limit. A scheduled run works through a queue in order and stops cleanly when the minute's requests are used.
Do the screenshots disappear when the API link expires?
No. When a script writes an attachment from a URL, Airtable downloads the file and stores it. The CDN link only has to work at that moment.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.