
## Build your first metadata workflow

URL: https://docs.atlan.com/product/capabilities/build-apps/sdks/python/tutorials/build-your-first-metadata-workflow

> A hands-on walkthrough of reading and updating Atlan metadata using the Python SDK (pyatlan) with real SDK methods and patterns.

:::tip
You might also like the [Atlan Platform Essentials certification](https://university.atlan.com/certifications/f4d88500-a7a7-11ed-80f9-06c0361096e5).
:::

This walkthrough takes you from connecting to Atlan through reading and updating metadata—in one guided session. You work with real SDK methods, understand why they work the way they do, and finish with the core patterns you use in almost every integration you build.

## Connect to Atlan

Every SDK operation starts with an `AtlanClient`. Create one by providing your tenant URL and an API token:

### Java

The SDK is available on [Maven Central](https://central.sonatype.com/artifact/com.atlan/atlan-java), ready to be included in your project:

```kotlin title="build.gradle.kts"
repositories {
 mavenCentral()
}

dependencies {
 implementation("com.atlan:atlan-java:+") // (1)
 testRuntimeOnly("ch.qos.logback:logback-classic:1.2.11") // (2)
}
```

1. Include the latest version of the Java SDK in your project as a dependency. You can also give a specific version instead of the `+`, if you'd like.
2. The Java SDK uses slf4j for logging purposes. You can include logback as a simple binding mechanism to send any logging information out to your console (standard out).

Provide two values to create an Atlan client:

```java showLineNumbers title="AtlanLiveTest.java"

public class AtlanLiveTest {
 public static void main(String[] args)
 }
}
```

1. Provide your Atlan tenant URL as the first parameter. You can also read the value from an environment variable, if you leave out both parameters.
2. Provide your [API token](https://ask.atlan.com/hc/en-us/articles/8312649180049) as the second parameter. You can also read the value from another environment variable, by leaving out this parameter.
3. You can then start writing some actual code to run within a static `main` method. (Examples appear further in this tutorial.) Once the block is complete, any resources held by the client (that is, for caching) are automatically released.

:::info[Set up logging for SDK]
You can also checkout to the [advanced configuration section](https://docs.atlan.com/llms/platform/python/set-up-sdk/llms.txt)
of the SDK to learn about how to set up logging.
:::

### Python

The SDK is available on [PyPI](https://pypi.org/project/pyatlan/). You can use pip to install it as follows:

```console title="Install the SDK"
pip install pyatlan
```

Provide two values to create an Atlan client:

```python showLineNumbers title="atlan_live_test.py"
from pyatlan.client.atlan import AtlanClient

client = AtlanClient(
 base_url="https://tenant.atlan.com", # (1)
 api_key="..." # (2)
)
```

1. Provide your Atlan tenant URL to the `base_url` parameter. (You can also [do this through environment variables](https://docs.atlan.com/llms/platform/python/set-up-sdk/llms.txt).)
2. Provide your [API token](https://ask.atlan.com/hc/en-us/articles/8312649180049) to the `api_key` parameter. (You can also [do this through environment variables](https://docs.atlan.com/llms/platform/python/set-up-sdk/llms.txt).)

:::info[Set up logging for SDK]
You can also checkout to the [advanced configuration section](https://docs.atlan.com/llms/platform/python/set-up-sdk/llms.txt)
of the SDK to learn about how to set up logging.
:::

### Kotlin

The SDK is available on [Maven Central](https://central.sonatype.com/artifact/com.atlan/atlan-java), ready to be included in your project:

```kotlin title="build.gradle.kts"
repositories {
 mavenCentral()
}

dependencies {
 implementation("com.atlan:atlan-java:+") // (1)
 implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") // (2)
 implementation("org.slf4j:slf4j-simple:2.0.7")
}
```

1. Include the latest version of the Java SDK in your project as a dependency. You can also give a specific version instead of the `+`, if you'd like.
2. The Java SDK uses slf4j for logging purposes. You can include slf4j-simple as a simple binding mechanism to send any logging information out to your console (standard out), along with the `kotlin-logging-jvm` microutil.

Provide two values to create an Atlan client:

```kotlin showLineNumbers title="AtlanLiveTest.kt"

fun main()
}
```

1. Provide your Atlan tenant URL as the first parameter. You can also read the value from an environment variable, if you leave out both parameters.
2. Provide your [API token](https://ask.atlan.com/hc/en-us/articles/8312649180049) as the second parameter. You can also read the value from another environment variable, by leaving out this parameter.
3. You can then start writing some actual code to run within a static `main` method. (Examples appear further in this tutorial.) Once the block is complete, any resources held by the client (that is, for caching) are automatically released.

:::info[Set up logging for SDK]
You can also checkout to the [advanced configuration section](https://docs.atlan.com/llms/platform/python/set-up-sdk/llms.txt)
of the SDK to learn about how to set up logging.
:::

### Go

The SDK is available on [GitHub](https://github.com/atlanhq/atlan-go), ready to be included in your project:

```go showLineNumbers title="main.go"
package main

 "github.com/atlanhq/atlan-go/atlan/assets"
)
```

Provide two values to set up connectivity to Atlan:

```go title="main.go"
func main()
```

1. Provide your Atlan tenant URL to the `assets.Context()` method. If you prefer using the value from an environment variable, you can use `assets.NewContext()` without any parameters.
2. Provide your [API token](https://ask.atlan.com/hc/en-us/articles/8312649180049) as the second parameter to the `assets.Context()` method. (Or again, have it picked up automatically by the `assets.NewContext()` method.)

:::info[Set up logging for SDK]
You can also checkout to the [advanced configuration section](https://docs.atlan.com/llms/platform/python/set-up-sdk/llms.txt)
of the SDK to learn about how to set up logging.
:::
<!-- vale off -->
:::warning[Don't forget to give permissions]
If you want to be able to access existing metadata with an API token, don't forget that you need to [assign one or more personas](https://ask.atlan.com/hc/en-us/articles/8312649180049#token-permissions-0-2) to the [API token](https://ask.atlan.com/hc/en-us/articles/8312649180049) that grant it access to metadata.
:::
<!-- vale on -->

## Understand assets and identifiers

Before writing any retrieval or update code, you need two mental models: what an asset is, and how Atlan uniquely identifies each one. These concepts underpin every SDK operation.

### Assets

In Atlan, every object that provides context to your data is called an **asset**.

```mermaid
classDiagram
 class Table {
 certificateStatus
 announcementType
 columnCount
 rowCount
 ...
 atlanSchema()
 columns()
 }
 class Column {
 certificateStatus
 announcementType
 dataType
 isNullable
 ...
 table()
 }
 Table *-- Column
```

Each type of asset has a set of:

- *Properties*, such as:

 - Certificates
 - Announcements

- *Relationships* to other assets, such as:

 - Schema child tables
 - Table parent schema
 - Table child columns
 - Column parent table

> *Assets are *instances* of metadata. — see full content on the documentation site.*

There are many different kinds of assets—tables, columns, schemas, databases, BI dashboards, reports, and more. Assets inter-relate with each other and share common properties (like certificates) while also having properties unique to their type (like `columnCount`, which only exists on tables, not on schemas or databases).

### Every asset has two identifiers {#importance-of-identifiers}

Every operation that reads or writes an asset requires an identifier. Atlan uses two, and understanding the difference between them is important before you write any update code:

#### GUID {#guid-1}

Atlan uses globally-unique identifiers (GUIDs) to uniquely identify each asset, _globally_. They look something like this:

`17f0356e-75f6-4e0b-8b05-32cebe8cd953`

As the name implies, GUIDs are:

- [x] Globally unique (across all systems).

They're:

- [x] ***Generated*** in a way that makes it nearly impossible for anything else to ever generate that same ID.[^2]

Note that this means the GUID itself is `_not_`:

- [] Meaningful or capable of being interpreted in any way

#### `qualifiedName` {#qualifiedname-1}

Atlan uses `qualifiedName`s to uniquely identify assets based on their characteristics. They look something like this:

`default/snowflake/1234567890/DB/SCHEMA`

Qualified names are `_not_`:

- [] Globally unique (across all systems).

Instead, they're:

- [x] Consistently ***constructed*** in a meaningful way, making it possible for them to be reconstructed.

Note that this means the `qualifiedName` is:

- [x] Meaningful and capable of being interpreted

:::info[How these impact updates]
Since they're truly unique, operations that include a GUID only *update* an asset, not create one. Conversely, operations that take a `qualifiedName` can:

- **Create** an asset, if no ***exactly-matching*** `qualifiedName` is found in Atlan.
- **Update** an asset, if an exact-match for the `qualifiedName` is found in Atlan.

These operations also require a `typeName`, so that if creation does occur the correct type of asset is created.
:::

:::warning[Unintended consequences of this behavior]
Be careful when using operations with only the `qualifiedName`. You may end up creating assets when you were only expecting them to be updated or to fail if they didn't already exist. This is particularly true when you don't give the _exact, case-sensitive_ `qualifiedName` of an asset. `a/b/c/d` is _not_ the same as `a/B/c/d` when it comes to `qualifiedName`s.
:::

Perhaps this leaves you wondering: why have a `qualifiedName` at all?

The `qualifiedName`'s purpose is to identify what's a unique asset. Many different tools might all have information about that asset. Having a common "identity" means that many different systems can each *independently* construct its identifier the same way.

- If a crawler gets table details from Snowflake it can upsert based on those identity characteristics in Atlan. The crawler won't create duplicate tables every time it runs. This gives idempotency.
- Looker knows the same identity characteristics for the Snowflake tables and columns. So if you get details from Looker about the tables it uses for reporting, you can link them together in lineage. (Looker can construct the same identifier for the table as Snowflake itself.)

These characteristics aren't possible using GUIDs alone.

## Retrieve metadata

With a client connected and a mental model in place, you're ready to read data from Atlan. There are two patterns: fetch directly by identifier when you know it, or search by criteria when you don't.

### Retrieve asset by identifier

Use `get_by_guid()` or `get_by_qualified_name()` to fetch a single known asset. Both methods take the asset type and the identifier and return the full asset object.

### Java

```java title="Retrieve an asset (AtlanLiveTest.java)"
try (AtlanClient client = new AtlanClient())
```

1. You can retrieve an asset using the static `get()` method on any asset type, providing the client and either the asset's GUID or `qualifiedName`. (Each asset type is its own unique class in the SDK.)

### Python

```python title="Retrieve an asset (atlan_live_test.py)"
table = client.asset.get_by_guid( # (1)
 asset_type=Table,
 guid="b4113341-251b-4adc-81fb-2420501c30e6"
)
table = client.asset.get_by_qualified_name(
 asset_type=Table,
 qualified_name="default/snowflake/1234567890/MY_DB/MY_SCHEMA/MY_TABLE"
)
```

1. You can retrieve an asset using the `asset.get_by_guid()` method on the Atlan client, providing both the type of asset you expect to retrieve and its GUID. (Each asset type is its own unique class in the SDK.)
2. You can also retrieve an asset using the `asset.get_by_qualified_name()` method on the Atlan client, providing the type of asset you expect to retrieve and its `qualified_name`. (Each asset type is its own unique class in the SDK.)

### Kotlin

```kotlin title="Retrieve an asset (AtlanLiveTest.kt)"
AtlanClient().use { client ->
 var table = Table.get(client, "b4113341-251b-4adc-81fb-2420501c30e6") // (1)
 table = Table.get(client, "default/snowflake/1234567890/MY_DB/MY_SCHEMA/MY_TABLE")
}
```

1. You can retrieve an asset using the static `get()` method on any asset type, providing the client and either the asset's GUID or `qualifiedName`. (Each asset type is its own unique class in the SDK.)

### Go

```go title="Request an asset (main.go)"
response, err := assets.GetByGuid[*assets.Table]( // (1)
 "b4113341-251b-4adc-81fb-2420501c30e6"
)
response, err := assets.GetByQualifiedName[*assets.Table]( // (2)
 "default/snowflake/1234567890/MY_DB/MY_SCHEMA/MY_TABLE"
)
```

1. You can retrieve an asset using the `assets.GetByGuid()` method on the Atlan client, providing both the type of asset you expect to retrieve and its GUID. (Each asset type is its own unique class in the SDK.)
2. You can also retrieve an asset using the `assets.GetByQualifiedName()` method on the Atlan client, providing the type of asset you expect to retrieve and its `qualifiedName`. (Each asset type is its own unique class in the SDK.)

Note that the response is strongly typed:

- If you are retrieving a table, you get a table back (as long as it exists).
- You don't need to figure out what properties or relationships exist on a table—the `Table` class defines them for you already.

In any modern IDE, this means you have type-ahead support for retrieving the properties and relationships from the `table` variable. You can also refer to the [types reference](https://docs.atlan.com/llms/platform/types/llms.txt) in this portal for full details of every kind of asset.

:::warning[Retrieval by identifier can be more costly than you might expect]
Even though you are retrieving an asset by an identifier, this can be more costly than you might expect. Retrieving an asset in this way:

- Retrieve all its properties and their values
- Retrieve all its relationships

Imagine the asset you are retrieving has 100's or 1000's of these. If you only care about its certificate and any owners, you end up retrieving far more information than you need.
:::

### Search for assets by criteria

Use `FluentSearch` when you don't know an asset's identifier, or when you want to retrieve many assets that share a common set of characteristics. Build a query, convert it to a request, and iterate the results—the SDK handles pagination automatically.

### Java

```java title="Search for an asset (AtlanLiveTest.java)"
try (AtlanClient client = new AtlanClient())
```

1. You can search all active assets of a given type using the `select()` static method.
2. Chain onto this method any conditions you want to apply to the search, in this example a `where` clause that matches any table whose name equals `MY_TABLE`.
3. You can then stream the results from this search and process them as any standard Java stream: filter them, limit them, or apply an action to each one. The results of the search are automatically paged and each page is lazily-fetched.

### Python

```python title="Search for an asset (atlan_live_test.py)"
from pyatlan.model.fluent_search import FluentSearch
from pyatlan.model.assets import Table

request = (
 FluentSearch() # (1)
 .where(FluentSearch.asset_type(Table))
 .where(FluentSearch.active_assets())
 .where(Table.NAME.eq("MY_TABLE")) # (2)
).to_request() # (3)
tables = []
for result in client.asset.search(request): # (4)
 tables.append(result)
```

1. You can search all active assets of a given type by creating a `FluentSearch()` object and chaining two `where` clauses:

 - `FluentSearch.asset_type` to limit to a particular kind of asset
 - `FluentSearch.active_assets()` to limit to only active assets of that kind

2. Chain onto this method any conditions you want to apply to the search, in this example a `where` clause that matches any table whose name equals `MY_TABLE`.
3. You can then convert this object into a search request using the `to_request()` method.
4. Run the request using the `asset.search()` method on the Atlan client, and you can directly iterate through the search results. The results of the search are automatically paged and each page is lazily-fetched.

### Kotlin

```kotlin title="Search for an asset (AtlanLiveTest.kt)"
AtlanClient().use { client ->
 val tables = Table.select(client) // (1)
 .where(Table.NAME.eq("MY_TABLE")) // (2)
 .stream() // (3)
 .toList()
}
```

1. You can search all active assets of a given type using the `select()` static method.
2. Chain onto this method any conditions you want to apply to the search, in this example a `where` clause that matches any table whose name equals `MY_TABLE`.
3. You can then stream the results from this search and process them as any standard Kotlin stream: filter them, limit them, or apply an action to each one. The results of the search are automatically paged and each page is lazily-fetched.

### Go

```go title="Search for an asset (main.go)"
searchResponse, err := assets.NewFluentSearch(). // (1)
 ActiveAssets().
 PageSizes(300).
 Where(ctx.Table.NAME.Eq("MY_TABLE")). // (2)
 Execute() // (3)

entities, errIter := searchResponse.Iter() // (4)

for asset := range entities {
	fmt.Println("Asset name:", *asset.Name)
}

if err := <-errIter; err != nil {
	fmt.Println("Error during iteration:", err)
}
```

1. You can search all active assets of a given type using the `NewFluentSearch()` method.
2. Chain onto this method any conditions you want to apply to the search, in this example a `Where` clause that matches any table whose name equals `MY_TABLE`.
3. You can then run the request using `Execute()`.
4. You can directly iterate through the search results. The SDK handles pagination for you, fetching each page lazily as needed.

By default, the search only returns minimal information about each asset (only its identifiers). However, you can also specify what information you want.

For example, if you want to know the certificate of the asset you only need to tack that onto the query:

To request specific properties alongside each result, add `include_on_results()` to your query. This also improves performance—you retrieve only what you need instead of fetching the full asset separately:

### Java

```java hl_lines="4" title="Search for an asset (AtlanLiveTest.java)"
try (AtlanClient client = new AtlanClient())
```

1. Only this line differs from the original query. You can chain as many `includeOnResults` calls as you want to specify the properties and relationships you want to retrieve for matching assets.

### Python

```python hl_lines="9" title="Search for an asset (atlan_live_test.py)"
from pyatlan.model.fluent_search import FluentSearch
from pyatlan.model.assets import Table

request = (
 FluentSearch()
 .where(FluentSearch.asset_type(Table))
 .where(FluentSearch.active_assets())
 .where(Table.NAME.eq("MY_TABLE"))
 .include_on_results(Table.CERTIFICATE_STATUS) # (1)
).to_request()
tables = []
for result in client.asset.search(request):
 tables.append(result)
```

1. Only this line differs from the original query. You can chain as many `include_on_results` calls as you want to specify the properties and relationships you want to retrieve for matching assets.

### Kotlin

```kotlin hl_lines="4" title="Search for an asset (AtlanLiveTest.kt)"
AtlanClient().use { client ->
 val tables = Table.select(client)
 .where(Table.NAME.eq("MY_TABLE"))
 .includeOnResults(Table.CERTIFICATE_STATUS) // (1)
 .stream()
 .toList()
}
```

1. Only this line differs from the original query. You can chain as many `includeOnResults` calls as you want to specify the properties and relationships you want to retrieve for matching assets.

### Go

```go hl_lines="5" title="Search for an asset (main.go)"
searchResponse, err := assets.NewFluentSearch().
 ActiveAssets().
 PageSizes(300).
 Where(ctx.Table.NAME.Eq("MY_TABLE")).
 IncludeOnResults(assets.CERTIFICATE_STATUS). // (1)
 Execute()

entities, errIter := searchResponse.Iter()

for asset := range entities {
	fmt.Println("Asset name:", *asset.Name)
}

if err := <-errIter; err != nil {
	fmt.Println("Error during iteration:", err)
}
```

1. Only this line differs from the original query. You can include as many attributes in `IncludeOnResults` as you want to specify the properties and relationships you want to retrieve for matching assets.

<!-- vale off -->
:::tip[Also gives the best performance]
Searching not only allows you to find an asset without knowing its identifier, it also improves retrieval performance. You no longer retrieve information you don't need—you can specify precisely the properties and relationships you want.
:::
<!-- vale on -->

## Update metadata

<!-- vale off -->
Most operations that write to Atlan are *upserts*—they create the asset if it doesn't exist, or update it if it does. This section covers two patterns: updating a single asset, and making bulk changes across many assets at once.
<!-- vale on -->

### Update a single asset

Use the `updater()` method to build a minimal change set. Provide the asset's identifier and only the properties you want to change—Atlan merges these into the existing asset and leaves everything else untouched.

### Java

```java title="Update an asset (AtlanLiveTest.java)"
try (AtlanClient client = new AtlanClient())
```

1. You can update an asset *without* first looking the asset up, if you know (can construct) its identifying `qualifiedName`. Using the `updater()` static method on any asset type, you pass in (typically) the `qualifiedName` and name of the asset. This returns a builder onto which you can then chain any updates.
2. You can then chain onto the returned builder as many updates as you want. In this example, this sets the certificate status to `VERIFIED`.
3. At the end of your chain of updates, you need to build the builder (into an object, in-memory).
4. And then, finally, you need to `.save()` that object to persist those changes in Atlan (passing the client for the tenant you want to save it in). The response contains details of the change: whether the asset was created, updated, or nothing happened because the asset already had those changes.

### Python

```python title="Update an asset (atlan_live_test.py)"
from pyatlan.model.assets import Table
from pyatlan.model.enums import CertificateStatus

to_update = Table.updater( # (1)
 qualified_name="default/snowflake/1234567890/MY_DB/MY_SCHEMA/MY_TABLE",
 name="MY_TABLE",
)
to_update.certificate_status = CertificateStatus.VERIFIED # (2)
response = client.asset.save(to_update) # (3)
```

1. You can update an asset *without* first looking the asset up, if you know (can construct) its identifying `qualified_name`. Using the `updater()` class method on any asset type, you pass in (typically) the `qualified_name` and name of the asset.
2. You can then add onto the returned object as many updates as you want. In this example, this sets the certificate status to `VERIFIED`.
3. And then, finally, you need to `client.asset.save()` that object to persist those changes in Atlan. The response contains details of the change: whether the asset was created, updated, or nothing happened because the asset already had those changes.

### Kotlin

```kotlin title="Update an asset (AtlanLiveTest.kt)"
AtlanClient().use { client ->
 val toUpdate = Table.updater( // (1)
 "default/snowflake/1234567890/MY_DB/MY_SCHEMA/MY_TABLE",
 "MY_TABLE")
 .certificateStatus(CertificateStatus.VERIFIED) // (2)
 .build() // (3)
 val response = toUpdate.save(client) // (4)
}
```

1. You can update an asset *without* first looking the asset up, if you know (can construct) its identifying `qualifiedName`. Using the `updater()` static method on any asset type, you pass in (typically) the `qualifiedName` and name of the asset. This returns a builder onto which you can then chain any updates.
2. You can then chain onto the returned builder as many updates as you want. In this example, this sets the certificate status to `VERIFIED`.
3. At the end of your chain of updates, you need to build the builder (into an object, in-memory).
4. And then, finally, you need to `.save()` that object to persist those changes in Atlan (passing the client for the tenant you want to save it in). The response contains details of the change: whether the asset was created, updated, or nothing happened because the asset already had those changes.

### Go

```go title="Update an asset (main.go)"
toUpdate := &assets.Table{} // (1)
toUpdate.Updater(
 "default/snowflake/1234567890/MY_DB/MY_SCHEMA/MY_TABLE",
 "MY_TABLE"
)
toUpdate.CertificateStatus = &atlan.CertificateStatusVerified // (2)
response, err := assets.Save(toUpdate) // (3)
```

1. You can update an asset *without* first looking the asset up, if you know (can construct) its identifying `qualifiedName`. Using the `Updater()` method on any asset type, you pass in (typically) the `qualifiedName` and name of the asset. This returns an object into which you can then place any updates.
2. You can place into the returned object as many updates as you want. In this example, this sets the certificate status to `VERIFIED`.
3. And then, finally, you need to `.Save()` that object to persist those changes in Atlan. The response contains details of the change: whether the asset was created, updated, or nothing happened because the asset already had those changes.

:::tip[Atlan handles idempotency]
By sending only the changes you want to apply, Atlan can make idempotent updates.

- Atlan only attempts to update the asset with the changes you send.
- Atlan leaves any existing metadata on the asset as-is.
- If the asset already has the metadata values you are sending, Atlan does nothing. It won't even update audit details like the last update timestamp, and is thus idempotent.
:::

### Update many assets at once

To update multiple assets efficiently, combine `FluentSearch` and `Batch`. Use search to find the assets you want to change, call `trim_to_required()` on each result to strip it down to its identifier, apply your changes, and pass it to the batch. The batch handles grouping and sending updates to Atlan automatically.

### Java

```java title="Bulk changes (AtlanLiveTest.java)"
try (AtlanClient client = new AtlanClient()));
 batch.flush(); // (8)
 List created = batch.getCreated(); // (9)
 List updated = batch.getUpdated();
}
```

1. Start by initializing a batch. Through this batch, you can automatically queue up and bulk-upsert assets—in this example, 20 at a time.
2. Then use the search pattern discussed earlier to find all the assets you want to update.
3. Be sure to include any details you might need to make a decision about whether to update the asset or not (and what to update it with).
4. It's a good idea to set the page size for search results to match the asset batch size, for maximal efficiency.
5. When you stream the results of the search, you can send an optional boolean parameter. If set to `true`, the SDK streams the pages of results in parallel (across multiple threads), improving throughput.
6. When you then operate on each search result, you can `add()` any updates directly into the batch you created earlier. The batch itself handles saving these to Atlan when a sufficient number have been queued up (20, in this example).
7. To make an update to a search result, first call `trimToRequired()` on the result. This pares down the asset to its minimal required attributes and return a builder. You can then chain as many updates onto this builder as you want, keeping to the same pattern—ensuring you are sending only changes.
8. You must `flush()` the batch outside of any loop where you've added assets into it. This ensures any final remaining elements in the batch are still sent to Atlan, even if the batch isn't "full."
9. Finally, from the batch you can retrieve the minimal details about any assets it created or updated.

### Python

```python title="Bulk changes (atlan_live_test.py)"
from pyatlan.model.fluent_search import FluentSearch
from pyatlan.model.assets import Table
from pyatlan.client.asset import Batch
from pyatlan.model.enums import CertificateStatus

batch = Batch(client.asset, max_size=20) # (1)
request = ( # (2)
 FluentSearch()
 .where(FluentSearch.asset_type(Table))
 .where(FluentSearch.active_assets())
 .where(Table.NAME.eq("MY_TABLE"))
 .include_on_results(Table.CERTIFICATE_STATUS) # (3)
 .page_size(20) # (4)
).to_request()
tables = []
for result in client.asset.search(request):
 revised = result.trim_to_required() # (5)
 revised.certificate_status = CertificateStatus.DEPRECATED
 batch.add(revised) # (6)
batch.flush() # (7)
created = batch.created # (8)
updated = batch.updated
```

1. Start by initializing a batch. Through this batch, you can automatically queue up and bulk-upsert assets—in this example, 20 at a time.
2. Then use the search pattern discussed earlier to find all the assets you want to update.
3. Be sure to include any details you might need to make a decision about whether to update the asset or not (and what to update it with).
4. It's a good idea to set the page size for search results to match the asset batch size, for maximal efficiency.
5. When you then operate on each search result, first call `trim_to_required()` on the result. This pares down the asset to its minimal required attributes. You can then add as many updates onto this object as you want, keeping to the same pattern—ensuring you are sending only changes.
6. You can then `add()` any updated objects directly into the batch you created earlier. The batch itself handles saving these to Atlan when a sufficient number have been queued up (20, in this example).
7. You must `flush()` the batch outside of any loop where you've added assets into it. This ensures any final remaining elements in the batch are still sent to Atlan, even if the batch isn't "full."
8. Finally, from the batch you can retrieve the minimal details about any assets it created or updated.

### Kotlin

```kotlin title="Bulk changes (AtlanLiveTest.kt)"
AtlanClient().use { client ->
 val batch = ParallelBatch(client, 20) // (1)
 Table.select(client) // (2)
 .where(Table.NAME.eq("MY_TABLE"))
 .includeOnResults(Table.CERTIFICATE_STATUS) // (3)
 .pageSize(20) // (4)
 .stream(true) // (5)
 .forEach { a ->
 batch.add( // (6)
 a.trimToRequired() // (7)
 .certificateStatus(CertificateStatus.DEPRECATED)
 .build())
 }
 batch.flush() // (8)
 val created = batch.created // (9)
 val updated = batch.updated
}
```

1. Start by initializing a batch. Through this batch, you can automatically queue up and bulk-upsert assets—in this example, 20 at a time.
2. Then use the search pattern discussed earlier to find all the assets you want to update.
3. Be sure to include any details you might need to make a decision about whether to update the asset or not (and what to update it with).
4. It's a good idea to set the page size for search results to match the asset batch size, for maximal efficiency.
5. When you stream the results of the search, you can send an optional boolean parameter. If set to `true`, the SDK streams the pages of results in parallel (across multiple threads), improving throughput.
6. When you then operate on each search result, you can `add()` any updates directly into the batch you created earlier. The batch itself handles saving these to Atlan when a sufficient number have been queued up (20, in this example).
7. To make an update to a search result, first call `trimToRequired()` on the result. This pares down the asset to its minimal required attributes and return a builder. You can then chain as many updates onto this builder as you want, keeping to the same pattern—ensuring you are sending only changes.
8. You must `flush()` the batch outside of any loop where you've added assets into it. This ensures any final remaining elements in the batch are still sent to Atlan, even if the batch isn't "full."
9. Finally, from the batch you can retrieve the minimal details about any assets it created or updated.

### Go

```go title="Bulk changes (main.go)"
batch := assets.NewBatch(ctx, 20, true, atlan.IGNORE, true) // (1)
searchResponse, _ := assets.NewFluentSearch(). // (2)
 AssetType("Table").
 ActiveAssets().
	Where(ctx.Table.NAME.Eq("MY_TABLE")).
 IncludeOnResults(assets.CERTIFICATE_STATUS). // (3)
 PageSizes(20). // (4)
	Execute()

entities, errIter := searchResponse.Iter()

for asset := range entities {
 revised, err := assets.TrimToRequired(*asset) // (5)
 if err != nil {
 	logger.Log.Errorf("Error trimming asset: %v", err)
 }
 revised.CertificateStatus = &atlan.CertificateStatusVerified
 err = batch.Add(revised) // (6)
	if err != nil {
		logger.Log.Errorf("Failed to add asset to batch: %v", err)
	}
}

if err := <-errIter; err != nil {
	fmt.Println("Error during iteration:", err)
}

batch.Flush() // (7)

for _, asset := range batch.Created()
for _, asset := range batch.Updated()
```

1. Start by initializing a batch. Through this batch, you can automatically queue up and bulk-upsert assets—in this example, 20 at a time.
2. Then use the search pattern discussed earlier to find all the assets you want to update.
3. Be sure to include any details you might need to make a decision about whether to update the asset or not (and what to update it with).
4. It's a good idea to set the page size for search results to match the asset batch size, for maximal efficiency.
5. When you then operate on each search result, first call `TrimToRequired()` on the result. This pares down the asset to its minimal required attributes. You can then add as many updates onto this object as you want, keeping to the same pattern—ensuring you are sending only changes.
6. You can then `add()` any updated objects directly into the batch you created earlier. The batch itself handles saving these to Atlan when a sufficient number have been queued up (20, in this example).
7. You must `flush()` the batch outside of any loop where you've added assets into it. This ensures any final remaining elements in the batch are still sent to Atlan, even if the batch isn't "full."
8. Finally, from the batch you can retrieve the minimal details about any assets it created or updated.

## What's next

Now that you know the core patterns: connect, retrieve, search, update, you can explore further using search (upper-right) or the top-level menu.

---
