> 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 app with App Configuration

For this tutorial we'll use the simple app from
[Building your first Benchling App](/docs/building-your-first-benchling-app). To review,
that app updates the "Species" field for all DNA sequences of a certain schema. The full
code is included here for reference:

```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,
    token_url=f"https://{tenant_name}.benchling.com/oauth/token")

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)
```

What happens if you try to install and run this app on a different Benchling tenant? Two
lines cause problems:

1. `entity_schema_to_check = "ts_AcqPy93F"` hardcodes a tenant-unique ID for the entity
   schema. A user installing this app on a different tenant will have a different ID
   value, and might have an entirely different type of schema whose "Species" field they
   want autofilled.
2. `current_species = sequence.fields["Species"].value` references a schema field named
   "Species" *exactly*. On another tenant the field names are unlikely to match. The
   field might be called "Organism" instead.

An app manifest lets us remove both tenant-specific dependencies. First, create a YAML
file with a manifest that includes configuration options to capture these details:

**`manifest.yaml`**

```yaml title="manifest.yaml"
manifestVersion: 1

info:
  name: My App Name
  description: A user understandable description of what the app does, and support contact info.

configuration:
  - name: DNA Sequence Schema
    description: Select a DNA sequence schema with a species field for this app to autofill
    type: entity_schema
    fieldDefinitions:
      - name: Species
        description: Field that the app will write the species into.
```

This defines two configuration options: a field of type `entity_schema`, and a field
named `Species` on that schema. The goal is to reference these instead of tenant-specific
identifiers, so Benchling users can supply the correct values themselves. To do that, we
install the app using this manifest:

![The Benchling App Workspace listing installed apps](/_fern-files/_img/9db9ede88d5baae9e832563d75f258d269f46c0d8602b35709638aa8530f87e6.webp)![The settings page for an example Benchling App](/_fern-files/_img/56e9d01623bcffb6fdf6e6d0a02993d643fef4f27f6780056e8e380770dc3015.webp)

With the app created from the manifest, follow the steps in
[Managing and Sharing Benchling Apps](/docs/managing-and-sharing-benchling-apps) to
complete the installation. Then finish configuring the app: click the "Connections"
sidebar option, then the "Apps" tab, then "My App Name", then the "Configuration" tab.

![An app's Configuration tab showing the DNA Sequence Schema and Species fields from the manifest](/_fern-files/_img/f8879596c173661bc629383653570eaf6793c09e440c37bb64e1f729e5dc009c.webp)

Our schema configuration options, defined in the app manifest, are configurable from
this page. Select the schema on this tenant that the app should interact with:

![The app configuration page with an open dropdown menu of entity schemas](/_fern-files/_img/06f9b0c744ba58d7f7e2327051e33c9ff854d784363fcb2deecb204e87fd912f.webp)

Once the schema is selected, choose the field on that schema the app should use, then
submit the configuration:

![The app configuration page showing a selected entity schema and an empty Fields selector](/_fern-files/_img/46ae93127eb9de43fc24d7b2c98651233220c07ffc7660d67aa92f4d976ac8a3.webp)![The app configuration page with the Fields selector dropdown open](/_fern-files/_img/eb95747965e3be7ab8e85cabefb020b41bc3831a79133197e82d788635b894dc.webp)![The app configuration page showing both the entity schema and field selected](/_fern-files/_img/6d6a0771a0a63b11fe0f3dc8154b000bfdce6793fa77a4236d00fcf7d0f1b169.webp)

We're doing this ourselves for the tutorial, but the power of App Configuration is that
**a tenant admin can perform these steps themselves**, which is what lets you build
tenant-agnostic apps installable by multiple customers.

## Updating app code to use configuration

> **Warning**
>
> The example app has its tenant URL, credentials, and app ID hardcoded as variables for
> demonstration purposes. This isn't a best practice: using App Configuration requires
> storing these in a secure parameter store accessible by your app. We've omitted the
> details of that process here, since it's specific to your app's deployment
> environment.

Using the app's credentials, app ID, and tenant URL, apps can read their own
configuration through the
[Get app configuration items endpoint](/docs/api/v2/benchling-api/apps/list-app-configuration-items).
The response looks like this:

> **Info**
>
> ### Reading app configuration in V3
>
> [V3](/docs/v3-api-overview) is the recommended API for new development. It exposes app configuration read-only, as **app config values** rather than configuration items: `GET /api/v3/app-config-value/items` and `GET /api/v3/app-config-value/{app_config_value_id}`. Each value carries a `path` locating it in the configuration hierarchy and a `valueData` holding the value itself, typed by the app's config spec. That is a different shape from the V2 response below, not a renamed one.
>
> There is no V3 write path for app configuration, so anything that sets a config value stays on V2. The examples on this page are V2 throughout.

```json
{
  "appConfigurationItems": [
    {
      "apiURL": "https://piproduct.benchling.com/api/v2/app-configuration-items/aci_ksU0sJ8w",
      "app": {
        "id": "app_kcWboNOQCnKjFfGQ"
      },
      "description": "Select a DNA sequence schema with a species field for this app to autofill",
      "id": "aci_ksU0sJ8w",
      "linkedResource": {
        "id": "ts_R3UNHyel",
        "name": "*** DNA entity schema"
      },
      "modifiedAt": "2022-10-25T22:58:12.793897+00:00",
      "path": [
        "DNA Sequence Schema"
      ],
      "requiredConfig": false,
      "type": "entity_schema",
      "value": "tsf_AnUZB7nO"
    },
    {
      "apiURL": "https://piproduct.benchling.com/api/v2/app-configuration-items/aci_yM8hXMed",
      "app": {
        "id": "app_kcWboNOQCnKjFfGQ"
      },
      "description": "Field that the app will write the species into.",
      "id": "aci_yM8hXMed",
      "linkedResource": {
        "id": "tsf_kng5zz63",
        "name": "Species"
      },
      "modifiedAt": "2022-10-25T22:58:12.793897+00:00",
      "path": [
        "DNA Sequence Schema",
        "Species"
      ],
      "requiredConfig": false,
      "type": "field",
      "value": "tsf_AnUZB7nO"
    }
  ],
  "nextToken": ""
}
```

The top-level `appConfigurationItems` is an array of objects corresponding to all
configuration options, each including metadata and the value selected by the tenant
admin. We're specifically interested in `linkedResource`, an object holding the API ID
and name values we had previously hardcoded. We can update the app to pull this
configuration dynamically at startup:

```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,
    token_url=f"https://{tenant_name}.benchling.com/oauth/token")

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

# Pull the app config. First, define the variables we care about.
entity_schema_to_check = ""
species_schema_field_name = ""

# list_app_configuration_items() returns a page iterator, so cast it to a list
# to get all pages up front.
config_pages = list(
    benchling.apps.list_app_configuration_items(app_id="app_kcWboNOQCnKjFfGQ")
)

# Iterate through all pages and config items
for page in config_pages:
    for item in page:
        # Marshal the path property into a tuple for comparison
        path = tuple(item.path)
        # Store the relevant IDs in our previously defined variables
        if path == ("DNA Sequence Schema",):
            entity_schema_to_check = item.linked_resource.id
        if path == ("DNA Sequence Schema", "Species"):
            species_schema_field_name = item.linked_resource.name

# 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_schema_field_name].value
            if not current_species:
                species_to_add = random.choices(species_list)[0]
                update = DnaSequenceBulkUpdate(
                    id=sequence.id,
                    fields=fields(
                        {
                            species_schema_field_name: {"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)
```

Both original problems are now resolved:

* `dna_sequences.list(..., schema_id=entity_schema_to_check)` uses the resource ID from
  App Configuration instead of a hardcoded ID value.
* `sequence.fields[species_schema_field_name].value` depends on the field listed in App
  Configuration instead of the hardcoded name `"Species"`.

These changes let the app be installed and configured on any Benchling tenant without
code changes. Users can also rename fields and change schemas without affecting the
app's functionality, as long as they update the corresponding values in App
Configuration.

Each app configuration item also includes other metadata like `id` and `apiURL`. In
future guides we'll cover best practices for interacting with app configuration options
in a more abstract way.