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:- 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:| Variable | Default | Purpose |
|---|---|---|
PROJECT_ID | "your-project-id" | The Braintrust project ID to query |
TAG | "My dummy tag" | The tag filter for selecting logs |
DAYS | 7 | How many days back to fetch logs |
LIMIT | 1000 | Page size for each BTQL request |
USER_IDS | Array of 5 UUIDs | Reviewer user IDs to distribute rows to |
BATCH_SIZE | 1000 | Number of assignments per API request |
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
| Issue | Solution |
|---|---|
BTQL query failed (401) | Check that BRAINTRUST_API_KEY is correct and has not expired |
BTQL query failed (404) | Verify that PROJECT_ID is correct |
No cursor returned, but more rows exist | Increase MAX_PAGES or check API logs |
Nothing to assign | All rows matching the criteria are already assigned to reviewers |
Populate USER_IDS first | Add at least one reviewer UUID to the USER_IDS list |
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