> For the complete documentation index, see [llms.txt](https://manual.bubble.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://manual.bubble.io/account-and-marketplace/building-plugins/updating-to-plugin-api-v4.md).

# Updating to Plugin API v4

This documentation covers how to update your server-side actions to be compatible with plugin API v4, which runs on Node 18.

Plugin API version 4 updates plugins to run on a newer version of Node, which removes support for the Fibers extension. Fibers is what let the previous server-side actions API return results directly instead of as promises. Because it's gone, some API functions change in a way that isn't backwards compatible.

{% hint style="warning" %}
If your plugin uses any server-side actions on plugin API version 3 or below, you'll need to update it manually to work with version 4. In most cases, the changes are straightforward.
{% endhint %}

This article explains what the updated plugin API looks like and how to bring older plugins up to date.

<details>

<summary>What's the difference between the Fibers extension and promises?</summary>

Fibers and promises are two different approaches to handling asynchronous operations, meaning code that has to wait for something (like a network request) to finish.

**The Fibers extension (old)** let you write asynchronous code in a more synchronous-looking way. It could pause and resume your code at certain points, making asynchronous tasks appear to run in a normal top-to-bottom flow. This made some code easier to read.

**Promises (new)** are a native JavaScript feature for managing asynchronous operations. A promise represents the eventual result of an asynchronous task, or its failure. Promises are the standard, widely used approach, and they let you chain operations, handle errors, and structure your code more predictably.

</details>

## What's changed

The functions below now return a promise for their original value. If a server-side action in your plugin uses any of them, that action will break when you update, until you adjust it.

<table data-search="false"><thead><tr><th>Function</th><th>Version 3 and below</th><th>Version 4</th></tr></thead><tbody><tr><td><code>context.request</code></td><td><code>context.request</code></td><td><code>context.v3.request</code> (returns a promise)</td></tr><tr><td><code>context.async</code></td><td><code>context.async</code></td><td><code>context.v3.async</code> (returns a promise)</td></tr><tr><td><code>.get</code> on a Bubble thing</td><td><code>.get</code></td><td><code>.get</code> (returns a promise)</td></tr><tr><td><code>.get</code> on a Bubble list</td><td><code>.get</code></td><td><code>.get</code> (returns a promise)</td></tr><tr><td><code>.length</code> on a Bubble list</td><td><code>.length</code></td><td><code>.length</code> (returns a promise)</td></tr><tr><td><code>get_object_from_id</code></td><td><code>get_object_from_id</code></td><td><code>context.getThingById</code></td></tr><tr><td><code>get_objects_from_ids</code></td><td><code>get_objects_from_ids</code></td><td><code>context.getThingsById</code></td></tr></tbody></table>

## How to update

Work through your server-side actions and apply the following changes:

{% stepper %}
{% step %}

#### Update context.request calls

For any top-level function that calls `context.request`, add `async` to the function and `await` before the call, then change `context.request` to `context.v3.request`. Consider switching to `node-fetch`, which has a more modern, standardized API.
{% endstep %}

{% step %}

#### Update context.async calls

For any top-level function that calls `context.async`, add `async` to the function and `await` before the call, then change `context.async` to `context.v3.async`. Consider switching to `util.promisify`, which is a more modern approach.
{% endstep %}

{% step %}

#### Update .get on Bubble things

For any top-level function that calls `.get` on a Bubble thing, add `async` to the function and `await` before `.get`.
{% endstep %}

{% step %}

#### Update .get on Bubble lists

For any top-level function that calls `.get` on a Bubble list, add `async` to the function and `await` before `.get`.
{% endstep %}

{% step %}

#### Update .length on Bubble lists

For any top-level function that calls `.length` on a Bubble list, add `async` to the function and `await` before `.length`.
{% endstep %}

{% step %}

#### Switch to version 4

Switch your *Bubble plugin API version* to version 4 in the *Dependencies* dropdown of the *Shared* tab.
{% endstep %}
{% endstepper %}

## Examples

Here are a few examples of how to update the `run_server` function of your plugin's server-side actions. Each shows the version 3 code first, then the version 4 equivalent.

### context.request

#### Version 3 and below:

```javascript
function(properties, context) {
    // Send a request to the cat API synchronously
    const response = context.request({
        url: 'https://api.thecatapi.com/v1/images/search',
        json: true
    })
    const cat_image = response.body[0].url
    return { cat_image }
}
```

#### Version 4:

```javascript
async function(properties, context) {
    // Send a request to the cat API asynchronously, with promises
    const response = await context.v3.request({
        url: 'https://api.thecatapi.com/v1/images/search',
        json: true
    })
    const cat_image = response.body[0].url
    return { cat_image }
}
```

You can also switch to `node-fetch`, which is globally available, instead of `context.request`:

```javascript
async function(properties, context) {
    // Use the node-fetch module to make an HTTP request
    const response = await fetch('https://api.thecatapi.com/v1/images/search')
    const body = await response.json()
    const cat_image = body[0].url
    return { cat_image }
}
```

### context.async

#### Version 3 and below:

In the old API, plugin authors often used `context.async` to work with modern libraries that already used promises. For example, using the `weathered` npm package to fetch real-time weather warnings:

```javascript
function(properties, context) {
    const { Client } = require('weathered')
    const client = new Client()
    const region = properties.region || 'NY'

    const alerts = context.async((cb) => {
        client.getAlerts(true, { region })
            .then((result) => cb(null, result))
            .catch((error) => cb(error))
    })

    const weather_alerts = alerts.features.map(f => f.properties.description)
    return { weather_alerts }
}
```

#### Version 4:

In the version 4 API, which is already promise-based, you can await the result directly:

```javascript
async function(properties, context) {
    const { Client } = require('weathered')
    const client = new Client()
    const region = properties.region || 'NY'

    const alerts = await client.getAlerts(true, { region })

    const weather_alerts = alerts.features.map(f => f.properties.description)
    return { weather_alerts }
}
```

#### Version 3 and below:

If the code you're waiting on is callback-based rather than promise-based, you'll still need a wrapper. Here's a (somewhat contrived) example using Node's callback-based `fs.stat` to inspect the filesystem of the lambda your action runs on:

```javascript
function(properties, context) {
    const fs = require('node:fs')
    const { inspect } = require('node:util')

    const stats = context.async((cb) => {
        fs.stat('/', cb)
    })
    return { stats: inspect(stats) }
}
```

#### Version 4:

Instead of `context.async`, wrap `fs.stat` with Node's built-in `promisify` utility so it returns a promise, then await it:

```javascript
async function(properties, context) {
    const fs = require('node:fs')
    const { inspect, promisify } = require('node:util')

    const stats = await promisify(fs.stat)('/')
    return { stats: inspect(stats) }
}
```

### .length()

#### Version 3 and below:

```javascript
function(properties, context) {
    let list_length = properties.my_list.length()
    return { list_length }
}
```

#### Version 4:

```javascript
async function(properties, context) {
    let list_length = await properties.my_list.length()
    return { list_length }
}
```

## What's new in version 4

Alongside the breaking changes, version 4 adds some new capabilities:

* **`getAll()` on a Bubble thing.** Returns a promise for an object containing all of the thing's fields and their values.
* **`id` field on a Bubble thing.** A field that contains the thing's unique ID.
* **`isBubbleThing` and `isBubbleList` on context.** Check whether a JavaScript value is a Bubble thing or a Bubble list. These replace the older `single_api` and `list_api` fields, which still work but are no longer encouraged.
* **Async iteration on lists.** Bubble lists now implement the `AsyncIterable` interface, so you can loop over them with `for (await ... of ...)` syntax.
* **`getThingById` and `getThingsById` on context.** Previously usable but undocumented, these are now officially supported and return promises. They replace the old global `get_object_from_id` and `get_objects_from_ids` functions.

### Full type reference

{% code overflow="wrap" lineNumbers="true" %}

```typescript
export type Primitive = string | number | boolean | Date | null | undefined

/** Any value that can be passed as a property to an action or stored in the DB. */
export type BubbleValue = Primitive | Primitive[] | BubbleThing | BubbleList

/** Fields present on any Bubble data type. */
export interface ThingFields {
    // These fields are guaranteed to be present on any database Thing.
    'Slug'?: string
    'Created Date': Date
    'Modified Date': Date

    // Other fields might also be present, depending on the data type.
    [_: string]: BubbleValue
}

/** Additional fields present on the User data type. */
export interface UserFields extends ThingFields {
    'email': string
    'logged_in': boolean
}

/** An object representing a single Thing from the Bubble app's database. */
export interface BubbleThing {
    /** Returns the names of the fields on the Thing. */
    listProperties(): string[]

    // CHANGED - now returns a promise
    /** Returns the value stored in a particular field of this Thing. */
    get(propertyName: string): Promise<BubbleValue>

    // NEW
    /** Returns an object with all the thing's fields and their values. */
    getAll(): Promise<Record<string, BubbleValue>>

    // NEW
    /** Field that contains the unique ID of this Thing. */
    readonly id: string

    // for historical interest - maybe de-document these
    single_api: true
    list_api: false
}

/**
 * An object representing a list of Things from the Bubble app's database.
 *
 * As lists may be quite large, the data contained in the list isn't all
 * loaded up front. Therefore, methods for accessing data within the list
 * are generally asynchronous.
 */
export interface BubbleList {
    // CHANGED - now returns a promise
    /** The number of items in this BubbleList. */
    length(): Promise<number>

    // CHANGED - now returns a promise
    /** Fetch a portion of a BubbleList as an array. */
    get(start: number, length: number): Promise<BubbleThing[]>

    // NEW
    /** Allows you to use a BubbleList with the `for (await x of list)` syntax */
    [Symbol.asyncIterator](): AsyncIterator<BubbleThing>

    // for historical interest - maybe de-document these
    single_api: false
    list_api: true
}

/** An object containing utility functions available to server-side actions. */
export interface Context {
    /** The current user who initiated the workflow that's running this action. */
    currentUser: BubbleThing

    /** The timezone the workflow is running in. */
    userTimezone: string

    /** An object containing any keys set for this plugin in the app. */
    keys: Record<string, string>

    // NEW
    /** Check if a JS value is a BubbleThing object. */
    isBubbleThing(x: unknown): boolean

    // NEW
    /** Check if a JS value is a BubbleList object. */
    isBubbleList(x: unknown): boolean

    // NEW - was secret global before
    /** Fetch a BubbleThing object for a given unique ID. */
    getThingById(id: string): Promise<BubbleThing | null>

    // NEW - was secret global before
    /**
     * Fetch the corresponding BubbleThing objects for each ID in an array.
     * Results will be returned in the same order as requested, with `null`
     * in place of any IDs with no corresponding Thing.
     */
    getThingsById(ids: string[]): Promise<Array<BubbleThing | null>>

    // NEW/CHANGED - these used to be top-level properties of the context
    v3: ContextDeprecatedV3
}

/** Context functions that are no longer recommended, but are provided for backwards compatibility. */
export interface ContextDeprecatedV3 {
    // CHANGED - now returns a promise
    /**
     * See documentation for the `request` library.
     *
     * @deprecated We recommend you use node-fetch instead.
     */
    request(...args: unknown[]): Promise<unknown>

    // CHANGED - now returns a promise
    /**
     * Takes a node-style asynchronous function which expects to be passed a callback,
     * and turns it into a promise.
     *
     * @deprecated We recommend you use node's built-in `util.promisify`.
     */
    'async': <T>(fn: (callback: (err: unknown, res?: T) => void) => void) => Promise<T>
}

declare global {
    /** Exposes node-fetch to plugins. */
    function fetch(...args: unknown[]): Promise<unknown>
}
```

{% endcode %}

## Changelog

* `context.request` is being deprecated (we plan to eventually stop supporting it), moved to `context.v3.request`, and now returns a promise. We recommend using the globally available `node-fetch` instead.
* `context.async` is being deprecated, moved to `context.v3.async`, and now returns a promise. We recommend using `promisify` from Node's built-in `util` module instead.
* The `.get` method on Bubble things now returns a promise.
* The `.get` method on Bubble lists now returns a promise.
* The `.length` method on Bubble lists now returns a promise.
* New `getAll()` method on Bubble things: returns a promise for an object with all the thing's fields.
* New `id` field on Bubble things: the thing's ID.
* Bubble lists now implement the `AsyncIterable` interface, so you can loop over them with `for (await ... of ...)` syntax.
* The `single_api` and `list_api` fields are still present but no longer encouraged, and will lose official documentation. Use the new `isBubbleThing` and `isBubbleList` context methods instead.
* New official support for `getThingById` and `getThingsById`, added to the context object for consistency with the rest of the API. Both return promises.
