Developers
@openrows/sdk
A typed JavaScript client for the OpenRows REST API v1. ESM and
CommonJS, Node 20+ and browsers, no dependencies beyond fetch. Its types are generated from the
API's own contracts, so they say what the server checks.
npm install @openrows/sdk
Quick start
Create a personal access token in OpenRows (Settings → "Developer: personal access tokens") with the scopes you need, then:
import { OpenRows } from '@openrows/sdk';
const openrows = new OpenRows({
apiKey: process.env.OPENROWS_TOKEN, // or_pat_…
baseUrl: 'https://app.openrows.io', // the default; your own server's origin when self-hosted
});
const bases = await openrows.bases.list();
const tables = await openrows.tables(bases[0].id).list();
require('@openrows/sdk') works too.
Records
A table is named by id or name. Cells are keyed by field name (or id, with
returnFieldsByFieldId). Give the table your fields' types to have them checked:
type Task = { Name: string; Status: string; Points: number };
const tasks = openrows.table<Task>(baseId, 'Tasks');
// Every record, page by page (100 at most per request).
const done = await tasks
.select({
view: 'Grid view',
filterByFormula: "{Status} = 'Done'",
sort: [{ field: 'Points', direction: 'desc' }],
fields: ['Name', 'Points'],
})
.all();
// One page at a time; answer `false` to stop.
await tasks.select({ pageSize: 50 }).eachPage((records) => {
for (const r of records) console.log(r.id, r.fields.Name);
});
// Or iterate, with one page in memory at a time.
for await (const record of tasks.select()) console.log(record.fields.Name);
const first = await tasks.select({ maxRecords: 1 }).firstPage();
const one = await tasks.find(recordId);
Writes take any number of records and send them 10 per request, in order. A failed request throws; the batches before it are already saved.
const created = await tasks.create([{ fields: { Name: 'Write docs' } }, { fields: { Name: 'Ship' } }]);
const single = await tasks.create({ Name: 'Review', Status: 'Todo' });
// typecast: new select options are added, linked records found by name.
await tasks.create({ Name: 'Triage', Status: 'Blocked' }, { typecast: true });
await tasks.update(single.id, { Status: 'Done' }); // only the fields given change
await tasks.update([{ id: single.id, fields: { Points: 3 } }]);
await tasks.replace(single.id, { Name: 'Review' }); // fields left out are cleared
await tasks.replace([{ id: single.id, fields: { Name: 'Review' } }]);
// Update records matching on up to 3 fields, create the others.
const { createdRecords, updatedRecords } = await tasks.upsert(
[{ fields: { Name: 'Ship', Points: 5 } }],
{ fieldsToMergeOn: ['Name'] }
);
await tasks.destroy(single.id); // to the trash
await tasks.destroy(created.map((r) => r.id));
Comments
const comments = tasks.comments(recordId);
await comments.create('Looks good to me');
const page = await comments.page({ pageSize: 20 }); // newest first, with `offset`
const all = await comments.all();
Schema
const table = await openrows.tables(baseId).create({
name: 'Projects',
fields: [
{ name: 'Name', type: 'singleLineText' }, // the first field is the primary field
{ name: 'Stage', type: 'singleSelect', options: { choices: [{ name: 'Idea' }] } },
],
});
const projects = openrows.table(baseId, table.id);
const field = await projects.fields.create({ name: 'Budget', type: 'number' });
await projects.fields.update(field.id, { description: 'In euros' });
Webhooks
Needs the webhook:manage scope and the creator role on the base.
const hooks = openrows.webhooks(baseId);
const hook = await hooks.create({
url: 'https://example.com/hooks/openrows',
events: ['record.created', 'record.updated'],
});
console.log(hook.secret); // the signing secret, shown only this once
await hooks.list();
await hooks.update(hook.id, { enabled: false });
const rotated = await hooks.update(hook.id, { rotateSecret: true });
await hooks.ping(hook.id); // a signed test event, answered with the attempt
await hooks.deliveries(hook.id, { limit: 20 });
await hooks.redeliver(hook.id, deliveryId); // queues a logged event again, same event id
await hooks.delete(hook.id);
Automations
The automations built in the app: the creator role on the base, with automations:read (list,
get, runs), automations:manage (on and off) and automations:run (trigger).
const automations = openrows.automations(baseId);
const [first] = await automations.list();
await automations.setEnabled(first.id, true); // it then runs as you
// A webhook-received automation, with any JSON body; a button-clicked one with { recordId }.
const { triggerId } = await automations.trigger(first.id, { body: { orderId: 42 } });
await automations.runs(first.id, { limit: 10, status: 'failed' });
Errors
Every refusal throws an OpenRowsError:
import { isOpenRowsError } from '@openrows/sdk';
try {
await tasks.select({ filterByFormula: 'LOWER({Name})' }).all();
} catch (error) {
if (isOpenRowsError(error)) {
error.status; // 422
error.type; // 'INVALID_FILTER_BY_FORMULA' (Airtable's error type where one fits)
error.code; // OpenRows' stable code
error.requestId; // quote it when reporting a problem
error.errors; // what failed validation
}
}
Rate limits
A request answered 429 is retried after its Retry-After (3 times by default). A wait longer
than maxRetryDelay throws at once, with error.retryAfter (seconds) and error.limit (base,
token or user).
Options
| Option | Default | |
|---|---|---|
apiKey |
A personal access token. Leave it out in a browser signed in to the same app | |
baseUrl |
https://app.openrows.io |
The app's origin (…/api and …/api/v1 work too) |
maxRetries |
3 |
Retries of a 429 |
maxRetryDelay |
60000 |
The longest Retry-After to wait, in ms |
timeout |
60000 |
Per request, in ms (0: none) |
fetch |
globalThis.fetch |
Your own fetch |
headers |
Extra headers for every request |
Keep tokens out of code that runs in someone else's browser: a token can do whatever its scopes and its owner allow.
Reference
The full reference is at /docs/api/sdk, and the HTTP API's at /docs/api.
License
AGPL-3.0-only, like the rest of the OpenRows core.
Releasing
Pushing an sdk-v<version> tag (the version in package.json) runs
.github/workflows/release-packages.yml, which builds, tests, checks the tarball and publishes
with provenance. Nothing is published from a local machine.