Run Evals In CI
Run an eval on every pull request and fail the check when quality, cost, or latency gets worse.
Catch quality regressions in review, the same way unit tests catch broken code. On every pull request, CI runs your task against a saved dataset, grades the outputs with your scorers, saves the run as an experiment, and fails the check when the result is below the bar you set.
- Task. A function in your repository that takes a dataset row's input and returns your app's output.
- CLI.
traccia eval(Python) ortraccia-ts eval(TypeScript) runs the task over the dataset, scores it, and saves an experiment. - Exit code. 0 means the gate passed. Anything else fails the job. Details land in
summary.json.
When To Use evaluate() Instead
evaluate() in the SDK records a run but never fails a job. Use it in notebooks and scripts. Use the CLI when a result should block a merge.Quickstart
You need a dataset and a scorer saved in the app, and a workspace API key from Settings → API Keys. A builtin scorer such as exact_match is enough to start.
1Write the task
The CLI loads the file and calls task once per dataset row. Return anything JSON-serializable. Your scorers grade that value against the row's expected output.
# evals/support_agent.pyfrom my_app import answer # your application code
def task(input): # input is the dataset row's input, for example {"question": "..."} return answer(input["question"])2Run it locally
This run fails unless every scorer passes at least 90 percent of rows. The link to the saved experiment is the url field in summary.json.
export TRACCIA_API_KEY=...
traccia eval evals/support_agent.py \ --dataset support-golden \ --scorer exact_match \ --min-pass-rate 0.9 \ --output summary.json
echo $? # 0 = pass, 1 = below the bar3Add the workflow
Add TRACCIA_API_KEY as a repository secret, then commit this file. The step's exit code is the check result. To block merges on it, add the job as a required status check in your branch protection rules.
# .github/workflows/evals.ymlname: Evalson: pull_request:
permissions: contents: read
jobs: eval: runs-on: ubuntu-latest timeout-minutes: 30 concurrency: group: evals-${{ github.event.pull_request.number }} cancel-in-progress: true steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - run: pip install -r requirements.txt # includes traccia
- name: Run evals env: TRACCIA_API_KEY: ${{ secrets.TRACCIA_API_KEY }} # Optional: record the commit and pull request on the experiment. TRACCIA_GIT_GITHUB_SHA: ${{ github.sha }} TRACCIA_GIT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} TRACCIA_GIT_HEAD_REF: ${{ github.head_ref }} TRACCIA_GIT_BASE_REF: ${{ github.base_ref }} TRACCIA_PR_NUMBER: ${{ github.event.pull_request.number }} TRACCIA_GIT_REPOSITORY: ${{ github.repository }} TRACCIA_GIT_ACTOR: ${{ github.actor }} run: | export TRACCIA_GIT_CHECKOUT_SHA="$(git rev-parse HEAD)" traccia eval evals/support_agent.py \ --dataset support-golden \ --scorer exact_match \ --min-pass-rate 0.9 \ --output summary.json
- uses: actions/upload-artifact@v4 if: always() with: name: eval-summary path: summary.jsonChoose A Gate
Pick what should block a merge. You can combine gates; the check fails if any of them fails.
| Gate | Flags | Fails when | Use it for |
|---|---|---|---|
| Execution only | none | The task or a scorer errors. | Your first week, while you calibrate the bar. The label is Execution Only. |
| Absolute bar | --min-pass-rate 0.9 | Any scorer passes fewer than 90 percent of its rows. | Must-pass suites, such as safety or format checks. |
| No regression | --baseline-prompt NAME --max-drop 0.05 | Any scorer is more than 5 points below the baseline. 0.90 may fall to 0.85. | Quality that varies by design, where you care about getting worse, not a fixed number. |
| Cost and latency | --max-task-cost-increase 0.2 --max-latency-increase 0.3 | Cost or mean latency is more than 20 or 30 percent above the baseline. | Model or prompt changes that could quietly get slower or more expensive. |
The bar applies to each scorer on its own. A Jev Decision scorer counts each question as its own scorer, so one failing question fails the bar even when the other questions pass. To record a scorer without gating on it, pass it to --informational as well as --scorer. Platform LLM-judge and Jev scorers read provider keys from the environment (OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_API_KEY, GROQ_API_KEY, TYPESAFE_API_KEY). TRACCIA_PROVIDER_KEYS can be a JSON object of provider name to key. The CLI forwards whichever of these are set and does not print them.
Compare Against A Baseline
The regression, cost, and latency gates compare the pull request with a baseline experiment. The usual baseline is the experiment you attached when you promoted the prompt that is in production now. Set it up once:
- Run the CLI on your main branch with
--prompt support-reply --prompt-label production(or--prompt-version 7) and no gate flags. This tags the experiment with the prompt version it tested. - In the app, open the prompt, choose Promote To Production, and attach that experiment.
- In the pull request workflow, add
--baseline-prompt support-replyand--max-drop.
traccia eval evals/support_agent.py \ --dataset support-golden \ --scorer exact_match \ --scorer faithfulness \ --prompt support-reply --prompt-label production \ --baseline-prompt support-reply \ --max-drop 0.05 \ --min-pass-rate 0.8 \ --output summary.jsonEvery promotion that attaches an experiment moves the baseline forward. Promoting without one clears it, and the next gated run fails with baseline_missing. To compare with one specific run instead, pass --baseline-experiment <id>. If that experiment ran more than one prompt version side by side, add --baseline-panel <n> to pick one (zero-based).
Baselines Must Be Like For Like
- A changed dataset fails with
dataset_changed. Pass--allow-dataset-changeonce you have reviewed the change. - An edited scorer fails with
scorer_definition_changed. Promote a new baseline that uses the new definition. - Local scorers (below) have no saved definition, so they cannot be used with
--max-drop. Use a scorer saved in the app.
Read The Results
The CLI prints progress and the final label to stderr. --output writes summary.json, which is the stable machine-readable result (schema_version: 1). Use the process exit code to pass or fail the job. Do not parse the label. A regression looks like this (trimmed):
{ "schema_version": 1, "exit_code": 1, "code": "regression", "label": "Failed", "quality_gate": false, "failures": [ { "code": "regression", "message": "exact_match dropped more than allowed" } ], "gates": { "max_drop": 0.05, "baseline_experiment_id": "8c1d…" }, "experiment_id": "0f6e2a54…", "url": "https://app.traccia.ai/…", "dataset_fingerprint": "a41f9c0e…", "dataset_verified": true, "scorers": [ { "name": "exact_match", "expected": 50, "completed": 50, "missing": 0, "errored": 0, "passes": 42, "pass_rate": 0.84, "baseline_pass_rate": 0.9, "informational": false } ]}pass_rate is null when any row is missing or a scorer errored, so a partial run never looks like a low score. Open url to see every row, its trace, and its score next to the baseline.
Exit Codes
Problems with the evidence are reported before quality, so a crash is never mistaken for a regression.
| Exit | Meaning | What to do |
|---|---|---|
| 0 | Passed. Label is Quality Gate when a bar was set, Execution Only when not. | Nothing. |
| 1 | Below the bar: threshold_missed, regression, cost_increase, latency_increase, or dataset_changed. | Open the experiment and compare failing rows with the baseline. Fix the change, or adjust the bar on purpose. |
| 2 | The command or the baseline can't be used, so nothing ran. Examples: bad_flags, unknown_scorer, baseline_missing, baseline_conflict. | Read the failure message. Common causes: a bad API key, a misspelled dataset or scorer, or a prompt that has no promoted experiment yet. |
| 3 | The run happened but its results can't be trusted. Examples: task_error, scorer_error, scorer_coverage, dataset_drift, scorer_definition_changed. | Fix the error in the task or scorer. A scorer error is never counted as a fail, so rerun after the fix. |
| 4 | The run finished but the experiment could not be saved (persist_failed). | Usually transient. Rerun the job. |
| 5 | summary.json could not be written (summary_failed). | Check that the --output path is writable. |
The full list of codes is in the CLI reference.
Post Results On The Pull Request
The Traccia Eval Action is optional. It runs your command in the environment you already installed, records the git metadata for you, refuses a stale or inconsistent summary.json, and keeps one comment per job up to date on the pull request. The step still exits with the CLI's exit code.
❌ **Traccia Eval failed** · `regression` (exit 1)
| Scorer | Pass rate | Baseline | Change | Graded ||---|---|---|---|---|| `exact_match` | 84.0% | 90.0% | −6.0 pts | 50/50 |
**Failures**- `regression` exact_match dropped more than allowed
**Gates:** Max drop 5 pts · Baseline `8c1d77e2-5b0`
[Open experiment](…) · [View run](…) · commit `4b7e19c`permissions: contents: read pull-requests: write # lets the action post the comment
# ...same job as the quickstart, with the "Run evals" step replaced by: - id: eval uses: traccia-ai/traccia-eval-action@v1 with: command: >- traccia eval evals/support_agent.py --dataset support-golden --scorer exact_match --min-pass-rate 0.9 --output summary.json env: TRACCIA_API_KEY: ${{ secrets.TRACCIA_API_KEY }}
- uses: actions/upload-artifact@v4 if: always() with: name: eval-summary path: ${{ steps.eval.outputs.summary_path }}| Input | Default | Description |
|---|---|---|
command | required | The eval command. Install the SDK in an earlier step; the Action never installs it. For TypeScript use npx --no-install traccia-ts eval .... |
summary | summary.json | Must match the command's --output. The Action deletes it before the run, and exits 5 if it is missing afterwards or disagrees with the exit code. |
working_directory | workspace root | Where to run the command, for monorepos. summary is relative to it. |
comment | true | Set to false to skip the comment. The job summary is still written. |
comment_key | workflow, job, and ref | Keeps one comment per key. In a matrix, include the matrix value or the legs overwrite each other. |
github_token | github.token | Used to post the comment. If posting fails, the check still uses the eval result and the job summary says why. |
Outputs, set even when the step fails: exit_code, code, label, quality_gate, experiment_id, experiment_url, summary_path, and comment_posted. More workflows, including a matrix and a monorepo setup, are in the Action README.
The eval command cannot read github_token. The default token updates the same comment on later pushes. A custom GitHub App token posts a new comment on each run.
Other CI Systems
The CLI works anywhere Python or Node runs. Set TRACCIA_API_KEY, run the command, and let its exit code fail the job. To show the commit and merge request on the experiment, set the TRACCIA_GIT_* variables from your CI's own variables. GitLab:
# .gitlab-ci.ymlevals: image: python:3.12 rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" variables: TRACCIA_GIT_CHECKOUT_SHA: $CI_COMMIT_SHA TRACCIA_GIT_HEAD_SHA: $CI_COMMIT_SHA TRACCIA_GIT_HEAD_REF: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME TRACCIA_GIT_BASE_REF: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME TRACCIA_PR_NUMBER: $CI_MERGE_REQUEST_IID TRACCIA_GIT_REPOSITORY: $CI_PROJECT_PATH TRACCIA_GIT_ACTOR: $GITLAB_USER_LOGIN script: - pip install -r requirements.txt # TRACCIA_API_KEY is a masked CI/CD variable in project settings. - traccia eval evals/support_agent.py --dataset support-golden --scorer exact_match --min-pass-rate 0.9 --output summary.json artifacts: when: always paths: [summary.json]| Variable | Recorded as |
|---|---|
TRACCIA_GIT_CHECKOUT_SHA | The commit that actually ran. Shown as Commit in the app. |
TRACCIA_GIT_HEAD_SHA | The branch tip. Shown as Head when it differs, for example when CI tests a merge commit. |
TRACCIA_GIT_HEAD_REF | The branch name. It is also appended to the experiment title. |
TRACCIA_PR_NUMBER | The pull or merge request number. Used in the title when there is no branch name. |
TRACCIA_GIT_GITHUB_SHA, TRACCIA_GIT_BASE_REF, TRACCIA_GIT_REPOSITORY, TRACCIA_GIT_ACTOR | Stored on the experiment for audit. |
Scorers In Your Repository
For a check that only makes sense in code, define the scorer next to the task and select it with --scorer local:name. The name is the key in the exported scorers map.
def task(input): return answer(input["question"])
def mentions_refund(output, expected, **kwargs): # Return True/False, a 0-1 score, or {"passed": ..., "score": ...} return "refund" in str(output).lower()
scorers = {"mentions_refund": mentions_refund}Python scorers are called with keyword arguments input, output, expected, and metadata, so accept **kwargs. TypeScript scorers receive one object with the same fields. Local scorers work with --min-pass-rate but not --max-drop.
All Flags
The flags are the same for traccia eval and traccia-ts eval.
| Flag | Description |
|---|---|
--dataset | Required. Dataset name or id in the API key's workspace. |
--output | Required. Where to write summary.json. |
--scorer | A scorer saved in the app, or local:name. Repeat the flag for more than one. |
--informational | Run and record a selected scorer without gating on its pass rate. If it errors, the check still fails. |
--min-pass-rate | 0 to 1. Every gated scorer must reach it. |
--max-drop | 0 to 1, in points. 0.05 allows 0.90 to fall to 0.85. Needs a baseline. |
--baseline-prompt | Use the experiment attached to this prompt's production version. |
--baseline-experiment | Use this experiment id. Use this or --baseline-prompt, not both. |
--baseline-panel | Which prompt version (zero-based) in a side-by-side baseline experiment. Prompt baselines already know their panel. |
--allow-dataset-change | Compare even though the dataset changed since the baseline. |
--max-task-cost-increase | Allowed relative increase in task cost. 0.2 is 20 percent. |
--max-judge-cost-increase | Allowed relative increase in LLM-judge cost. |
--max-latency-increase | Allowed relative increase in mean task latency. |
--allow-concurrency-mismatch | Compare latency even if the baseline ran at a different concurrency. Runs use a concurrency of 10. |
--prompt with --prompt-version or --prompt-label | Tag the experiment with the prompt version it tested, so it can be attached when you promote. A prompt name alone is rejected. |
--name | Experiment title. By default it is the script name (or the dataset name for task.py, eval.py, main.py, index, or run) plus the branch. |
Troubleshooting
- The job fails on fork pull requests. GitHub does not give secrets to forks, so the API key is empty. Skip the job with
if: github.event.pull_request.head.repo.full_name == github.repository. Do not switch topull_request_target, which would run untrusted code with your secrets. traccia-ts: command not found. Run it throughnpx --no-install traccia-tsor an npm script, afternpm ci.- A TypeScript task file won't load. Use a
.mjsor.jsfile. Node 22.18 and later can also load.tsfiles directly. baseline_missingon every run. The production version of the prompt has no attached experiment. Promote it again and attach an experiment from step 1 of the baseline setup.latency_concurrency_mismatch. The baseline ran at a different concurrency, for example from the Prompt Playground. Use a CLI run as the baseline, or pass--allow-concurrency-mismatch.- The pass rate is lower than in the app. The gate leaves errored rows out of the pass rate and fails the run instead. Check
erroredandmissinginsummary.json.
What A Green Check Guarantees
- Every row in the dataset snapshot ran and was graded exactly once. Missing, duplicate, or errored results fail the check.
- Rows that change while the run is in progress fail the check with
dataset_drift. - Scorers saved in the app are frozen when the run starts, so an edit mid-run does not change the grades.
- Cost and latency gates pass only with a complete measurement on both sides. Unknown cost never counts as zero.
- The experiment was saved, so the link in the summary always opens.
Next Steps
© 2026 Traccia.