sandboxReuse is also too slow, since the Python tasks have no reason to wait for the Rust tasks.
Eval groups solve this. Use defineEvalGroup() to put the 20 Rust tasks into one group: the group shares one Sandbox and runs its evals in ID order, while the Python tasks and other groups run in parallel as usual.
An eval group only schedules the Attempts that really need to run this time. Evals that reuse last results do not enter the group Sandbox; the rules for
--rerun, attempts, and early exit are the same as without groups.
Choose the right kind of reuse first
The same Experiment cannot both select eval groups and declare
sandboxReuse: true. Both decide which evals share a Sandbox; when both appear, the run reports eval-group-sandbox-reuse-conflict before creating any Sandbox.
Step 1: Put the group file next to its members
Create a directory underevals/ and put the members and eval-group.ts in it. The directory’s path is the group ID:
evals/**/eval-group.ts. Naming it *.eval-group.ts means it is not discovered; putting it at evals/eval-group.ts does not work either, because it has no enclosing directory to serve as the group ID.
Step 2: List the members in the group file
Each member default-exports the return value ofdefineEval() or defineScoreEval() as usual. The group file imports these return values and lists them in evals:
evals:
- It cannot be empty, and only accepts imported return values. Eval IDs, directory prefixes,
glob,tag, andselectorare not allowed. - NiceEval does not collect files from the same directory automatically. Adding or removing members always shows up in the diff of
eval-group.ts. - Members must be either all
defineEval()or alldefineScoreEval(). When they are mixed, the run reports an error before planning Sandboxes and lists the eval IDs of both kinds, so you can split them into two groups. - An eval belongs to at most one group, and cannot be listed twice in the same group.
onUnavailable is required. It decides what happens when the group Sandbox cannot be created, reset, or prepared:
"stop-group": the rest of this group’s evals are no longer dispatched."replace-sandbox": discard the current Sandbox and try creating a new one for the next Attempt.
eval-group.ts reports an error. Whether to spend more money rebuilding or to stop after a failure is your decision; NiceEval does not guess for you.
Which evals run is still decided by the Experiment and the command line. When only workflow/02-query is selected, workflow/01-index is not pulled into this run automatically.
Step 3: Have the Experiment provide the Sandbox
In most projects, the Experiment chooses the Provider andtemplate, and the eval group only decides “which evals share a Sandbox”:
sandboxReuse: true here. maxConcurrency: 4 is the cap for the whole Experiment and does not make the same group run four at once; the remaining concurrency capacity goes to other groups and ungrouped evals.
Eval groups only work with Agents that run in a Sandbox and Providers that support reuse. An Agent without a Sandbox cannot run eval groups.
Step 4: Confirm the grouping before running
First look at the plan with--dry; it creates no Sandbox and calls no model:
evalGroupId:
evalGroupId should be workflow. When the field is missing, first check that the group file’s name and location are evals/workflow/eval-group.ts. After confirming, run:
attempts is greater than 1, the Attempts of one eval run back to back before the next eval’s turn. Positions that do not need to run because of early exit or result reuse are skipped directly. This is only a stable queueing rule; it cannot be used to pass data between evals.
After the run, view results in the terminal with pnpm exec niceeval show, or in the browser with pnpm exec niceeval view.
Put shared preparation on the eval group
Sandbox preparation can be written in three places: the Experiment, the eval group, and the eval. Only one of the three can choosetemplate. A common split:
Write the eval group’s preparation steps as
sandboxLayer():
template, but they can add their own .before() / .after() commands. The preparation steps of the Experiment, eval group, members, and Agent are placed in one order: dependencies first, then ascending changeFrequency. So a rarely changing step in the eval group can come before a frequently changing step in the Experiment.
Lifecycle callbacks and Agent Context within a group can read ctx.evalGroup.id and ctx.evalGroup.definitionHash. The same function may also serve ungrouped evals, so evalGroup is an optional field. When you need to isolate caches or service names by group outside workdir, first check that the field exists, then build names from the group ID.
Do not rely on group order to pass results
After each Attempt, NiceEval restoresworkdir to its state when shared preparation finished. Files an earlier eval wrote into workdir are not visible to a later one; $HOME, /tmp, global installs, and background processes may stay behind. When you cannot accept those leftovers, do not reuse Sandboxes.
An earlier eval may not even run. Result reuse, command-line filtering, an exhausted budget, and interruption can all keep a member out of the Sandbox this time. When a later step must read a file from an earlier step, write both steps in the same eval. Only put evals in the same group when what they share already lives outside workdir and each eval can reach a verdict on its own.
Common errors
Read next
- Reuse Complete Eval Conditions with Plugins: write a full set of conditions needed by groups, experiments, or members once, and attach it where needed.
- Reuse Sandboxes: when the whole batch suits sharing Sandboxes, the lifecycle and leftover boundaries of
sandboxReuse: true. - Tune Concurrency: how eval groups work with the global and Experiment concurrency caps.
- Sandbox Provider Configuration: choose a
template, set lifetimes, and write preparation steps. - Rerun and Reuse: which Attempts enter the group queue this time.