Skip to main content
Applies to:
  • Plan -
  • Deployment -

Overview

This script fetches log records tagged as “My dummy tag” from Braintrust using the BTQL (Braintrust Query Language) endpoint, identifies which rows are not yet assigned to a reviewer, and distributes those rows evenly across a list of specified reviewers. The script performs three main operations:
  1. Fetch: Retrieves all matching logs from a Braintrust project
  2. Filter: Identifies which logs are unassigned
  3. Distribute & Assign: Evenly distributes unassigned rows to reviewers and commits the assignments

Requirements

Before running this script, ensure you have:
  • Python 3.7+ with the requests library installed
  • Environment variables:
    • BRAINTRUST_API_KEY - Your Braintrust API key (required)
    • BRAINTRUST_API_URL - Only needed for self-hosted deployments (optional)
Install dependencies:

Configuration

The script includes several configuration constants at the top that you can customize:

How It Works

1. BTQL Query & Pagination

The script builds a BTQL query that:
  • Selects logs from the specified project
  • Filters by tag and creation date (within the last DAYS days)
  • Returns the log id, tags, and current assignments metadata
  • Applies pagination with a cursor-based strategy
Key detail: The cursor is embedded within the BTQL query itself (| cursor: '...'), not as an HTTP header. The API returns the next cursor in the response body’s cursor field or the x-bt-cursor header.

2. Fetching All Logs

The fetch_all_logs() function:
  • Iterates through pages of results (capped at MAX_PAGES = 10,000)
  • Stops when the server returns no new cursor or a page with fewer rows than the LIMIT
  • Returns a complete list of all matching log records

3. Identifying Unassigned Rows

The script uses the ~__bt_assignments metadata field to check which rows already have a reviewer assigned:
  • Calls get_assignments(row) to extract the assignments list
  • Calls is_assigned(row) to test whether a row has at least one reviewer
  • Filters out already-assigned rows

4. Even Distribution

The distribute() function divides unassigned row IDs as evenly as possible across reviewers:
  • With N users and M rows: each user gets either ⌊M/N⌋ or ⌊M/N⌋ + 1 rows
  • The first (M % N) users receive one extra row to ensure all rows are assigned
  • Returns a list of (row_id, user_id) pairs

5. Assigning to Reviewers

For each unassigned row:
  • Creates a merge-update event using build_event() that:
    • Sets the ~__bt_assignments field to the reviewer’s user ID
    • Sets the ~__bt_review_lists field with a default review list in “PENDING” status
    • Uses _is_merge: true to instruct the API to merge rather than replace metadata
  • Batches assignments in groups of BATCH_SIZE
  • POSTs each batch to the /v1/project_logs/{PROJECT_ID}/insert endpoint
Setting ~__bt_assignments alone is sufficient for assigned rows to be marked complete from the Review page. Setting the ~__bt_review_lists status to PENDING remains recommended so the rows also appear in status-based review filters such as Awaiting review.

Usage

Basic Run

This fetches logs, shows the assignment plan, and commits assignments.

Dry Run

Shows the assignment plan without updating any rows. Useful for validation before committing.

Output

The script prints progress to stdout:
Additionally, a file named to_assign_ids.json is created in the script’s directory containing the list of row IDs that were assigned (useful for reference or further automation).

Key Functions

build_query(cursor: str | None) -> str

Constructs the BTQL query string, optionally including a pagination cursor.

fetch_all_logs() -> list[dict]

Fetches all log records matching the query criteria, handling multi-page pagination.

get_assignments(row: dict) -> list

Extracts the assignment list from a log row’s metadata; returns an empty list if no assignments exist.

is_assigned(row: dict) -> bool

Returns True if a row has at least one reviewer assigned.

distribute(ids: list[str], users: list[str]) -> list[tuple[str, str]]

Divides row IDs as evenly as possible across reviewers and returns (id, user) pairs.

build_event(log_id: str, user_id: str) -> dict

Creates a merge-update event that assigns a row to a reviewer.

assign_batch(events: list[dict]) -> None

POSTs a batch of assignment events to the Braintrust API.

Error Handling

The script includes error handling for:
  • Missing API key: Exits with an error message if BRAINTRUST_API_KEY is not set
  • BTQL query failures: Exits if the API returns a non-200 status code
  • Empty reviewer list: Exits if no reviewers are configured in USER_IDS
  • Assignment failures: Exits if an assignment batch fails to POST

Safety Features

  • Pagination cap: MAX_PAGES = 10,000 prevents infinite loops due to cursor issues
  • Timeout: All HTTP requests have a 120-second timeout
  • Dry-run mode: Test assignments without committing changes
  • Existing assignments: Skips rows already assigned to prevent re-assignment
  • JSON export: Saves the list of assigned row IDs for audit purposes

Common Customizations

Change the tag or time window

Edit the TAG and DAYS variables:

Add or remove reviewers

Update the USER_IDS list with the UUIDs of your reviewers:

Adjust batch sizes for performance

Increase LIMIT (page size) for faster fetching or BATCH_SIZE (assignment batch size) for faster assignment posting:

Self-Hosted Deployments

If you’re using a self-hosted Braintrust instance, set the BRAINTRUST_API_URL environment variable:
The script will use this URL instead of the default https://api.braintrust.dev.

Troubleshooting

Manual Execution with curl

If you prefer to run the equivalent operations manually using curl commands, follow these steps:

Step 1: Fetch Unassigned Logs via BTQL

First, query all logs matching your criteria. The cursor-based pagination allows you to retrieve all results in multiple requests. Initial request (first page):
The response includes a cursor field. Use this cursor to fetch the next page: Subsequent request (with cursor):
Repeat the subsequent request, replacing <CURSOR_VALUE> with the cursor from the previous response, until the response no longer includes a cursor. Save the response data: Extract and save all the log IDs from the responses. Filter out rows where metadata."~__bt_assignments" is already populated (these are already assigned).

Step 2: Distribute Row IDs Across Reviewers

Manually distribute the unassigned row IDs across your reviewers. For example, with 1000 unassigned rows and 5 reviewers:
  • Each reviewer gets 200 rows
  • Assign rows 1-200 to reviewer 1, 201-400 to reviewer 2, etc.

Step 3: Assign Rows via Merge-Update

For each row, create a merge-update event and POST it to the insert endpoint. You can batch multiple assignments in one request. Single assignment:
Batch assignment (recommended for performance):
Replace the placeholders:
  • <LOG_ID> - The ID from the BTQL response
  • <REVIEWER_USER_ID> - A user ID from your reviewer list
  • Repeat the event object for each row you’re assigning
Repeat this request for each batch of assignments until all rows are assigned.

Full script