> This page is for Developer Documentation.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.benchling.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.benchling.com/_mcp/server.

# Building your first Benchling App

This guide walks through creating and configuring a simple Benchling App in order to
highlight the standard permissions and authentication strategies for Benchling Apps.
The beginning overlaps with
[Getting Started with Benchling Apps](/docs/getting-started-benchling-apps), so if
you've already completed those steps, skip ahead to [Building the app](#building-the-app).
By the end, we'll have an app that reviews newly created or modified DNA entities for a
filled-in Species field and updates those with missing values.

You'll need:

* A Benchling Registry
* A Python development environment with the Benchling SDK installed locally
* Benchling Developer Platform access
* Tenant Admin permissions

## Getting access

First, make sure you have access to the "Developer Platform" capability. See
[Developer Platform access](https://help.benchling.com/hc/en-us/articles/9714802977805-Access-the-Benchling-Developer-Platform),
or the general guide to
[Capability Management](https://help.benchling.com/hc/en-us/articles/9684234546317-Capability-Management-Industry).

## Where to find apps

Apps live on the Feature Settings page. There are several ways to get there; the
easiest is through your user menu, which takes you to the "Developer Console" section
of Feature Settings and opens the "Apps" settings.

## Creating an app

Click the "Create" button in the top right to create your first app. You can always
change the name and description afterward.

![The app detail page in the Developer Console showing the app's Client ID and a Generate Secret button](/_fern-files/_img/cc9a59c54dffae0c2a2e5882c6e4755a3164f7dd7cbf1af27bdeb0c9a12f1dba.webp)

Take note of the Client ID, which is specific to the app, and save it for later. Next,
click "Generate Secret" to create the private key for this app.

> **Warning**
>
> ### Do not close this popup yet
>
> Copy the Client Secret for your app to a secure location before closing the modal.
> Once you close it, the full key will not be accessible again. If you lose this
> private key, you will need to generate a new one, and the old key will no longer be
> valid.

## Adding an app to an organization

Adding an app to an organization gives the app access to any projects accessible to all
members of the organization, as well as the Registry itself. This is generally necessary
when creating a new app. Org admins can add apps on the organization page under the
"APPS" tab:

![The APPS tab of an organization page with the dialog for adding an app](/_fern-files/_img/4b1ebe9b2f05495dcb6b02ad7c3e51f8701b5a2c4665d55fd700decf3efee14b.webp)

Once added, it appears in the "APPS" tab for that organization:

![The APPS tab of an organization page listing the newly added app](/_fern-files/_img/7d27398985a964ab262f34f2a388b95c13ed1070d2b1c808855fcca7c72aea16.webp)

Be aware that your tenant may be configured for Registry control by individual project
rather than at the organization level. If you think this is the case, contact your
Benchling admin or your Benchling Customer Success Manager.

### Adding an app to projects and teams

Once an app is added to the organization, team or org admins can also add apps to teams,
which similarly gives the app access to the projects team members can reach. This is
useful when managing several apps together ("Automation Apps"), or for apps specific to
a certain team ("Developer Team"). The
[Getting Started with Benchling Apps](/docs/getting-started-benchling-apps) guide covers
these additional permissions.

In the example below, the project housing the entities to be updated is configured so
that all members of the Sandbox Org have read-only permissions by default, so the
'Entity Field Updater' app has been explicitly granted admin permissions within the
project.

![A project's collaborator settings showing the Entity Field Updater app with admin permissions](/_fern-files/_img/ad26bd385cbbd012920621e4a6e0ea4699306eedf60f5622b177bd9209ffd087.webp)

## Building the app

Now that the app exists in Benchling and has the right permissions, it's time to build
it. Because the Benchling Registry is highly configurable, some app actions are
necessarily scoped to data models specific to your instance. The general purpose of
this app is to fill in missing Species data on recently created or modified DNA sequence
entities, but similar logic can sync with an external database or ontology system,
perform field validation, or periodically update entities with other relevant data.

We'll also use the Benchling SDK, which provides a typed interface for the Benchling
REST API. Make sure the SDK is installed in your local environment if you want to run
this code as shown. See
[Getting Started with the V2 SDK](/docs/getting-started-with-the-sdk).

### Data model prerequisites

To keep the focus on app development rather than data model design, we only need a
single DNA entity schema with a text schema field for Species, as shown below. Once
you've created this entity schema, or added an appropriate field to a pre-existing
entity, take note of the schema API ID, either from the URL or from the Copy API ID
button if you've enabled that option in your personal settings. We'll use that schema
API ID to filter our API calls.

![A DNA sequence schema configuration screen with a text field named Species](/_fern-files/_img/a25055af0ee3b0d72fb4764fb13a4d71a6bca626f0b0758fdf7fb54ebff84916.webp)

### Authentication

If you've used the SDK before, this will look familiar. Instantiating the Benchling
client still requires only your tenant URL and an `auth_method`, but we use the OAuth2
Client ID and secret from the app creation process rather than an individual user's API
key.

```python
from benchling_sdk.benchling import Benchling
from benchling_sdk.auth.client_credentials_oauth2 import ClientCredentialsOAuth2

tenant_name = "your_tenant"
# Your app's client credentials
client_id = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
client_secret = "cs_AppClientSecret"

auth_method = ClientCredentialsOAuth2(
    client_id=client_id,
    client_secret=client_secret)

benchling = Benchling(url=f"https://{tenant_name}.benchling.com", auth_method=auth_method)
```

### Retrieving a list of DNA sequences

With the client instantiated, we need a list of recently modified DNA sequences to
verify. Since that list could be long, the SDK's `.list()` methods return an iterable
generator rather than the full set of entities, so we can iterate page by page and act
in chunks. The
[iterating through pages](/docs/common-sdk-interactions-and-examples#iterating-through-pages)
guide has more on how the iterator works.

The list endpoint also accepts filter parameters, so we narrow the query to entities of
the schema we care about, and only those with a `modified_at` timestamp within the last
day. We've also raised the page size from the default of 50 to 100 to keep the number of
API calls reasonable.

```python
from datetime import datetime, timedelta

# Update this value to specify which schema this script will poll
entity_schema_to_check = "ts_AcqPy93F"

# Generates yesterday's date as a string formatted YYYY-MM-DD.
# A production app would also need to account for time zone.
today = datetime.today().date()
yesterday = today - timedelta(days=1)
time_string = f"> {yesterday.strftime('%Y-%m-%d')}"

# Creates a generator returning DNA sequences matching the filter params
dna_sequences_list = benchling.dna_sequences.list(
    modified_at=time_string,
    schema_id=entity_schema_to_check,
    page_size=100,
)
```

### Iterating through the returned entities

Now we iterate through the returned entities one page at a time, identifying which ones
need an update to the Species field. Each entity that needs one goes into an array of
updates, along with the key and value of the field to update.

Thanks to how the update endpoints work, we don't need to provide fields that aren't
being updated, so rather than unpacking the whole entity, we can supply just the
registry ID and the changed fields. For simplicity we pick a species at random from a
hardcoded array, but this is where you'd call another function or a third-party API.

```python
from benchling_sdk.models import DnaSequenceBulkUpdate, AsyncTaskStatus
from benchling_sdk.helpers.serialization_helpers import fields
import random

species_list = ["Human", "Mouse", "Rat", "Camel"]

def update_species_bulk(sequence_pages):
    # Loop through the pages provided by the generator, updating Species as required
    for page in sequence_pages:
        updates = []
        for sequence in page:
            current_species = sequence.fields["Species"].value
            if not current_species:
                species_to_add = random.choices(species_list)[0]
                update = DnaSequenceBulkUpdate(
                    id=sequence.id,
                    fields=fields(
                        {
                            "Species": {"value": species_to_add}
                        }
                    )
                )
                updates.append(update)
```

### Bulk update and async tasks

Assuming we found entities with empty Species fields, we now have an `updates` array of
`DnaSequenceBulkUpdate` objects, each with the `id` of the entity to update and a
`fields` parameter that uses the SDK's serialization helpers to format the new species
string.

The next step, if `updates` isn't empty, is to call the
[DNA sequences bulk update endpoint](/docs/api/v2/benchling-api/dna-sequences/bulk-update-dna-sequences)
with our list of updates. All bulk operation endpoints in the Benchling API return a
task ID rather than waiting to send a response, since larger jobs can take longer than a
standard timeout. Through the SDK, `.bulk_update()` returns an `AsyncTaskLink` that we
can pass to `.wait_for_task()` to poll the `tasks` endpoint for status updates. The
[async tasks section of the SDK examples](/docs/common-sdk-interactions-and-examples#async-tasks)
has more detail.

This block continues the `for page in sequence_pages` loop from above; the full
reference code is at the [bottom of this guide](#example-app-full-code).

```python
        if updates:
            # Calls the bulk update API and returns an AsyncTask we need to poll
            update_task_link = benchling.dna_sequences.bulk_update(updates)
            # Polls every 1 second up to 30 seconds by default to see if it completes
            update_task = benchling.tasks.wait_for_task(update_task_link.task_id)
            if update_task.status != AsyncTaskStatus.SUCCEEDED:
                raise RuntimeError(
                    f"Failed to update entities. Task ID: {update_task_link.task_id}\n"
                    f"Error: {update_task.errors}"
                )
            print("Completed bulk update for task", update_task_link.task_id)
        else:
            print("No entities found to update")
```

All that's left is to call `update_species_bulk(dna_sequences_list)` and monitor the
output. In production you could schedule this with a system automation tool like `cron`
or a serverless environment like AWS Lambda, or upgrade the app to be evented and
trigger off the [`entity.registered` event](/docs/events-reference). See
[Getting Started with EventBridge](/docs/events-getting-started).

## Example app full code

As noted above, this app uses the Benchling SDK, which provides a typed interface for
the Benchling REST API. Make sure the SDK is installed in your local environment if you
want to run this code as shown; see
[Getting Started with the V2 SDK](/docs/getting-started-with-the-sdk) for more.

```python
from benchling_sdk.benchling import Benchling
from benchling_sdk.models import DnaSequenceBulkUpdate, AsyncTaskStatus
from benchling_sdk.auth.client_credentials_oauth2 import ClientCredentialsOAuth2
from benchling_sdk.helpers.serialization_helpers import fields
from datetime import datetime, timedelta
import random

tenant_name = "Your tenant"
client_id = "Your app's Client ID"
client_secret = "Your app's client secret"

auth_method = ClientCredentialsOAuth2(
    client_id=client_id,
    client_secret=client_secret)

benchling = Benchling(url=f"https://{tenant_name}.benchling.com", auth_method=auth_method)

# Update this value to specify which schema this script will poll
entity_schema_to_check = "ts_AcqPy93F"

# Generates yesterday's date as a string formatted YYYY-MM-DD
today = datetime.today().date()
yesterday = today - timedelta(days=1)
time_string = f"> {yesterday.strftime('%Y-%m-%d')}"

species_list = ["Human", "Mouse", "Rat", "Camel"]

# Creates a generator returning DNA sequences matching the filter params
dna_sequences_list = benchling.dna_sequences.list(
    modified_at=time_string,
    schema_id=entity_schema_to_check,
    page_size=100,
)

def update_species_bulk(sequence_pages):
    # Loop through the pages provided by the generator, updating Species as required
    for page in sequence_pages:
        updates = []
        for sequence in page:
            current_species = sequence.fields["Species"].value
            if not current_species:
                species_to_add = random.choices(species_list)[0]
                update = DnaSequenceBulkUpdate(
                    id=sequence.id,
                    fields=fields(
                        {
                            "Species": {"value": species_to_add}
                        }
                    )
                )
                updates.append(update)
        if updates:
            # Calls the bulk update API and returns an AsyncTask we need to poll
            update_task_link = benchling.dna_sequences.bulk_update(updates)
            # Polls every 1 second up to 30 seconds by default to see if it completes
            update_task = benchling.tasks.wait_for_task(update_task_link.task_id)
            if update_task.status != AsyncTaskStatus.SUCCEEDED:
                raise RuntimeError(
                    f"Failed to update entities. Task ID: {update_task_link.task_id}\n"
                    f"Error: {update_task.errors}"
                )
            print("Completed bulk update for task", update_task_link.task_id)
        else:
            print("No entities found to update")

update_species_bulk(dna_sequences_list)
```

Next, [building your first app with App Configuration](/docs/building-your-first-app-with-app-configuration)
shows how to remove the tenant-specific values hardcoded above.