Requires TypeScript SDK v3.37.0 or later. Import from the top-level
braintrust package. The API is marked experimental in the SDK.How workflow evaluations work
A workflow evaluation has three stages:- Define an evaluation: Call
defineWorkflowEval()with your data, task, scorers, and a store. It works likeEval(), except that the task or scorers can send their work to a provider’s asynchronous API. It returns an object with methods that you use to start and resume runs. - Start the run: Call
start()to submit the requests. It returns arunIdright away, without waiting for the provider. - Resume until completion: Pass the
runIdtopoll()on a schedule, or toprocessSubmissionResult()from a webhook handler. Each call collects the results that are ready and scores them. The run is done when its status is"completed".
Example: OpenAI Batch API
This example sends questions about capital cities through the OpenAI Batch API and scores each answer against the expected city. Here’s what the script does:- Submits the cases to OpenAI together, in one batch.
- Polls OpenAI until the batch finishes, then collects each answer.
- Scores each answer with
exactMatch(), a plain scorer function, and logs the results to Braintrust.
braintrust and openai packages, and set your BRAINTRUST_API_KEY and OPENAI_API_KEY environment variables:
evaluation.ts. They live in their own module so the Redis and webhook examples later on this page can import them:
evaluation.ts
workflow-eval.ts:
workflow-eval.ts
npx tsx workflow-eval.ts. OpenAI batches can take minutes or hours, so the script can run for a while. After each poll(), it prints the run’s status and how many batches are still pending. When it finishes, the Braintrust experiment contains two rows, each with an exactMatch score.
The rest of this section walks through the example’s three stages.
Define an evaluation
First,evaluation.ts defines the evaluation in createEvaluation(). In outline, the call looks like this:
defineWorkflowEval().
Project and experiment
Project and experiment
The first argument is the project name,
"capital-cities-eval" in the example. Braintrust creates the project if it doesn’t exist. Each run logs to a new experiment unless you set experimentName.Data
Data
The
data argument accepts the same cases as Eval(). Each case needs a unique id, so the SDK can match results to cases when the run resumes. Cases can also set metadata, tags, and trialCount.Case data must be JSON-serializable, so the store can save it.Task
Task
The
task argument accepts a plain task function, a WorkflowBatchTask, or a WorkflowTask. Use a WorkflowBatchTask when the provider accepts many requests at once and returns their results later, as batch APIs do. The example’s task, with polling completion, looks like this:batching:maxSizesets the most items in one batch.start()submits every case and trial right away, in batches of up tomaxSizeitems.submit: Sends one batch to the provider. Receives an array of items, one for each case and trial, with the case’s fields and acustomIdthat identifies the item within its batch, such as"item-0". Use thecustomIdas the provider’s request ID, such as OpenAI’scustom_id. Returns data, such as the provider’s batch ID. The SDK saves it and passes it to thecompletionandcollectcallbacks when it calls them.completion: How the SDK learns the batch is done: by polling with a function such as the example’spollBatch(), or from a webhook. See completion options.collect: Fetches the finished batch’s results and returns one{ customId, result }or{ customId, error }entry per item, as an array, an iterable, or an async iterable.resultholds the task’soutput. The SDK records an item that has an error or no entry as failed: its row shows the error, and its scorers don’t run.
submit and collect return, so both must be JSON-serializable. In the example, those are { batchId: ... } and the model’s answers.The example passes type arguments to defineWorkflowEval<string, string, string>() for the input, output, and expected output. TypeScript uses them to check the data and to infer the parameter types of callbacks written inline, so input is a string and submission is a Submission without annotations. For the fields of the submitted items and the collected entries, see WorkflowBatchTask.To send each case as its own request instead, use a WorkflowTask. See Submit one request per case.Scores
Scores
The It takes the same callbacks as a Then change
scores argument accepts plain scorer functions, like the example’s exactMatch(), WorkflowBatchScorer instances, and WorkflowScorer instances. Use a WorkflowBatchScorer when scoring needs requests that finish later, such as LLM-as-a-judge calls through a batch API. It has this shape:WorkflowBatchTask, plus a name. Its items include the task’s output along with the case’s fields, and each collect entry’s result has a score. A failed item omits that score and records the error in the row’s scorer_errors metadata. See WorkflowBatchScorer.A WorkflowBatchScorer submits a batch once maxSize items have task outputs, or once no more items can get one. To submit partial batches sooner, set batching.maxWaitMs. The SDK submits a partial batch that has waited that long on the next start(), poll(), or processSubmissionResult() call.Example: Score with a batch requestThe following llmJudge scorer asks a model to grade each answer through the OpenAI Batch API. It reuses the example’s submitBatch(), collectText(), and pollBatch(). To try it, add WorkflowBatchScorer to the braintrust import in evaluation.ts, then add this code right before createEvaluation():scores: [exactMatch] to scores: [exactMatch, llmJudge] and run workflow-eval.ts again. Your exactMatch() scorer runs as soon as the answers arrive. The same poll() submits one grading batch for both answers, and a later poll() collects the grades.Store
Store
The
store argument saves the run’s progress between calls:new WorkflowEvalMemoryStore(): Keeps progress in memory. Use it when the whole run happens in one process, as in the example.new WorkflowEvalRedisStore({ client }): Keeps progress in Redis, using a connectedredis,ioredis, or@upstash/redisclient. Use it when the run must survive process exits or be resumed by another process. Records expire after seven days by default.
WorkflowEvalStore. Every process that works on a run must use the same store and the same evaluation definition.Start the run
Next,main() in workflow-eval.ts starts a run by calling start(), which submits the first batches and returns a result with the runId, without waiting for the provider.
Save the runId. You pass it to poll(), processSubmissionResult(), and status().
Resume until completion
Finally, the example callspoll() in a loop until every case is scored. A waiting run can resume in either of two ways, or both:
- Polling: Call
evaluation.poll({ runId })on a schedule. - Webhooks: Call
evaluation.processSubmissionResult()when the provider sends a completion event.
poll({ runId }) to collect the scorer results.
Check progress. Each call returns a result whose status stays "waiting" until the run completes, and whose pending field has poll and webhook counts of the batches not yet collected. To check without advancing the run, call evaluation.status({ runId }).
Get the results. When the status is "completed", result.summary has the scores and a link to the experiment.
Resume with polling
Resume with polling
The main example runs in one process that waits until the run finishes, and it keeps progress in memory.For runs that take hours, an alternative is a separate script, such as the following To use it:
resume-eval.ts, that runs in place of workflow-eval.ts. Each time it runs, it does one step and exits, and it saves progress in Redis so the next run continues where the last one stopped. It imports createEvaluation() from evaluation.ts so it doesn’t repeat the evaluation. It requires the redis package (npm install redis) and a REDIS_URL environment variable, such as redis://localhost:6379:resume-eval.ts
- Running
npx tsx resume-eval.tswith no arguments submits the requests and prints the run ID. - Running
npx tsx resume-eval.ts <runId>advances the run once. A scheduled job can run it periodically until it printscompleted.
Resume from provider webhooks
Resume from provider webhooks
Use webhook completion when your provider sends an event when a request finishes. For OpenAI, configure a webhook endpoint for Set
batch.completed, batch.failed, batch.expired, and batch.cancelled events, and use a shared Redis store. The following webhook-handler.ts reuses createEvaluation() and client from evaluation.ts:webhook-handler.ts
OPENAI_WEBHOOK_SECRET to the endpoint’s signing secret from OpenAI. Pass the incoming Request from your HTTP endpoint, such as a Next.js route handler, to handleWebhook(). It reads the unparsed body, which signature verification requires. Return an error status if it throws, so OpenAI redelivers the event. Start the run with startRun().Keep in mind:- Failed batches: The example passes
batch.failed,batch.expired, andbatch.cancelledevents toprocessSubmissionResult()along withbatch.completed. The SDK records each request without a result as failed, so the run still completes. - Run ID: The example passes only the batch ID. That works when each batch ID belongs to one run of the evaluation, as OpenAI batch IDs do. If several runs can share an external ID, also pass
runId. - Security:
processSubmissionResult()doesn’t check that the event came from the provider. In the example,unwrap()does. - Repeated events: The same event can arrive more than once. Make your
collectcallbacks safe to run more than once.
processSubmissionResult().Handle failures and concurrency
Handle failures and concurrency
- Concurrency:
maxConcurrencylimits how many of yoursubmit,getExternalId, polling, andcollectcallbacks the SDK runs at once during each call. It defaults to10. For a batch task or scorer, eachsubmitcall sends one batch. SeedefineWorkflowEval(). - Provider failures: When your polling callback returns
{ status: "failed", error },evaluation.poll()throws the error. The SDK checks the batch again on the nextpoll(), so a permanent failure makes every laterpoll()throw, and the run never completes. The example’spollBatch()avoids this by returning{ status: "complete" }for failed, expired, and cancelled batches, socollectrecords the requests without results as failed. The SDK doesn’t resubmit or cancel requests that the provider accepted, so retries and cancellation are up to your integration. - Errors in your functions: For polling, the SDK retries your polling callback and your
collectfunction on the nextpoll(). For webhooks,collectruns again only whenprocessSubmissionResult()is called again, such as when the provider redelivers the event. If yoursubmitfunction throws, the nextpoll()calls it again with the same items andsubmissionId, for both polling and webhook completion. If your provider supports idempotency keys, use thesubmissionIdas the key so a retry doesn’t submit twice. Asubmitthat throws duringstart()makesstart()throw before it returns therunId, so savecontext.runIdinsubmitif you need to resume. The SDK runs your plain task, scorer, and classifier functions only once per case, so if one of them fails, that case never finishes. Fix the problem and start a new run.
Submit one request per case
AWorkflowTask or WorkflowScorer submits one request for each case and trial instead of batching them. Use one when your provider’s asynchronous API takes one request per job, or when each case’s scorers should start as soon as that case’s output is ready.
The following task sends each case to OpenAI as its own single-request batch. To try it, add WorkflowTask to the braintrust import in evaluation.ts, and replace the WorkflowBatchTask in createEvaluation() with this task:
WorkflowBatchTask in these ways:
- Items:
submitreceives one item, without acustomId. Thecontext.submissionIdis unique to each case and trial, so the example uses it as OpenAI’scustom_id. - Results:
collectreturns one result, such as{ output }for aWorkflowTaskor{ score }for aWorkflowScorer, instead of entries keyed bycustomId. - Failures: If
collectthrows, the SDK calls it again on the nextpoll(), so a request that can’t succeed, such as one in an expired batch, keeps the run waiting. Start a new run to recover. - Scorers: A
WorkflowScorersubmits each case’s request as soon as that case’s task output is ready.
WorkflowTask and WorkflowScorer.
Next steps
- Review
defineWorkflowEval()and the workflow evaluation methods. - Learn how to run experiments in code.
- Add custom scorers to measure task quality.