BrowserStack AI Evals
Evaluation

Experiments

Create and run experiments across TypeScript, Python, and Java SDKs.

Experiments

Experiments let you systematically evaluate a prompt + dataset + evaluator combination. Each run executes every dataset item through the prompt and scores the output.

Setup

import { AISDK } from '@browserstack/ai-sdk';

const testOps = new AISDK({
  publicKey: process.env.AISDK_PUBLIC_KEY,
  secretKey: process.env.AISDK_SECRET_KEY,
});

const experiments = testOps.experiments;       // Experiments
const experimentRuns = testOps.experimentRuns; // ExperimentRuns
import os
from browserstack_ai_sdk import AISDK

client = AISDK(
    public_key=os.environ["AISDK_PUBLIC_KEY"],
    secret_key=os.environ["AISDK_SECRET_KEY"],
)
import com.browserstack.aisdk.TestOps;
import com.browserstack.aisdk.eval.ExperimentsClient;
import com.browserstack.aisdk.eval.ExperimentRunsClient;

TestOps sdk = TestOps.fromEnv();
ExperimentsClient experiments = sdk.experiments();
ExperimentRunsClient experimentRuns = sdk.experimentRuns();

Create an Experiment

An experiment requires a name, an evaluator list, and either a datasetRunTagId or both promptId + datasetId.

With a prompt and dataset

import { CreateExperimentWithPromptRequest } from '@browserstack/ai-sdk';

const experiment = await experiments.create({
  name: 'qa-accuracy-test',
  description: 'Evaluate QA accuracy on golden set',
  promptId: 'prompt-id-abc',
  datasetId: 'dataset-id-xyz',
  evaluatorListId: 'eval-list-id-123',
  concurrency: 5,
} satisfies CreateExperimentWithPromptRequest);

console.log(experiment.id);
experiment = client.experiments.create({
    "name": "rag-eval-v2",
    "evaluatorListId": "eval-list-id",
    "datasetId": "dataset-id",
    "promptId": "prompt-id",
})
import com.browserstack.aisdk.eval.model.CreateExperimentRequest;
import com.browserstack.aisdk.eval.model.ExperimentResponse;

ExperimentResponse experiment = experiments.create(
    CreateExperimentRequest.builder()
        .name("gpt-4o-faithfulness-v2")
        .evaluatorListId("evl_abc123")
        .promptId("prm_xyz789")
        .datasetId("ds_def456")
        .concurrency(5)
        .build()
);

System.out.println("Experiment ID: " + experiment.getId());

You must provide either datasetRunTagId alone, or both promptId and datasetId together. Mixing them throws IllegalArgumentException.

With a dataset run tag

import { CreateExperimentWithTagRequest } from '@browserstack/ai-sdk';

const experiment = await experiments.create({
  name: 'tag-based-experiment',
  datasetRunTagId: 'tag-id-abc',
  evaluatorListId: 'eval-list-id-123',
  concurrency: 3,
} satisfies CreateExperimentWithTagRequest);
experiment = client.experiments.create({
    "name": "rag-eval-v1",
    "evaluatorListId": "eval-list-id",
    "datasetRunTagId": "tag-id-from-dataset-run",
})
print(experiment)

List and Get Experiments

const result = await experiments.list(
  20, // limit (1-100)
  1   // page
);

for (const exp of result.experiments) {
  console.log(exp.id, exp.name, exp.createdAt);
}

const experiment = await experiments.find('experiment-id-abc');
console.log(experiment.name, experiment.status);
experiments = client.experiments.list(page=1, limit=50)

experiment = client.experiments.get("experiment-id")
print(experiment)
ListExperimentsResponse list = experiments.list(20, 1);
list.getData().forEach(e -> System.out.println(e.getId() + " " + e.getName()));

ExperimentResponse experiment = experiments.find("exp_abc123");
System.out.println("Status: " + experiment.getStatus());

Create and Monitor Runs

Create a Run

const run = await experimentRuns.create(
  'experiment-id-abc', // experimentId
  'output',            // llmColumnName
  5,                   // concurrency (1-100)
  { temperature: 0.0 } // optional runConfig
);

console.log(run.id, run.status); // 'PENDING'
run = client.experiment_runs.create({
    "experimentId": "experiment-id",
    "name": "run-2026-04-01",
})
print(run)
import com.browserstack.aisdk.eval.model.CreateExperimentRunRequest;
import com.browserstack.aisdk.eval.model.ExperimentRunResponse;

ExperimentRunResponse run = experimentRuns.create(
    CreateExperimentRunRequest.builder()
        .experimentId(experiment.getId())
        .build()
);

System.out.println("Run ID: " + run.getId());

Poll for Completion

const result = await experimentRuns.subscribe(
  run.id,
  120_000, // timeout in ms
  5_000    // poll interval in ms
);

if (result.finalStatus === 'COMPLETED') {
  console.log('Run completed:', result.experimentRunData);
} else {
  console.error('Run failed or timed out:', result.finalStatus);
}
// Default: polls every 5s, times out after 5 minutes
ExperimentRunResponse finalRun = experimentRuns.subscribe(
    run.getId(),
    update -> System.out.println("Status: " + update.getStatus())
);

System.out.println("Final status: " + finalRun.getStatus());

With a custom timeout:

long tenMinutes = 10 * 60 * 1_000L;

ExperimentRunResponse finalRun = experimentRuns.subscribe(
    run.getId(),
    update -> System.out.println("[" + update.getStatus() + "] " + update.getProgress()),
    tenMinutes
);

Summarize a Run

summarize() returns scores, metrics, diffs, and UI deep-links for a completed run in a single call. Use it in CI to gate on threshold status, post PR comments, or feed dashboards.

Parameters

ParameterDefaultDescription
compare_to_run_id / compareToRunId / compareToRunIDUI-pinned baseline (or none)Explicit baseline run ID. When omitted (or "" in Go, None in Python, not passed in TypeScript/Java), the server uses the experiment's UI-pinned baseline (set via "Set as baseline" in the experiment view). If neither is set, no comparison is performed and every diff is null. The server does NOT auto-pick a baseline by recency.

Basic call

const summary = await experimentRuns.summarize("run-uuid-1");

console.log(summary.summary.verdict);       // "PASS" | "REGRESSION"
console.log(summary.urls.scoresTab);        // deep-link to Scores tab in the UI
const exactMatch = summary.scores.find(s => s.name === "exact_match");
console.log(exactMatch?.thresholdStatus); // "pass" | "fail"

Pass an options object to pin the baseline:

// With an explicit baseline
const summary = await experimentRuns.summarize("run-uuid-1", {
  compareToRunId: "run-uuid-0",
});
summary = client.experiment_runs.summarize("run-uuid-1")

print(summary.summary.verdict)                              # "PASS" or "REGRESSION"
print(summary.urls.scores_tab)                              # deep-link to Scores tab
exact_match = next((s for s in summary.scores if s.name == "exact_match"), None)
print(exact_match.threshold_status if exact_match else None)  # "pass" or "fail"

With an explicit baseline:

summary = client.experiment_runs.summarize(
    "run-uuid-1",
    compare_to_run_id="run-uuid-0",
)
ExperimentRunSummary summary = experimentRuns.summarize(runId);

System.out.println(summary.getSummary().getVerdict());                              // "PASS" or "REGRESSION"
System.out.println(summary.getUrls().getScoresTab());                               // deep-link to Scores tab
ExperimentRunScoreSummary exactMatch = summary.getScores().stream()
    .filter(s -> "exact_match".equals(s.getName()))
    .findFirst()
    .orElse(null);
System.out.println(exactMatch != null ? exactMatch.getThresholdStatus() : null);    // "pass" or "fail"

Two overloads are available, matching the list() / get() pattern used elsewhere in the Java SDK:

// Uses UI-pinned baseline if any, else no comparison
ExperimentRunSummary summary = experimentRuns.summarize(runId);

// Explicit baseline
ExperimentRunSummary summary = experimentRuns.summarize(runId, "run-uuid-0");
summary, err := client.ExperimentRuns.Summarize(ctx, "run-uuid-1", "")
if err != nil {
    log.Fatal(err)
}

fmt.Println(summary.Summary.Verdict)                              // "PASS" or "REGRESSION"
fmt.Println(summary.Urls.ScoresTab)                               // deep-link to Scores tab
var exactMatch *crud.ExperimentRunScoreSummary
for i := range summary.Scores {
    if summary.Scores[i].Name == "exact_match" {
        exactMatch = &summary.Scores[i]
        break
    }
}
if exactMatch != nil {
    fmt.Println(exactMatch.ThresholdStatus)                       // "pass" or "fail"
}

Pass an empty string for compareToRunID to use auto-resolution.

// Uses UI-pinned baseline if any, else no comparison
summary, err := client.ExperimentRuns.Summarize(ctx, "run-uuid-1", "")

// Explicit baseline
summary, err := client.ExperimentRuns.Summarize(ctx, "run-uuid-1", "run-uuid-0")

CI usage — post a PR comment

A common pattern is running summarize() at the end of a CI job and posting the result as a pull-request comment.

// GitHub Actions step — gate on regression and post a PR comment.
// The SDK returns structured `scores` and `metrics`; render the comment
// however you like (table, one-line summary, Slack blocks, etc.).
const summary = await experimentRuns.summarize(run.id);

if (summary.summary.verdict === "REGRESSION") {
  console.error(`Run regressed: ${summary.summary.regressedCount} scorer(s) degraded`);
  process.exit(1);
}

const scoreLines = summary.scores.map(
  (s) => `- **${s.name}** — ${s.score?.toFixed(3) ?? "—"} (${s.thresholdStatus ?? "—"})`,
);
const body = [
  `**Eval summary — ${summary.summary.verdict}**`,
  ...scoreLines,
  `\n[View full scores](${summary.urls.scoresTab})`,
].join("\n");

await octokit.issues.createComment({ body, /* owner, repo, issue_number */ });
# GitHub Actions step — gate on regression and post a PR comment.
# The SDK returns structured `scores` and `metrics`; render the comment
# however you like (table, one-line summary, Slack blocks, etc.).
summary = client.experiment_runs.summarize(run["id"])

if summary.summary.verdict == "REGRESSION":
    print(f"Run regressed: {summary.summary.regressed_count} scorer(s) degraded")
    sys.exit(1)

score_lines = [
    f"- **{s.name}** — {s.score:.3f} ({s.threshold_status or '—'})"
    for s in summary.scores
    if s.score is not None
]
body = "\n".join([
    f"**Eval summary — {summary.summary.verdict}**",
    *score_lines,
    f"\n[View full scores]({summary.urls.scores_tab})",
])

github.create_pull_request_comment(body=body)
// GitHub Actions step — gate on regression and post a PR comment.
// The SDK returns structured `scores` and `metrics`; render the comment
// however you like (table, one-line summary, Slack blocks, etc.).
ExperimentRunSummary summary = experimentRuns.summarize(run.getId());

if ("REGRESSION".equals(summary.getSummary().getVerdict())) {
    System.err.println("Run regressed: "
        + summary.getSummary().getRegressedCount() + " scorer(s) degraded");
    System.exit(1);
}

StringBuilder body = new StringBuilder("**Eval summary — ")
    .append(summary.getSummary().getVerdict()).append("**\n");
for (ExperimentRunScoreSummary s : summary.getScores()) {
    body.append("- **").append(s.getName()).append("** — ")
        .append(s.getScore() != null ? String.format("%.3f", s.getScore()) : "—")
        .append(" (").append(s.getThresholdStatus() != null ? s.getThresholdStatus() : "—").append(")\n");
}
body.append("\n[View full scores](").append(summary.getUrls().getScoresTab()).append(")");

githubClient.createPullRequestComment(body.toString());
// GitHub Actions step — gate on regression and post a PR comment.
// The SDK returns structured `scores` and `metrics`; render the comment
// however you like (table, one-line summary, Slack blocks, etc.).
summary, err := client.ExperimentRuns.Summarize(ctx, run.ID, "")
if err != nil {
    log.Fatal(err)
}

if summary.Summary.Verdict == "REGRESSION" {
    fmt.Fprintf(os.Stderr, "Run regressed: %d scorer(s) degraded\n",
        summary.Summary.RegressedCount)
    os.Exit(1)
}

var lines []string
lines = append(lines, fmt.Sprintf("**Eval summary — %s**", summary.Summary.Verdict))
for _, s := range summary.Scores {
    score := "—"
    if s.Score != nil {
        score = fmt.Sprintf("%.3f", *s.Score)
    }
    status := "—"
    if s.ThresholdStatus != nil {
        status = *s.ThresholdStatus
    }
    lines = append(lines, fmt.Sprintf("- **%s** — %s (%s)", s.Name, score, status))
}
lines = append(lines, fmt.Sprintf("\n[View full scores](%s)", summary.Urls.ScoresTab))
body := strings.Join(lines, "\n")
githubClient.CreatePullRequestComment(ctx, body)

Response shape

The returned object has these top-level fields:

FieldTypeDescription
projectIdstringProject ID
projectNamestringProject name
experimentIdstringExperiment ID
experimentNamestringExperiment name
experimentRunIdstringThis run's ID
experimentRunNamestringThis run's name
comparisonExperimentRunIdstring | nullBaseline run ID (null when no baseline)
comparisonExperimentRunNamestring | nullBaseline run name (null when no baseline)
urls.projectstringUI link to the project
urls.experimentstringUI link to the experiment
urls.runstringUI link to the run page
urls.scoresTabstringDirect link to the Scores tab
urls.analyticsTabstringDirect link to the Analytics tab
scoresarrayOrdered list of evaluator score summaries — worst-regressed first; look up by name (see below)
metricsobjectMap of metric name → metric summary (see below)
summary.verdict"PASS" | "REGRESSION"Overall run verdict
summary.failureCountnumberScorers below threshold
summary.improvedCountnumberScorers that improved vs baseline
summary.regressedCountnumberScorers that regressed vs baseline

Each score summary includes thresholdStatus ("pass" or "fail"), diff vs baseline, improvements, and regressions per evaluator. Numeric scores additionally carry passCount, failCount, and totalItems; categorical scores carry categoryCounts and passCategories.

Each metric summary includes metric (the aggregate value), unit (s, ms, tok, $), and diff vs baseline.


Run Status Values

enum ExperimentRunStatus {
  PENDING    = 'PENDING',
  RUNNING    = 'RUNNING',
  COMPLETED  = 'COMPLETED',
  FAILED     = 'FAILED',
  CANCELLED  = 'CANCELLED',
}

Complete End-to-End Example

import { AISDK, ExperimentRunStatus } from '@browserstack/ai-sdk';

const testOps = new AISDK({
  publicKey: process.env.AISDK_PUBLIC_KEY,
  secretKey: process.env.AISDK_SECRET_KEY,
});

async function runExperiment() {
  const experiments = testOps.experiments;
  const experimentRuns = testOps.experimentRuns;

  // 1. Create the experiment
  const experiment = await experiments.create({
    name: `accuracy-test-${Date.now()}`,
    description: 'Automated accuracy evaluation',
    promptId: process.env.PROMPT_ID!,
    datasetId: process.env.DATASET_ID!,
    evaluatorListId: process.env.EVALUATOR_LIST_ID!,
    concurrency: 5,
  });

  console.log('Created experiment:', experiment.id);

  // 2. Start a run
  const run = await experimentRuns.create(experiment.id, 'output', 5);
  console.log('Started run:', run.id, 'status:', run.status);

  // 3. Wait for completion
  const result = await experimentRuns.subscribe(run.id, 300_000, 10_000);

  if (result.finalStatus === 'COMPLETED') {
    console.log('Experiment completed successfully!');
    console.log('Run details:', result.experimentRunData);
  } else {
    console.error('Experiment did not complete:', result.finalStatus);
    process.exit(1);
  }

  await testOps.shutdown();
}

runExperiment();
import os
from browserstack_ai_sdk import AISDK

client = AISDK(
    public_key=os.environ["AISDK_PUBLIC_KEY"],
    secret_key=os.environ["AISDK_SECRET_KEY"],
)

# 1. Create experiment
experiment = client.experiments.create({
    "name": "support-qa-eval",
    "evaluatorListId": "<evaluator-list-id>",
    "datasetId": "<dataset-id>",
    "promptId": "<prompt-id>",
})

# 2. Create a run
run = client.experiment_runs.create({
    "experimentId": experiment["id"],
    "name": "initial-run",
})

print(f"Experiment run created: {run['id']}")
import com.browserstack.aisdk.TestOps;
import com.browserstack.aisdk.eval.model.*;
import java.util.List;

public class ExperimentExample {
    public static void main(String[] args) throws InterruptedException {
        TestOps sdk = TestOps.fromEnv();

        // 1. Create an experiment
        ExperimentResponse experiment = sdk.experiments().create(
            CreateExperimentRequest.builder()
                .name("aurora-qa-v1")
                .evaluatorListId("evl_abc123")
                .promptId("prm_xyz789")
                .datasetId("aurora-qa")
                .build()
        );

        // 2. Start a run
        ExperimentRunResponse run = sdk.experimentRuns().create(
            CreateExperimentRunRequest.builder()
                .experimentId(experiment.getId())
                .build()
        );

        // 3. Wait for completion
        ExperimentRunResponse result = sdk.experimentRuns().subscribe(
            run.getId(),
            r -> System.out.printf("[%s] %s%n", r.getStatus(), r.getId())
        );

        System.out.println("Done: " + result.getStatus());
        sdk.shutdown();
    }
}