Composite scoring
TL;DR Don't ask "rank this candidate." Ask four independent Score questions in one call, divide each
scorebylen(criteria) - 1to normalize to 0–1, and combine with weights in code. Different weight vectors over the same answers give you different rankings (senior IC vs. engineering manager) without another API call.
Problem
From raw/docs/patterns__composite-scoring.md:
Oftentimes we want to rank a set of items based on several criteria at once.
A single "how good is this candidate" question buries the trade-offs inside the model: you cannot see why one resume outranked another, and adjusting the priorities means rewriting a prompt and re-running everything. You also cannot reuse the judgment for a second role with different priorities.
Pattern
Composite scoring is an easy way to think about this: break the judgment into independent dimensions, score each one separately, and combine them with weights you control in code.
Three moves:
- Decompose into dimensions that can be judged independently.
- Score each with its own rubric — an ordered
criteriaarray where every level "describes concrete situations and stands on its own" (agent skill). - Normalize and weight in code, where the policy is readable and editable.
Implementation
The documented example is resume screening: "you are processing resumes for engineering roles. You want to rank the candidates based on several criteria, and ultimately select the top X candidates for further review."
Step 1: score each dimension independently
Questions, verbatim from the source (the resume text is the state; add "state": ... and "model": "jev-latest" for a complete request, see HTTP API: POST /v1/systemone and GET /v1/models):
{
"python_depth": {
"type": "score",
"instructions": "How much depth of python experience does this candidate have, based on the supplied resume?",
"criteria": [
"No Python experience mentioned",
"Mentioned but no detail",
"Used in projects, some specifics",
"Primary language, multiple projects",
"Deep expertise: architecture, performance, libraries"
]
},
"team_leadership": {
"type": "score",
"instructions": "How much experience does this candidate have managing or leading engineering teams?",
"criteria": [
"No management experience mentioned",
"Informal mentorship or tech lead role",
"Led a small team or project",
"Managed a team with direct reports",
"Managed multiple teams or an engineering org"
]
},
"system_design": {
"type": "score",
"instructions": "How much experience does this candidate have designing large-scale or distributed systems?",
"criteria": [
"No architecture work mentioned",
"Contributed to design discussions",
"Designed components of a larger system",
"Owned architecture of a significant system",
"Designed systems at scale across multiple domains"
]
},
"generalist": {
"type": "score",
"instructions": "How much evidence is there that this candidate picks up unfamiliar tools, roles, or domains outside their core specialty?",
"criteria": [
"Only one domain or role mentioned",
"Some variety but within a narrow field",
"Worked across a few different areas or tech stacks",
"Regularly moved between domains, wore many hats",
"Track record of ramping up in unfamiliar areas and delivering"
]
}
}
Each rubric has five levels, so raw score values run 0–4. Every level is a concrete, standalone description rather than a bare adjective — that is what makes the levels comparable across candidates. In SDK v0.6.0 Score.criteria is an ordered sequence, not an int-keyed dict; see Score questions.
Step 2: combine with weights
Each dimension is normalized to 0–1 and weighted. The weights give you an easy way to adjust the relative importance of each dimension, without losing any of the nuance of the individual scores.
py = response.answers["python_depth"].score / 4
lead = response.answers["team_leadership"].score / 4
arch = response.answers["system_design"].score / 4
general = response.answers["generalist"].score / 4
# Senior IC
ic_score = (0.40 * py) + (0.10 * lead) + (0.40 * arch) + (0.10 * general)
# Engineering Manager
em_score = (0.15 * py) + (0.40 * lead) + (0.20 * arch) + (0.25 * general)
The divisor 4 is len(criteria) - 1 for these five-level rubrics — the maximum attainable score. Both weight vectors sum to 1.00, so each composite also lands in 0–1.
Why this beats one big question, per the source:
This gives you the ability to rank the candidates based on the composite score. But more importantly, it gives you visibility into how exactly the final score is being calculated. If the highest ranking candidates are not matching your expectations, you can adjust the weights to find the right balance.
And, critically, you re-rank without re-calling the model. The agent skill: "Changing a weight or display filter need not rerun inference when evidence and question meanings are unchanged."
When it fails
- The rule is not compensating. Weighted sums let a high score on one dimension offset a low one. The agent skill draws the line: "Weighted scores suit compensating preferences; an 'any serious violation' rule needs separate conditions." Hard disqualifiers belong in
ifstatements, not coefficients. - Rubrics of different lengths compared without normalizing. A raw
scoreof 2 means something different on a 3-level and a 5-level rubric. Normalize bylen(criteria) - 1before combining or comparing, as the example does. - Dimensions that are not independent. Decomposing too far destroys the thing being judged. The skill: "Split independently useful dimensions, without destroying the relationship being judged."
scoreis an expected value, not a level. It "can land between levels" (see HTTP API: POST /v1/systemone and GET /v1/models), soscore > 1.5is a real threshold, not a rounding error. Do notint()it away.- Ranking across items needs comparable questions. The skill: "use comparable per-item Scores for graded ranking" — identical rubrics per item, or the composites are not comparable.
- Ignoring
confidenceon the dimensions. Score answers carryconfidence; a composite silently averages away per-dimension uncertainty. Gate separately if the stakes warrant it — see Confidence-gated routing.
Variants
- Multiple weight vectors over one evaluation. The example's
ic_score/em_score— one call, two rankings. - User-tunable weights. The agent skill's "Turn judgments into reusable data": "Score dimensions once, then let code or user controls change weights, thresholds, rankings, and views."
- Scores as ML features. Same source: "With labeled outcomes, those signals can become classical ML features." See Cookbook: Autoresearch feature discovery.
- Composite scoring for reranking retrieved candidates. See Cookbook: Re-ranking.
- Mixing Noul gates with the weighted sum. Use a Noul for the disqualifying condition and the weighted composite for the rest (inferred from the "any serious violation" guidance above).
Related
- Score questions — rubric rules,
legend,probabilities,confidence - Patterns overview — the four-pattern catalog
- Writing instructions and criteria that Jev reads correctly — writing levels that stand on their own
- Confidence-gated routing — gating the dimensions you combine
- Cookbook: Re-ranking — ranking retrieved candidates
- Cookbook: Autoresearch feature discovery — judgments as features
Sources
- raw/docs/patterns__composite-scoring.md (https://docs.typesafe.ai/patterns/composite-scoring)
- raw/docs/patterns.md (https://docs.typesafe.ai/patterns)
- raw/github/skills/skills/typesafe-ai/SKILL.md (https://github.com/typesafe-ai/skills/blob/main/skills/typesafe-ai/SKILL.md)