Running evaluations¶
Three ways to run a target: the admin UI, the ai-eval:run Drush command, and a
scheduled CI job. All three execute the same run engine and write the same
result row. This page is for whoever operates the evals.
From the UI¶
The Dashboard target list at /admin/config/ai/ai-eval has a per-target Run
button. Open a test to use its workflow page, which also has a Run button and
a run history table with Started, Status, Progress, Errors,
Updated, Score, Gate, Source, and Actions columns. The result
link for a completed attempt is in Actions. History shows the newest 20
attempts and results, including results from Drush and scheduled jobs. Results
without a browser attempt say Started outside this page and have no progress
or cancel controls. A browser attempt and its result appear once.
The run advances one question per HTTP request, so a hundred-question dataset is
a hundred small requests rather than one that times out. Progress and the error
count update on each poll. Execution is browser-driven: closing the page pauses
the run, and opening the Dashboard or the test's workflow page again picks it up
where it left off. A run left paused longer than run_stale_seconds is shown as
Interrupted (see below). A run started here never continues in the
background. For a run nobody watches, such as a nightly check, use
drush ai-eval:run from CI or cron (In CI).
A run started from the admin pages runs its questions and graders as the
account that started it, even when someone else opens its page. The progress
line shows who started it. A question's caller_uid switches only the target
call; graders keep the starter's account. If that account is deleted or
blocked, the run stops with a message asking you to start it again. Older runs
without a recorded starter keep using the account viewing the page.
A Cancel button stops the run cleanly. A
cancelled run is marked cancelled and writes no result row, so its partial
scores remain only on the run attempt and do not become a completed result.
A run does not start when the test's dataset has no questions to run. The dataset may be missing or not valid, or every question may be turned off. The Run button shows the reason and no result is stored. drush ai-eval:run prints the same reason for that target, runs the other targets, and exits with 1. The same applies when --question names an ID that a target does not have, or --split names an empty split: that target is reported as an error and the command exits with 1.
An active run is shown as Interrupted when it has made no progress for the
configured run_stale_seconds interval, 300 seconds by default. Resume
continues that same attempt from its saved progress. Cancel abandons it.
After a cancelled, errored, or interrupted attempt, Start a new run runs
every example again. It can repeat provider calls and incur provider charges.
If a progress request throws, the attempt enters the error state and polling
stops. Retry starts a separate run. The last_error field records the message, with the exception class in front
for unexpected failures, truncated to 1,024 characters. A message the module
words itself, such as the one for a dataset that changed during the run, is
stored without the class. This supplies the error detail shown in the UI.
An admin run keeps the question IDs and order it had at its start, so reordering the dataset or adding questions does not change which questions it runs. If a selected question is missing when its step starts, the run stops with an error that names that question. Older runs, or datasets without unique, non-empty question IDs, keep running by position and stop if the dataset loses questions, with an error that names the question counts at the start and now. Retry starts a new run on the dataset as it is.
Two locks limit concurrent work: a per-target lock at run start, and a per-run lock on each step. A slow question can outlast its step lock and be called again. Only the first step to save that question's progress keeps its score. A question that finishes after cancellation cannot change the saved scores. Finalization claims the active run before scoring and writes one result, even when two requests try to finish it together.
stateDiagram-v2
[*] --> pending: Run pressed
pending --> running: first step
running --> running: one question per poll
running --> complete: last question graded
running --> cancelled: Cancel pressed
running --> error: run failed
complete --> [*]: result row written
cancelled --> [*]: no result row
Running from the UI needs the operate ai eval or administer ai eval
permission.
A result page leads with how many examples met your expectations, then the run summary and the per-example table with the failed examples first:

The run summary also shows the target model, the judge models, the tokens used and how many responses were shortened before the judges read them. Each expanded example lists its model, its tokens and whether its response was shortened. A count the run did not record reads "not recorded", never 0.
From Drush¶
The command is ai-eval:run, alias aer. With no options it runs every enabled
target.
# Every enabled target
drush ai-eval:run
# One target, or one question of one target
drush ai-eval:run --target=my_target
drush ai-eval:run --target=my_target --question=Q01
# List what would run, calling no provider
drush ai-eval:run --dry-run
# Also write a timestamped JSON results file
drush ai-eval:run --json
# Tag the run so cron runs are distinguishable from manual ones
drush ai-eval:run --source=cron
# Run every question as Drupal user 2
drush ai-eval:run --as-user=2
Options¶
| Option | Default | What it does |
|---|---|---|
--target |
all enabled targets | Run only this target id. An unknown id is an error that lists the available ids. |
--question |
all questions | Run only this question id. Skips the sample-size advisory, since the run is a single question. |
--split |
all |
Run only the train, validation, or test partition of the dataset. Any other value is an error. See datasets. |
--source |
manual |
The run source tag stored on the result row. |
--as-user |
none | The Drupal uid used as caller for every question. |
--dry-run |
off | List targets and question counts, then exit. Ignores --split, see below. |
--json |
off | Also write a JSON results file. |
Dry run¶
--dry-run resolves each target's dataset and prints the target id, the question
count, and the dataset reference. It calls no AI provider, grades nothing, and
writes no result row, so it costs nothing and is the cheap way to confirm that a
target points at the dataset you think it does.
$ drush ai-eval:run --dry-run
DRY RUN: would evaluate these targets:
my_target: 24 questions from my_dataset.yml
Warning
--dry-run ignores --split. The dry-run branch loads and counts the
dataset before the split reaches the runner configuration, so
--dry-run --split=test reports the whole dataset, not the test partition.
The count it prints is the full question count in that case.
The JSON results file¶
--json writes one file per invocation, covering every target the run touched.
It goes to the results_path set in configuration, or to
the system temp directory plus /ai_eval_results when that setting is empty. The
filename is results-YYYYmmdd-HHMMSS.json and the payload is the run timestamp
plus one entry per target id. The path is printed at the end of the run.
This file is a raw dump of the run, not a portable artifact. For an artifact meant to travel between sites, see sharing results.
Tagging the run source¶
--source sets the source column on the result row, and nothing else. It
defaults to manual, the UI writes ui, and a scheduled job should pass
--source=cron. The tag is carried into exported envelopes as run_source, so
"did this number come from a nightly job or from someone pressing Run" stays
answerable after the fact.
Running as a specific user¶
--as-user=N sets the run's default caller uid. A question that carries its own
caller_uid keeps it, so the option is a default rather than an override.
The caller is the Drupal account the test runs as: AI Eval switches to that
user for the agent or chat call, and back afterwards, so access checks and AI
Metering see that user. Judges are not switched. 0 means anonymous on purpose;
a uid with no account fails the question with "caller uid N has no account".
Warning
Without --as-user or a caller_uid, the test runs as whoever started the
run, and from drush that is anonymous. On a site with AI Metering, anonymous
traffic shares one monthly token budget, so agent tests run from drush
spend the budget your visitors use, and can use it up. Run agent tests as a
dedicated eval user.
A host module can also register an ai_eval.caller_context service
implementing Drupal\ai_eval\Service\CallerContextInterface, for user-scoped
state beyond the Drupal account (per-user memory, for example). AI Eval hands it
the caller uid as well; it ships no implementation of its own.
The result line¶
A must-pass failure prints a warning naming each question and grader, for
example q2 (fact_match_grader). It fails the gate whatever the average or
pass rate. Hard gates exit 1; soft gates still exit 0. JSON output includes
must_pass_failures with question ids and grader lists.
Each target prints one line with its average score, its pass rate with the likely range, the gate verdict and how long it took:
my_target: 4.80/5.0 (pass_rate: 100% [70.1%–100%], 9 of 12 measured: 3 errors) [PASS] 34s
The pass rate counts only the questions that were measured. A question that errored (the agent or a judge failed, for example on a provider rate limit) or that no grader could score is left out, so when that happens the line says how many of the questions the rate is over, and why the others are missing. When every question was measured, that part is not shown. The result page shows the same count next to the pass rate.
Advisories printed during a run¶
A run prints warnings that never change the verdict or the exit code:
- The dataset is below the sample-size floor. See scoring.
- The gate verdict rests on a judge that is not trusted: it failed validation, its validation is out of date, or it was never validated. See judges.
- The judge shares a model family with the evaluated model, so the scores are not an independent measurement.
- The target runs in agent mode and no attached grader observed what the agent did, so every score is a reading of the response text. See graders.
Exit codes¶
| Code | Meaning |
|---|---|
0 |
Every hard gate passed, or the run was a dry run. |
1 |
A hard gate failed, or the command could not run: unknown target, no targets configured, an invalid --split, or a failed JSON write. |
A soft gate that misses its threshold logs a warning and does not change the exit code by itself. Which targets are hard and which are soft is a per-target setting, covered in scoring.
With the refuse_unvalidated_judges setting on, a pass that rests on a judge
that has not passed validation is stored as a failed gate, so on a hard gate
the exit code is 1. The last line of the output names every gate that did
not pass and says whether it failed or its pass was not counted; it reads
All hard gates passed. only when every gate passed. Changing the setting
applies to runs that finish after the change, a run already in progress
included: earlier results keep the verdict they were stored with.
In CI¶
Because a failed hard gate exits 1, a scheduled pipeline job can gate on eval
health. AI Eval ships no cron integration on purpose: a full run can be hundreds
of LLM calls, and when to spend that belongs to site operations. A Drush run
executes every question in one process, so it needs no open page.
# .gitlab-ci.yml, run from a scheduled pipeline
ai_eval_nightly:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
script:
- drush ai-eval:run --source=cron --target=my_target --json
artifacts:
when: always
paths:
- sites/default/files/ai-eval-results/
expire_in: 30 days
Point the paths entry at whatever results_path you configured, and create the
schedule under CI/CD, Schedules in the project settings. A crontab line does the
same job on a host with no pipeline:
0 2 * * * cd /var/www/html && drush ai-eval:run --source=cron --as-user=2
Replace 2 with the uid of a dedicated eval user. Without --as-user or a
question's caller_uid, a Drush run calls the AI feature as anonymous (see
Running as a specific user).
Two things are worth adding to a pipeline that gates on eval results. Run
drush ai-eval:judges --json in the same job to record which judges were trusted
at the time, since an eval result is only as good as the judges behind it; see
judges. And export an envelope if you want the run's evidence to
outlive the pipeline log; see sharing results.