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,baseandoutputobjects 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
Sitesthat has three fields:Website(URL),Screenshot(Attachment) andShot 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
- Open Automations and create an automation. As the trigger, choose At scheduled time and pick an interval, for example every hour.
- Add the action Run a script.
- In the script sidebar, open Secrets, choose Add existing secret (or create one) and name it
RS_API_KEY. The script reads it withinput.secret(), so the key never appears in the script text. Airtable also lets only people with access to the secret edit the script. - Paste the script below and click Test. The console shows a line like
captured 3 of 7; stopped: rate limit window used. - 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
fetchcalls per run, a 30-secondfetchtimeout 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 budgetafter one or two rows. The sites are slow. LowerMAX_PER_RUN, or run the automation more often.