Skip to main content
A scorer group composes several scorers - LLM judges, patterns, code and external scorers - into one 0-10 score. Members are stored by reference, so a group reuses the scorers your project already has: editing a member scorer changes every group that includes it. The group itself is then what a dataset run or live traffic is graded with, which answers the question per-scorer configuration never could: “I have five opinions about this response - what is THE score?” Create and manage groups on the Scorers page’s Groups sub-tab.

How the score is computed

Every member keeps its native contract and is normalized to a 0-1 “goodness” before weighting: On the wire a member is { "kind", "refId", "weight", "gate" }, with exactly three kind values: judge (refId is an LLM judge scorer id), pattern (a pattern id), and custom (a custom-evaluator id - this one kind covers both code and external scorers, since either is a custom evaluator under the hood). The group score is the weighted average of member goodness, reported on 0-10 - so it reads like a rating everywhere ratings already live: the run rating column, CI gates (gate(fail_under=...)), and live alert thresholds. Three safeguards:
  • Renormalization: weights average over the members that actually produced a score - a deleted scorer or a judge outage shrinks the denominator instead of dragging the score down (the failed member is still reported on the result, never hidden). A group whose every member has weight 0 produces no score at all; gates still apply.
  • Must-pass gates: a member marked must pass zeroes the whole group score when its goodness lands below 0.5 (a matched failure pattern, a judge under 5/10, a code score under 0.5) - “every quality bar must pass”, regardless of how good the blend looks.
  • Fail-closed gates: a must-pass member that could not score at all (judge outage, deleted reference, a scorer that errors) produces no score rather than a blend that quietly reports “passed” for a safety check that never ran - the result reads No score: must-pass member "..." could not score (...).
A judge member contributes its rubric verdict only: its similarity metrics and attached code checks belong to that scorer’s own grading recipe and do not run inside a group. Blending metrics into a score stays an attached-code-check job, scoped to the scorer that owns those metrics.

Grading a dataset run with a group

Pass the group’s id as scorer_group_id (instead of scorer_id) - the group’s aggregate fills the rating column, judge members surface as labeled per-row verdicts, and deterministic members as scorer rows:
scorer_group_id and scorer_id are mutually exclusive - the group wins when both are sent, and a group-graded run also nulls out any additional_scorer_ids: the group is the whole grading recipe, not a layer on top of one. Member verdicts land per row (judges in judge_scorer_results, deterministic members as scorer rows), the run’s breakdown leads with “Name (group)”, and the Evaluate list’s Scorers column shows the group name. A non-gating member deleted after the group was built degrades to “not scored” on future results rather than failing the run; a deleted must-pass member fails the group closed instead - no score, per the fail-closed rule above. In the dashboard, the Create evaluation dialog offers your groups next to the judge scorer picker (“Or grade with a scorer group”), and the generated snippet emits scorer_group_id=. The Playground’s Scorers panel lists groups too - star one and it grades every grid cell (the aggregate in the rating pill, member verdicts as chips); starred groups are saved with Playground workbench profiles.

Scoring live traffic

Give a group an online profile and it scores live traffic exactly the way an online evaluator does: a group score below the alert threshold raises a Signal, events key on scorer-group:<id>, and the score history feeds the group’s ratings chart. The profile’s scope picks the unit of judgment - "trace" (the default) scores each sampled trace as it arrives, "session" scores whole multi-turn conversations once they go quiet.

Scoring each trace

The default scope - every sampled ingested trace runs every member:
Live scoring has full parity with online evaluators:
  • Score chip: when a group has scored a trace, the group score owns the Score chip in Live Traces (the composed final opinion beats any individual verdict for the headline); individual verdicts stay in the expanded row.
  • Trace Details: when both kinds scored the trace, Trace Details shows a Group score / Judge scores switch - the group aggregate with its blend breakdown on one side, per-member verdicts (recorded individually at scoring time) on the other. With only one kind present, the section shows it directly, no toggle.
  • Webhooks: a below-threshold group score pages the agent’s alert channels exactly like a matched pattern or a low online-evaluator verdict.
  • Judge budget: judge members draw from the same online judge-call budget online evaluators reserve from - a group with 3 judge members takes 3 slots per sampled trace, and an exhausted budget skips the group rather than scoring it on a partial panel.
  • KPIs: group scores (and member verdicts) are ratings, not runs - they never inflate the Overview’s run counts.
Judge members are real LLM spend at the sample rate - the sample dial is the cost dial.

Scoring whole sessions

Some failures don’t live in any single reply - the agent contradicts turn 1 on turn 3, or the conversation ends without the user getting what they came for. Set scope: "session" and the group judges the whole conversation as one unit, on the same idle trigger session evaluation uses: once a session with 2+ turns has been quiet for idleSeconds (default 120), the background sweep scores it - and re-scores it automatically if the conversation later grows. Individual traces of a session-scoped group are deliberately not scored at ingest, so a conversation is never double-judged.
Every member reads the same assembled transcript:
  • Judge members are asked the structured whole-session question built from their criteria (consistency across turns, whether the user’s need was resolved). A judge’s per-trace judgePrompt never applies at session scope - the same rule session evaluators follow.
  • Pattern and code/external members get the conversation split by speaker, not the formatted transcript: every user turn joined together as their input, every agent turn joined together as their output - so a “response contains” condition reads everything the agent said across the conversation without ever firing on the user’s own words. Both sides are read from root spans first, with fallbacks for OTel-style sessions whose roots are empty: the user side falls back to the full formatted transcript (over-matching is the honest direction for a tripwire), while the agent side falls back only to child span outputs - genuinely the agent’s words - and stays empty when even those are missing. Tool calls are not passed, and a code scorer’s spans array covers only the session’s last (anchor) trace, not every turn.
  • Weights, renormalization, and must-pass gates work identically - an apology tripwire pattern with weight: 0, gate: true zeroes a conversation that apologizes anywhere.
The verdict lands in the session’s judge rail (Observe → Sessions, labeled “Name (group)”), joins the group’s ratings history, and a below-threshold score raises a Signal keyed on the same scorer-group:<id> - and pages the agent’s webhook channels exactly like a below-threshold trace score. Judge members draw from the same online judge-call budget as trace scoring - one slot per judge call actually made. sampleRate is a true per-conversation fraction: the keep-or-skip decision is made once per (conversation, group) and stays stable across sweep ticks, so 0.2 really means about a fifth of conversations get judged. Reading verdicts back from the SDK:

SDK surface

client.monitor.scorer_groups: list(), get(id), create(name, members, description=None, online=None), update(id, **fields) (sparse; online is merged key-by-key with the stored profile, so omitted keys keep their values, and online=None detaches live scoring), delete(id), and ratings(id, window="7d"). The key-by-key merge only works when the group already has a stored online profile. Patching online on a group that has none returns a 400 asking for a full online object (enabled, sampleRate, alertThreshold, severity; scope defaults to "trace") - a partial patch against nothing would mean the engine silently inventing defaults you never chose. Limits enforced at save time: 1-20 members per group, weight 0-1000, sampleRate 0-1, alertThreshold 0-10 (or null), idleSeconds 0-86400. Members are validated at create/update time: an unknown refId returns a 400 instead of storing a group that silently blends nothing (refs that were already stored before a scorer was deleted are grandfathered - they degrade at scoring time as described above). A judge scorer with requiresExpected needs a reference answer and cannot grade live traffic, so it is rejected from any group whose live scoring is enabled - disable requiresExpected or keep the group’s live scoring off. The runnable, assertion-style walkthroughs are sample-scripts/eval_deep_dive/09_scorer_groups.py (dataset runs, gates, trace-scope live scoring) and 10_session_group_scoring.py (session scope end to end).
Groups replace the older habit of chaining scorers inside one judge’s attached code checks: those still work (and can still blend via scores), but a group is reusable, visible in the catalog, and composes every scorer kind - not just code.