# Scripting Extension Reference

This document covers the Airtable **Scripting Extension** (the interactive scripting app added via Extensions). This is a **different runtime** from Automation scripts. Many APIs exist in one environment but not the other.

---

## Two Environments: Know Which One You're In

| | Scripting Extension | Automation Script |
|---|---|---|
| **Where it runs** | In the browser (with server-side fetch proxy) | On Airtable's servers |
| **Triggered by** | User clicks "Run" | Automation trigger (record change, schedule, etc.) |
| **HTTP requests** | `remoteFetchAsync(url, options)` | `fetch(url, options)` |
| **User output** | `output.text()`, `output.markdown()`, `output.table()`, `output.inspect()` | `output.set(key, value)` only |
| **User input** | `input.textAsync()`, `input.buttonsAsync()`, `input.tableAsync()`, etc. | `input.config()` (one call), `input.secret()` |
| **Interactive** | Yes - can prompt user mid-script | No - runs unattended |
| **Timeout** | Longer (user-facing, less strict) | 180 seconds hard kill |
| **fetch limit** | Not explicitly documented; less restrictive | 50 calls per script |
| **Select field options** | Can modify via `updateOptionsAsync()` | Cannot modify |
| **Cursor / viewport** | `cursor` object available | Not available |

### Common mistake: using the wrong fetch

```javascript
// SCRIPTING EXTENSION - use remoteFetchAsync
let response = await remoteFetchAsync("https://api.example.com/data", {
  method: "GET",
  headers: { "Content-Type": "application/json" }
});

// AUTOMATION SCRIPT - use fetch
let response = await fetch("https://api.example.com/data", {
  method: "GET",
  headers: { "Content-Type": "application/json" }
});
```

`remoteFetchAsync` proxies through Airtable's servers to bypass CORS. It does NOT exist in automation scripts. `fetch` in automation scripts already runs server-side, so CORS is not an issue.

### remoteFetchAsync limitations

- Does **not** follow redirects. Only `error` and `manual` redirect modes are supported.
- This effectively blocks OAuth flows that rely on redirect-based auth.
- No secure place to store tokens - the extension environment is not designed for long-lived credentials.

---

## Output Methods (Extension Only)

These methods render content in the right-hand results pane of the Scripting Extension. They do **not** exist in automation scripts.

### output.text(string)

Outputs plain text.

```javascript
output.text("Found 42 records");
output.text("Processing complete in " + elapsed + "ms");
```

### output.markdown(string)

Outputs markdown-formatted text. Supports headers, bold, italic, links, code blocks, and tables.

```javascript
output.markdown("# Results");
output.markdown("Found **42** records in `Table 1`");
output.markdown("[View on Google](https://google.com)");

// Markdown table
output.markdown(
  "| Name | Value |\n|---|---|\n| Total | 42 |\n| Errors | 0 |"
);
```

### output.table(records)

Renders an array of objects as a formatted table. Each object becomes a row; keys become column headers.

```javascript
let results = [
  { Name: "Aeron", Price: "$450", Status: "Available" },
  { Name: "Leap V2", Price: "$350", Status: "Sold" }
];
output.table(results);
```

Also accepts an array of Airtable records directly from a query.

### output.inspect(value)

Debug inspection of any value. Renders the structure of objects, arrays, etc. Useful for exploring API responses or cell values.

```javascript
let record = query.records[0];
let cellValue = record.getCellValue("My Field");
output.inspect(cellValue);  // shows the full structure
```

### console.log() / console.error()

Available in both environments. In the Scripting Extension, console output appears in the browser dev tools console, NOT in the results pane. Use `output.*` methods for user-visible output and `console.log` for debugging.

```javascript
console.log("Debug: response status " + response.status);  // dev tools only
output.text("Request succeeded");  // visible in results pane
```

---

## Input Methods (Extension Only)

These methods prompt the user interactively. They are `async` and pause script execution until the user responds. They do **not** exist in automation scripts.

### input.textAsync(prompt)

Prompts the user to enter text. Returns a string.

```javascript
let searchTerm = await input.textAsync("Enter a search term");
output.text("Searching for: " + searchTerm);
```

### input.buttonsAsync(prompt, buttons)

Prompts the user to click one of several buttons. Returns the selected button object.

```javascript
let action = await input.buttonsAsync("What do you want to do?", [
  { label: "Create records", value: "create" },
  { label: "Update records", value: "update" },
  { label: "Cancel", value: "cancel" }
]);
output.text("Selected: " + action);
```

### input.tableAsync(prompt)

Prompts the user to select a table from the current base. Returns a Table object.

```javascript
let table = await input.tableAsync("Pick a table");
output.text("Selected table: " + table.name);
```

### input.fieldAsync(prompt, table)

Prompts the user to select a field from a given table. Returns a Field object.

```javascript
let table = await input.tableAsync("Pick a table");
let field = await input.fieldAsync("Pick a field", table);
output.text("Selected: " + field.name + " (type: " + field.type + ")");
```

### input.recordAsync(prompt, table)

Prompts the user to select a record from a given table. Returns a Record object.

```javascript
let table = base.getTable("Listings");
let record = await input.recordAsync("Pick a record", table);
output.text("Selected: " + record.name);
```

### input.fileAsync(prompt)

Prompts the user to upload a file. Returns a File object with contents.

```javascript
let file = await input.fileAsync("Upload a CSV");
output.text("File: " + file.file.name);
// file.parsedContents available for CSV files
```

### input.config() in Extensions (Script Settings)

In the Scripting Extension, `input.config()` creates a **Script Settings UI** - a persistent configuration panel that non-technical users can fill out without editing code. This is different from automation `input.config()` which reads automation input variables.

```javascript
let config = input.config({
  title: "RSS Feed Monitor",
  description: "Configure which feeds to monitor",
  items: [
    input.config.table("sourceTable", { label: "Destination table" }),
    input.config.field("titleField", { parentTable: "sourceTable", label: "Title field" }),
    input.config.text("feedUrl", { label: "RSS Feed URL" }),
    input.config.select("frequency", {
      label: "Check frequency",
      options: [
        { label: "Every hour", value: "hourly" },
        { label: "Every day", value: "daily" }
      ]
    })
  ]
});

let table = config.sourceTable;
let url = config.feedUrl;
```

---

## Field Types and Cell Values

When reading cell values with `record.getCellValue(fieldName)`, the return type depends on the field type:

| Field Type | getCellValue() returns | Notes |
|---|---|---|
| Single line text | `string` or `null` | |
| Long text | `string` or `null` | For rich text fields, use `getCellValueAsString()` for plain text |
| Number | `number` or `null` | |
| Currency | `number` or `null` | Raw number, no formatting |
| Percent | `number` or `null` | Decimal form: 0.5 = 50% |
| Single select | `{id, name, color}` or `null` | |
| Multi select | `[{id, name, color}, ...]` | Empty array if none selected |
| Checkbox | `boolean` | `true` or `false` (not null) |
| Date | `string` or `null` | ISO format: "2026-04-06" |
| Date and time | `string` or `null` | ISO format: "2026-04-06T10:30:00.000Z" |
| URL | `string` or `null` | |
| Email | `string` or `null` | |
| Phone | `string` or `null` | |
| Linked records | `[{id, name}, ...]` | Array of linked record references |
| Attachments | `[{id, url, filename, size, type, ...}, ...]` | |
| Formula | varies | Depends on formula result type |
| Rollup | varies | Depends on rollup aggregation |
| Lookup | `[values, ...]` | Array of looked-up values |
| Auto number | `number` | Read-only |
| Created time | `string` | Read-only, ISO format |
| Last modified time | `string` | Read-only, ISO format |

### Writing cell values

When writing with `createRecordAsync` or `updateRecordAsync`, values must match expected shapes:

```javascript
await table.updateRecordAsync(recordId, {
  "Single Line Text": "hello",
  "Number Field": 42,
  "Single Select": { name: "Option A" },         // use name, not id
  "Multi Select": [{ name: "A" }, { name: "B" }],
  "Checkbox": true,
  "URL": "https://example.com",
  "Date": "2026-04-06",
  "Linked Records": [{ id: "recABC123" }],        // must use record IDs
  "Attachments": [{ url: "https://example.com/file.pdf" }]
});
```

**Common gotcha:** Single select and multi select fields require `{ name: "value" }` objects, not bare strings. Writing `"Option A"` instead of `{ name: "Option A" }` throws a cell value error.

---

## Table, Field, and Base Metadata

### Base

```javascript
// List all tables
for (let table of base.tables) {
  output.text(table.name + " (" + table.id + ")");
}

// Get a specific table by name or ID
let table = base.getTable("Table Name");
let table = base.getTable("tblXXXXXXXXXXXXXX");
```

### Table

```javascript
let table = base.getTable("Listings");

// Properties
table.name          // string
table.id            // string (tblXXXXX)
table.fields        // array of Field objects
table.views         // array of View objects

// Get specific field or view
let field = table.getField("Status");
let view = table.getView("Grid view");

// Select records from a specific view
let query = await view.selectRecordsAsync({ fields: ["Name", "Status"] });
```

### Field

```javascript
let field = table.getField("Status");

field.name          // string
field.id            // string (fldXXXXX)
field.type          // string: "singleLineText", "number", "singleSelect", etc.
field.options       // object with type-specific options (e.g., select choices)
field.isComputed    // boolean - true for formula, rollup, lookup, etc.
```

### Field type strings

`singleLineText`, `multilineText`, `richText`, `number`, `currency`, `percent`, `singleSelect`, `multipleSelects`, `checkbox`, `date`, `dateTime`, `url`, `email`, `phone`, `multipleRecordLinks`, `multipleAttachments`, `formula`, `rollup`, `lookup`, `autoNumber`, `createdTime`, `lastModifiedTime`, `singleCollaborator`, `multipleCollaborators`, `barcode`, `button`, `rating`, `duration`, `externalSyncSource`

### Updating select field options (Extension only)

```javascript
let field = table.getField("Status");
await field.updateOptionsAsync({
  choices: [
    ...field.options.choices,
    { name: "New Option", color: "blueLight2" }
  ]
});
```

This only works in the Scripting Extension, not in Automation scripts.

---

## Delete Records

```javascript
// Single delete
await table.deleteRecordAsync(recordId);

// Batch delete (max 50 per call)
let idsToDelete = records.map(r => r.id);
for (let i = 0; i < idsToDelete.length; i += 50) {
  let batch = idsToDelete.slice(i, i + 50);
  await table.deleteRecordsAsync(batch);
}
```

---

## Discovery Script Pattern (Extension)

When exploring an unfamiliar API or data source, use this pattern to inspect the response before building the full pipeline:

```javascript
let url = "https://api.example.com/data";

try {
  let response = await remoteFetchAsync(url);
  console.log("Status: " + response.status);

  if (!response.ok) {
    let errorBody = await response.text();
    output.text("HTTP " + response.status + ": " + errorBody.substring(0, 500));
    throw new Error("Request failed");
  }

  let raw = await response.text();
  output.text("Response length: " + raw.length + " chars");

  // Try JSON parse
  try {
    let data = JSON.parse(raw);
    output.inspect(data);
  } catch (e) {
    // Not JSON - show raw preview (likely XML/HTML)
    output.text("Non-JSON response. Preview:");
    output.text(raw.substring(0, 1000));
  }
} catch (err) {
  output.text("Error: " + err.message);
  console.error(err);
}
```
