Applies to:
- Plan - Any
- Deployment - Any
- A guide to distributing unassigned log records to reviewers using the Braintrust BTQL endpoint
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:- Fetch: Retrieves all matching logs from a Braintrust project
- Filter: Identifies which logs are unassigned
- 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
requestslibrary installed - Environment variables:
BRAINTRUST_API_KEY- Your Braintrust API key (required)BRAINTRUST_API_URL- Only needed for self-hosted deployments (optional)
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
DAYSdays) - Returns the log
id,tags, and current assignments metadata - Applies pagination with a cursor-based strategy
| 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
Thefetch_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
Thedistribute() 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_assignmentsfield to the reviewer’s user ID - Sets the
~__bt_review_listsfield with a default review list in “PENDING” status - Uses
_is_merge: trueto instruct the API to merge rather than replace metadata
- Sets the
- Batches assignments in groups of
BATCH_SIZE - POSTs each batch to the
/v1/project_logs/{PROJECT_ID}/insertendpoint
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
Dry Run
Output
The script prints progress to stdout: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_KEYis 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,000prevents 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 theTAG and DAYS variables:
Add or remove reviewers
Update theUSER_IDS list with the UUIDs of your reviewers:
Adjust batch sizes for performance
IncreaseLIMIT (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 theBRAINTRUST_API_URL environment variable:
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):cursor field. Use this cursor to fetch the next page:
Subsequent request (with cursor):
<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:<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