Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encord.com/llms.txt

Use this file to discover all available pages before exploring further.

Label branches let you keep multiple versions of a Project’s labels side-by-side. Use them to preserve a labeled state before a re-labeling pass, run an experiment without disturbing main, or hold a reviewer-only copy of labels. You create a new label version by copying labels from an existing branch onto a new branch with project.copy_labels_to_branch(). Every Project starts with a main branch. Copying from main (or any branch) into a new branch name creates that branch and populates it with the matching label rows.
Label branches are the recommended method for Pre-labeling Agents and Exporting Labels Agents.
  • A Pre-labeling Agent writes its model predictions onto a dedicated branch (for example, pre-labels-v1) instead of main. Annotators continue working on main undisturbed, and you can review or merge the agent’s output on its own branch with copy_labels_to_branch(..., overwrite=True) once it’s been validated.
  • An Exporting Labels Agent copies the current state of main onto a frozen snapshot branch (for example, export-2026-05-12) before export. The export reads from that branch via list_label_rows_v2(branch_name=...), so the result is reproducible even if annotation continues on main in parallel.
project.copy_labels_to_branch(
    target_branch="<new-branch-name>",
    source_branch="main",
    overwrite=False,
    data_hashes=None,
    label_hashes=None,
    batch_size=50,
)
The method returns the total number of label rows created or updated on the target branch.
source_branch and target_branch MUST be different. Passing the same name for both raises a ValueError.
By default, data units that already have a label row on target_branch are skipped. Set overwrite=True to replace existing label rows on the target branch with the source content.
All examples on this page use bundle when calling initialise_labels() or save() to reduce network round-trips. Keep bundle sizes under 1000 operations — larger bundles can degrade performance. A good starting point is BUNDLE_SIZE = 100; for videos with many labels, start at 20 and adjust. copy_labels_to_branch() does not need a bundle — it batches internally via its own batch_size parameter (default 50).
The examples below are intended as quick tests and cap themselves at the first MAX_LABEL_ROWS = 10 label rows so you can validate the workflow safely. Remove that cap deliberately before running against a full Project — copying or overwriting labels on tens of thousands of rows without filtering can take a long time and is hard to undo.

Copy All Labels to a New Branch

The following example creates a new label version named pre-label-v1 by copying every label row on main into the new branch. This is the most common pattern: snapshot the current label state before making changes.
Copy all labels to a new branch
# Import dependencies
from encord import EncordUserClient

SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
TARGET_BRANCH = "pre-label-v1"  # Replace with the name of the new branch
BATCH_SIZE = 50  # Number of label rows copied per API request (server-side batching)
MAX_LABEL_ROWS = 10  # Quick-test cap. Remove or raise this for a full Project copy.

user_client = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path=SSH_PATH,
    # For US platform users use domain="https://api.us.encord.com"
    domain="https://api.encord.com",
)

project = user_client.get_project(PROJECT_ID)

# Pick the first MAX_LABEL_ROWS data units from the source branch.
# Metadata-only — no initialisation needed, so no bundle required here.
source_rows = project.list_label_rows_v2(branch_name="main")
data_hashes = [row.data_hash for row in source_rows[:MAX_LABEL_ROWS]]

# copy_labels_to_branch batches internally; no bundle required here.
rows_copied = project.copy_labels_to_branch(
    target_branch=TARGET_BRANCH,
    source_branch="main",
    data_hashes=data_hashes,
    batch_size=BATCH_SIZE,
)

print(f"Copied {rows_copied} label rows from 'main' to '{TARGET_BRANCH}' (test run).")

Copy Labels for Specific Data Units

To version labels for only a subset of your Project, pass the data_hashes (or label_hashes) you want to include. Everything outside that list stays untouched on the target branch. When you need to discover the label_hashes first, fetch them with list_label_rows_v2() from the source branch. You only need to initialise label rows (with a bundle) when you intend to read their contents — for the hash-discovery step alone, the metadata returned by list_label_rows_v2() is enough.
# Import dependencies
from encord import EncordUserClient

SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
TARGET_BRANCH = "subset-v1"  # Replace with the name of the new branch
BATCH_SIZE = 50

DATA_HASHES = [
    "11111111-1111-1111-1111-111111111111",
    "22222222-2222-2222-2222-222222222222",
]  # Replace with the data hashes you want to copy

user_client = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path=SSH_PATH,
    domain="https://api.encord.com",
)

project = user_client.get_project(PROJECT_ID)

rows_copied = project.copy_labels_to_branch(
    target_branch=TARGET_BRANCH,
    source_branch="main",
    data_hashes=DATA_HASHES,
    batch_size=BATCH_SIZE,
)

print(f"Copied {rows_copied} label rows for {len(DATA_HASHES)} data units to '{TARGET_BRANCH}'.")
When both data_hashes and label_hashes are specified, data_hashes is applied first and label_hashes is applied to the result.

Switch to a Branch and Read Labels

Once a branch exists, use list_label_rows_v2() with branch_name to fetch the label rows on that branch. Initialise each label row inside a create_bundle() context so all the download calls are batched into a single round-trip.
Read labels from a branch
# Import dependencies
from encord import EncordUserClient

SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
BRANCH_NAME = "pre-label-v1"  # Replace with the branch you want to read from
BUNDLE_SIZE = 100  # Keep bundle sizes under 1000 for best performance
MAX_LABEL_ROWS = 10  # Quick-test cap. Remove this slice to read every label row on the branch.

user_client = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path=SSH_PATH,
    domain="https://api.encord.com",
)

project = user_client.get_project(PROJECT_ID)

# Fetch label rows for a specific branch, then cap at MAX_LABEL_ROWS for the test run.
label_rows = project.list_label_rows_v2(branch_name=BRANCH_NAME)[:MAX_LABEL_ROWS]

# Initialise labels using bundles to reduce network calls
with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
    for label_row in label_rows:
        label_row.initialise_labels(bundle=bundle)

for label_row in label_rows:
    print(f"{label_row.data_title}: {len(label_row.get_object_instances())} object instances on '{BRANCH_NAME}'")
branch_name cannot be combined with include_all_label_branches=True. Pick one: either query a single branch by name, or pull every branch as separate label row objects.
To compare versions, list rows on each branch and inspect their contents. Bundle the initialisation calls for each branch separately so each batch maps cleanly to one branch.
Compare two branches
# Import dependencies
from encord import EncordUserClient

SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
SOURCE_BRANCH = "main"
TARGET_BRANCH = "pre-label-v1"
BUNDLE_SIZE = 100
MAX_LABEL_ROWS = 10  # Quick-test cap. Remove this slice to compare every data unit.

user_client = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path=SSH_PATH,
    domain="https://api.encord.com",
)

project = user_client.get_project(PROJECT_ID)

# Cap each branch at MAX_LABEL_ROWS for the test run.
source_rows = {
    r.data_hash: r
    for r in project.list_label_rows_v2(branch_name=SOURCE_BRANCH)[:MAX_LABEL_ROWS]
}
# Only compare data units that exist on both branches.
target_rows = {
    r.data_hash: r
    for r in project.list_label_rows_v2(
        branch_name=TARGET_BRANCH,
        data_hashes=list(source_rows.keys()),
    )
}

# Initialise the source branch in a single bundled request
with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
    for row in source_rows.values():
        row.initialise_labels(bundle=bundle)

# Initialise the target branch in a single bundled request
with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
    for row in target_rows.values():
        row.initialise_labels(bundle=bundle)

for data_hash, source_row in source_rows.items():
    target_row = target_rows.get(data_hash)
    if target_row is None:
        print(f"{source_row.data_title}: missing on '{TARGET_BRANCH}'")
        continue

    src_count = len(source_row.get_object_instances())
    tgt_count = len(target_row.get_object_instances())
    print(f"{source_row.data_title}: {SOURCE_BRANCH}={src_count}, {TARGET_BRANCH}={tgt_count}")

Sync or Merge Branches

There is no dedicated “merge” call. To push updates from one branch back into another, call copy_labels_to_branch() again with overwrite=True. This replaces label rows on the target branch with the latest content from the source branch.
overwrite=True is destructive. Label rows on the target branch are replaced with the source content for every matched data unit. There is no automatic conflict resolution — the source wins. Filter with data_hashes or label_hashes if you want to limit the sync.
# Import dependencies
from encord import EncordUserClient

SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
SOURCE_BRANCH = "pre-label-v1"  # Branch with the updated labels
TARGET_BRANCH = "main"  # Branch to update
BATCH_SIZE = 50
MAX_LABEL_ROWS = 10  # Quick-test cap. Remove this slice to sync every data unit on the source branch.

user_client = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path=SSH_PATH,
    domain="https://api.encord.com",
)

project = user_client.get_project(PROJECT_ID)

# Pick the first MAX_LABEL_ROWS data units from the source branch to sync back.
source_rows = project.list_label_rows_v2(branch_name=SOURCE_BRANCH)
data_hashes = [row.data_hash for row in source_rows[:MAX_LABEL_ROWS]]

rows_synced = project.copy_labels_to_branch(
    target_branch=TARGET_BRANCH,
    source_branch=SOURCE_BRANCH,
    overwrite=True,
    data_hashes=data_hashes,
    batch_size=BATCH_SIZE,
)

print(f"Synced {rows_synced} label rows from '{SOURCE_BRANCH}' to '{TARGET_BRANCH}' (test run).")
After copying, verify the result by listing label rows on the target branch:
Verify the copy
verification_rows = project.list_label_rows_v2(branch_name="pre-label-v1")
print(f"'pre-label-v1' now contains {len(verification_rows)} label rows.")