Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Decision protocol for the map-reduce / dynamic fan-out pattern in LM pipelines — "given list L, run f(item) for each item in parallel, then combine". Activates when the coder agent is about to process N items with N LM calls (per-doc summarize, per-query retrieve, per-candidate rank, parallel tool fan-out). Encodes the *when*, *how many at once*, *what to do when one fails*, and *how to reduce* — not the API of any single framework. Cross-framework: LangGraph `Send`, CrewAI parallel tasks / Flow
.claude/skills/agentsope-agentsop-map-reduce-fanout/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 273% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 333% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 300% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 326% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 298% | 0% |
> Pattern: results = reduce(combine, parallel_map(f, L)) where f is one > or more LM calls. The only reason to fan out is that latency or > throughput matters more than the cost of doing it. The only reason to > fan in is that the consumer wants one answer, not N.
> Source posture: claims grounded in primary docs and 2026 production > write-ups, cited inline with short tags resolved in the citation index.
Activate this skill when any of these is true:
call, a retriever hit, or any I/O-bound step costing >100ms.
for item in items: result = llm(item) loopand the items are independent (no item depends on the previous result).
asyncio.gather(...), ThreadPoolExecutor(...),Send(...), Process.hierarchical parallel branches, or crew.kickoff_for_each(...) and the question is how to use them safely.
"vote across M models", "ensemble", "parallel agents", "multi-query retrieval", "scatter-gather", "fan out".
InvalidUpdateError on a key two parallelbranches write to (see OP-3 cross-link in skill O5 state-reducer).
Send-based fan-out is hitting GRAPH_RECURSION_LIMIT orrate-limit 429s because all N workers fired at once [aipractitioner/scaling].
Do not activate when:
across docs) — fan-out destroys the dependency.
asyncio.wait(..., return_when=FIRST_COMPLETED), not gather.
strings in one OpenAI request). That's a batched single call, not map-reduce. Use it; it's cheaper.
Fan out for latency, fan in for coherence.
The whole protocol is two questions: what runs concurrently? and how do the answers merge? Everything else — Send, gather, Semaphore, reducers — is mechanics.
Three load-bearing concepts:
f(item) may itselfbe a multi-step LM workflow (retrieve → rerank → synthesize) — that is fine. What you parallelise is the per-item function. Don't confuse "parallel LM calls" with "parallel workflow instances". The latter is what Send and gather(f(x) for x in L) actually do [deepwiki/mapreduce].
(OpenAI/Anthropic API, your vector DB, your KV cache) has at least one of: RPM limit, TPM limit, connection-pool limit, GPU KV-cache budget. asyncio.gather(*[call(x) for x in 100_items]) will not call 100 things — it will fire 100, the API will 429 most, and you'll spend the next 20 minutes in retry-storm hell [newline/asyncio-llm] [tianpan/structured-concurrency]. The first thing you choose, before any code, is N_concurrent.
concatenate keepsall evidence (large output, no judgment); summarize collapses (information loss, smaller output); vote / majority picks one (lossy but decisive); rank-top-K selects the best few. Pick the reducer before you write the map — because the map's expected_output shape is dictated by the reduce.
The Pregel-lineage version of the same point: a fan-out / fan-in is one superstep. Either it all succeeds and the reduce node runs, or one branch fails and (in LangGraph) the whole superstep is discarded [aipractitioner/scaling]. Your code must decide before fan-out which semantics you want: atomic-or-nothing, or best-effort-with-holes.
Walk this top-down. Each step has a decision gate.
Gate: can f(item_i) run without seeing f(item_j) for any j ≠ i?
If no, stop. You have a sequential or recursive problem masquerading as map-reduce. Use a chain, or model the dependency explicitly (DAG, beam search, etc.). Fan-out will silently drop the cross-talk.
Before any code, multiply:
cost = N · cost_per_item (in $, tokens, AND seconds_wall)
peak_rps = N_concurrent / mean_latency_per_itemThree numbers must fit inside three budgets:
cost_$ ≤ task budgetpeak_rps ≤ min(API RPM/60, vector-DB QPS, GPU concurrency)peak_tps ≤ API TPM / 60 — TPM is the silent killer: 50 parallel callseach with a 4k-token prompt instantly exceeds most providers' TPM even if RPM is fine [newline/asyncio-llm].
If any budget is tight, fan-out is the wrong tool. Options: batch calls into single API request (embeddings, reranking), reduce N (pre-filter items), or accept sequential.
Default rubric:
| Constraint | Pick | |---|---| | API-bound (OpenAI/Anthropic) | min(10, RPM/60 · target_latency_s) — keep at most one "request-second" of headroom | | Self-hosted vLLM/SGLang | Start at num_kv_blocks / mean_prompt_blocks; for most setups 8–32 | | Vector DB retrieval | Provider QPS limit / 2 (leave headroom for other paths) | | Mixed (LM + tool calls) | Constrain by the slowest dependency | | No instrumentation yet | Start at 5. Always. Then measure. |
N_concurrent is enforced with asyncio.Semaphore(N) for asyncio, ThreadPoolExecutor(max_workers=N) for sync, max_concurrency config in LangGraph, max_rpm in CrewAI. Never rely on the framework's defaults.
Four canonical policies — pick one explicitly:
| Policy | When to use | How | |---|---|---| | Abort-all | One missing item invalidates the answer (legal review, regulated workflows) | asyncio.gather(*, return_exceptions=False) — first exception cancels siblings. In LangGraph this is the default superstep semantic [aipractitioner/scaling]. | | Best-effort | Concatenate / summarize use cases — partial is OK | asyncio.gather(*, return_exceptions=True) then filter; or per-task try/except returning a sentinel | | Retry-then-skip | API flakiness is the main failure | Wrap f with tenacity / backoff: 3 attempts × exponential, then sentinel | | Quorum | Vote / ensemble — need at least K of N | asyncio.as_completed, collect K, cancel the rest |
Anti-pattern: making this decision implicitly. The single most common production bug in this pattern is "I assumed gather would skip failures" or "I assumed one failed branch wouldn't kill the rest" — both are wrong defaults [aipractitioner/scaling] [newline/asyncio-llm].
| Reducer | Shape change | Cost | When to use | |---|---|---|---| | concatenate | [A, B, C] → "A\nB\nC" | None | Downstream LM can handle big context; you want full evidence | | summarize (LM call) | [A, B, C] → "abc" | +1 LM call | Output must fit in a final prompt; lossy by design | | vote / majority | [A, B, A] → A | None | Self-consistency / ensemble agreement | | rank-top-K | [(a,0.9), (b,0.4), (c,0.7)] → [a, c] | None | Multi-query retrieval, reranking | | merge-dedupe | overlapping lists → set | Cheap | Multi-query retrieval, multi-source enrichment | | tree-reduce | binary combine(x, y) recursively | log₂(N) LM calls | When pairwise merging is meaningful (summary-of-summaries) |
Choose by asking: what does the next node consume? The reducer is just "adapt the fan-out shape to the consumer's input shape."
Two independent timeouts:
asyncio.wait_for(call, timeout=30) — protects againstone stuck call holding a semaphore slot forever (the canonical "100 items, 99 done in 5s, the whole job blocked 5 min on item 73"). Default 30s.
asyncio.wait_for(gather(...), timeout=N · mean + 3σ)— protects against pathological N. Default 2× the optimistic wall estimate.
In LangGraph, recursion_limit is not a timeout — set both, plus node-level RetryPolicy [lc-docs/errors]. In CrewAI, max_rpm rate- limits but does not timeout — wrap kickoff() in asyncio.wait_for.
After the reducer runs, do one cheap LLM-free check:
concatenate → assert len(merged) is within expected bounds; raise ifone branch returned a 50KB blob.
vote → log the vote distribution; if it's near-uniform, the itemsweren't actually deciding the same question — go back to Step 1.
rank-top-K → assert scores are not all identical (failure mode of abroken reranker).
These cheap checks catch the "fan-out succeeded but the answer is garbage" bug class that no exception will surface.
Format: Trigger → Action → Output → Evidence.
asyncio.gather with bounded concurrency (Python baseline)/ OpenAI), N items, you want a list back.
python sem = asyncio.Semaphore(N_CONCURRENT) async def bounded(item): async with sem: return await asyncio.wait_for(f(item), timeout=PER_CALL_S) results = await asyncio.gather( *[bounded(i) for i in items], return_exceptions=True, # explicit best-effort ) ok = [r for r in results if not isinstance(r, Exception)]
the rate limiter; return_exceptions=True is the failure policy; wait_for is the per-call timeout.
[newline/asyncio-llm], [soumendrak/semaphore],[superfastpython/gather].
asyncio.as_completed for quorum / first-Kearly-exit, "first 3 of 10 retrieve hits").
python tasks = [asyncio.create_task(f(i)) for i in items] done = [] for fut in asyncio.as_completed(tasks): try: done.append(await fut) except Exception: pass if len(done) >= K: for t in tasks: t.cancel() break
[instructor/learn-async].Send for dynamic fan-outbranches is decided at runtime from state.
python from langgraph.types import Send def route_fanout(state): return [Send("worker", {"item": x}) for x in state["items"]] graph.add_conditional_edges("planner", route_fanout, ["worker"]) The worker writes to a list-typed state key with a reducer (see OP-4). Cap concurrency at .compile() time via compile(...).with_config({"max_concurrency": N}) or graph.invoke(..., {"max_concurrency": N}).
Send is its own tracesegment in LangSmith.
[deepwiki/mapreduce], [mlplus/langgraph-mr],[medium/send-api]. Skill langgraph-sop OP-4 is the canonical entry.
O5)Send-spawned workers (or any parallelbranches) write to the same state key. Without a reducer, LangGraph raises InvalidUpdateError.
summaries: Annotated[list[str], operator.add] forconcatenate, add_messages for chat, or a custom reducer for dedupe/top-K.
[cheatsheet/gotchas] "Reducers are mandatory, notoptional, for parallel execution"; [lc-docs/persistence].
kickoff_for_each (parallel crew invocations)topic).
python results = crew.kickoff_for_each(inputs=[{"topic": t} for t in topics]) # async variant: kickoff_for_each_async Rate-limit at crew creation: Crew(..., max_rpm=30). Wrap calls in asyncio.wait_for for total wall budget.
[crewai-docs/kickoff]. Note: CrewAI's hierarchicalprocess is not a fan-out primitive — see DC-3.
ThreadPoolExecutor for sync LM clientsvariant), but the workload is I/O-bound.
python from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=N_CONCURRENT) as ex: results = list(ex.map(f, items, timeout=PER_CALL_S)) as_completed(futures, timeout=...) for failure isolation. Avoid for CPU-bound f — use ProcessPoolExecutor or a job queue.
concurrent.futures docs.each → reduce.
MultiQueryRetriever or QueryFusionRetriever withnum_queries=N; let LlamaIndex parallelise internally. Combine with RRF (reciprocal rank fusion) as the reducer, not naive concat.
llamaindex-sop.pairwise summarize(a, b) → c is meaningful.
layer 2 N/2 → N/4, until 1. Each layer is its own bounded fan-out. In LangGraph, model as repeated Send rounds; in plain Python, recursive gather.
O(N), latency O(log N).predecessor used the same shape.
gather)f not the gather:python @tenacity.retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10), retry=retry_if_exception_type((RateLimitError, APITimeoutError)), ) async def f_retry(item): return await f(item) Retry inside the semaphore (slot held during backoff is intentional — it spaces out load).
bubble up to the failure-policy layer.
produces a wrong answer.
item_id in the LangSmith /OTel / Logfire trace. Log len(items), len(ok), len(failed), wall_time_s on the reduce step. For LangGraph, each Send is already a distinct trace segment — name the worker node descriptively (summarize_doc not worker).
[swarnendu/best]; LangSmith Send-tracing docs.100_docs]) against the Anthropic API. First 50 fire instantly, the rest queue inside the client; provider returns 429 for half; tenacity retries pile on; nothing finishes. Wall time worse than sequential.
if all fire) → TPM is the binding constraint, not RPM.
= 200_000 / (60 · 3000) ≈ 1.1 → round to 2 in-flight, not 10. RPM said 50 — TPM said 2. Bind to the tighter one.
100 · 5s / 2 = 250s— exceeds SLA.
similarity to the query → drop to 20 relevant docs.
20 · 5s / 2 = 50s ✓ under SLA.return_exceptions=True and a 30s per-call wait_for. Ifone doc hangs, the others still finish.
actual completed-per-second goes up because no retry-storm. SLA met.
and TPM, not the looser. When the math says SLA can't be met at the safe concurrency, reduce N before fan-out — never raise concurrency past the real ceiling. [newline/asyncio-llm]
Send fans out 50 workers, all write state['summaries'], get InvalidUpdateError"Send to spawn one summarizer perretrieved doc. State has summaries: list[str]. LangGraph crashes on the first run with InvalidUpdateError: At key 'summaries': Can receive only one value per step. Use an Annotated key to handle multiple values. Skill langgraph-sop flags this [cheatsheet/gotchas].
O5 state-reducer: parallel writes requirean explicit reducer.
summaries: Annotated[list[dict], operator.add]where each worker returns [{"doc_id": ..., "summary": ...}].
operator.add order (Python listconcat is in completion order); sort by doc_id in the reduce node.
doc_id.
merged list length and order.
regardless of completion order.
Sendworker must have a reducer. Pick operator.add for concat, add_messages for chat, a custom function for dedupe / top-K. Order is completion order, not Send order — sort in the reduce node if you need stability. [cheatsheet/gotchas], [deepwiki/mapreduce]
record). One record has malformed input → call returns 4xx. Coder doesn't know whether to abort the whole batch, drop the bad record, or retry it.
timeout) from permanent (400 bad input, 401, 422).
RateLimitError, APITimeoutError, APIConnectionError)). Permanent errors fall through immediately.
return_exceptions=True): permanentfailures land as exceptions in the result list, tagged with the input id.
failed_records list alongside ok_records— never lose the failure metadata.
len(failed) / N > threshold,surface a louder failure (e.g., refuse to emit the CSV). Otherwise emit partial + a sidecar errors file.
structured failures, not a 30-call atomic abort.
(a) is the consumer OK with holes? (b) can you classify transient vs. permanent? (c) what % failure makes the result useless? Encode those answers as code — not as hope. [aipractitioner/scaling]
temperature=0.7, vote on the answer. Three say "A", two say "B". Coder is about to return mode(answers).
Return both the mode and the agreement ratio.
confident_if agreement_ratio ≥ 0.8. Belowthat, return ("unknown", {"votes": Counter(...)}).
more) or escalate to a stronger model — never random tiebreak.
a near-tie is your early-warning signal for prompt drift.
false certainty; can choose to escalate.
Preserve the distribution as metadata; let the caller decide what "confident enough" means. Default threshold: 0.8 agreement for high stakes, 0.6 for low.
summarize_doc returns ~400 tokens. Flatconcat = 80k tokens → exceeds context for the final synthesis step.
budget allows extra LM calls.
LM call summarising 80k tokens loses too much detail.
layer 2 groups of 5 → 4 quarter-summaries; layer 3 → 1 final. Each layer is its own bounded fan-out (concurrency cap re-applies).
chain on suspicious shrinkage (one layer dropping >70% of content is usually a prompt bug).
200 + 20 + 4 + 1 = 225 calls (vs. flat-concat'simpossible 1 call); latency O(log_10 200) ≈ 3 layers.
overflows; latency stays roughly constant in N.
answer — but each layer is itself a bounded fan-out, with its own concurrency cap, failure policy, and verification. Treat each layer as a separate superstep. [langchain/map-reduce-chain]
Concrete don'ts:
asyncio.gather. "100 tasks at once" is notparallelism — it's a denial-of-service against your own dependencies. Always wrap in a semaphore, even if you think N is small [newline/asyncio-llm].
wait_for, a single stuckcall holds a semaphore slot forever and silently lowers your effective concurrency to N−1, then N−2, etc.
best-effort, retry-then-skip, or quorum. Implicit defaults are wrong half the time: gather(...) aborts on first failure (surprise to most), but LangGraph parallel branches also abort the entire superstep — not the same level but the same shape of surprise [aipractitioner/scaling].
Send for fixed-cardinality work. If N is always 3,static parallel edges are simpler, more readable, and easier to trace [aipractitioner/scaling]. Send is for runtime-variable N.
reducer. This is the canonical InvalidUpdateError (cross-link skill O5 state-reducer) [cheatsheet/gotchas].
the goal. Self-consistency requires diversity — sample with temperature > 0, vary the prompt, or vary the model. Same prompt + temperature 0 + N parallel calls = N identical answers and a meaningless vote.
f. asyncio.gather andThreadPoolExecutor parallelise I/O, not CPU. CPU-bound f (heavy local tokenisation, image preprocessing) needs ProcessPoolExecutor or a job queue.
flat-concat dressed up; the context-overflow failure mode is identical. See DC-5: tree-reduce when N is large.
max_concurrency. LangGraph,CrewAI, and LlamaIndex all default to "as many as you ask for". Set the cap explicitly, in code, near the fan-out.
A 50ms function fanned 100-wide over a 200ms-RTT network finishes in ~250ms regardless of N — the win evaporates. Profile first.
Hard boundaries — when this skill is the wrong tool:
search). Use a chain or a DAG.
map-reduce. Use it.
the previous call's writes). That's a state machine, not map-reduce.
| Framework | Primitive | Concurrency cap | Failure default | Reduce mechanism | |---|---|---|---|---| | asyncio | gather(*coros) / as_completed | Semaphore(N) | return_exceptions=False aborts on first | Post-gather list comprehension; manual | | threads | ThreadPoolExecutor(max_workers=N) | max_workers arg | Per-future result() raises | as_completed; manual aggregation | | LangGraph | Send("worker", state) from conditional edge | config={"max_concurrency": N} | Atomic superstep — one fails, all discarded [aipractitioner/scaling] | State reducer (operator.add, add_messages, custom) — mandatory for parallel writes [cheatsheet/gotchas] | | CrewAI | crew.kickoff_for_each(inputs=[...]), Flow @listen | Crew(max_rpm=N) | Per-kickoff; not atomic | Caller-side merge; or downstream task with context=[...] | | LlamaIndex | MultiQueryRetriever, QueryFusionRetriever, async_aggregate=True on QueryEngine | Internal; configurable in retriever | Per-retriever | RRF / dedupe / score-merge built into the retriever | | OpenAI/Anthropic batch APIs | Native batch endpoint | Provider-managed | Per-item | One sync API call returns the list |
Quick chooser:
Send + reducer (OP-3 + OP-4).kickoff_for_each (OP-5); for conditionalrouting → CrewAI Flow with parallel @listen branches.
(1 call), not fan-out.
The general rule: prefer the lowest abstraction that lets you set concurrency cap, per-call timeout, and failure policy explicitly. If a framework hides any of the three, wrap it.
pythonimport asyncio from typing import Awaitable, Callable, TypeVar T = TypeVar("T"); R = TypeVar("R") async def map_reduce( items: list[T], f: Callable[[T], Awaitable[R]], reduce: Callable[[list[R]], R], *, n_concurrent: int = 5, per_call_timeout_s: float = 30.0, fail_policy: str = "best_effort", # "abort" | "best_effort" ) -> R: sem = asyncio.Semaphore(n_concurrent) async def bounded(x: T): async with sem: return await asyncio.wait_for(f(x), timeout=per_call_timeout_s) raw = await asyncio.gather( *(bounded(x) for x in items), return_exceptions=(fail_policy == "best_effort"), ) if fail_policy == "best_effort": ok = [r for r in raw if not isinstance(r, BaseException)] failed = [r for r in raw if isinstance(r, BaseException)] if not ok: raise RuntimeError(f"all {len(items)} branches failed: {failed[:3]}") return reduce(ok) return reduce(raw)
Use this when there's no graph framework already in the codebase. Replace reduce with lambda xs: "\n".join(xs) (concat), Counter(xs).most_common(1)[0][0] (vote), or a follow-up LM call (summarize). The same three knobs — n_concurrent, per_call_timeout_s, fail_policy — show up in every framework variant; this snippet just makes them explicit.
Send referencepythonimport operator from typing import Annotated, TypedDict from langgraph.graph import StateGraph, START, END from langgraph.types import Send class State(TypedDict): items: list[str] summaries: Annotated[list[dict], operator.add] # reducer is mandatory class WorkerState(TypedDict): item: str def fanout(state: State): return [Send("worker", {"item": x}) for x in state["items"]] async def worker(s: WorkerState): # one LM call per item; state writes are merged via the reducer above text = await llm.ainvoke(f"Summarize: {s['item']}") return {"summaries": [{"item": s["item"], "summary": text.content}]} def reduce_node(state: State): ordered = sorted(state["summaries"], key=lambda d: state["items"].index(d["item"])) return {"final": "\n".join(d["summary"] for d in ordered)} g = StateGraph(State) g.add_node("worker", worker) g.add_node("reduce", reduce_node) g.add_conditional_edges(START, fanout, ["worker"]) g.add_edge("worker", "reduce") g.add_edge("reduce", END) app = g.compile() result = await app.ainvoke({"items": docs}, {"max_concurrency": 5})
Cross-references: skill langgraph-sop (OP-4 Send, OP-3 reducers), skill O5 state-reducer (deeper on parallel-write semantics).
[deepwiki/mapreduce] = deepwiki.com/langchain-ai/langchain-academy/7.1-map-reduce-pattern[medium/send-api] = medium.com/@vishy2k5/langgraph-send-api-7aaab56bc6b8[mlplus/langgraph-mr] = machinelearningplus.com/gen-ai/langgraph-map-reduce-parallel-execution/[aipractitioner/scaling] = aipractitioner.substack.com/p/scaling-langgraph-agents-parallelization[cheatsheet/gotchas] = sumanmichael.github.io/langgraph-cheatsheet/cheatsheet/faqs-gotchas/[lc-docs/persistence] = docs.langchain.com/oss/python/langgraph/persistence[lc-docs/errors] = docs.langchain.com/oss/python/langgraph/errors[newline/asyncio-llm] = newline.co/@zaoyang/python-asyncio-for-llm-concurrency-best-practices[soumendrak/semaphore] = soumendrak.com/blog/semaphores-python-async-programming/[superfastpython/gather] = superfastpython.com/asyncio-gather-limit-concurrency/[instructor/learn-async] = python.useinstructor.com/blog/2023/11/13/learn-async/[tianpan/structured-concurrency] = tianpan.co/blog/2026-04-09-structured-concurrency-ai-pipelines-parallel-tool-calls[crewai-docs/kickoff] = docs.crewai.com/en/concepts/crews (kickoff_for_each)[swarnendu/best] = swarnendu.de/blog/langgraph-best-practices/[langchain/map-reduce-chain] = python.langchain.com/docs/versions/migrating_chains/map_reduce_chain/| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-08 | fail→fail | 17,428 | 11,614 | -33% | 1 | 1 | 0% | 2,578 | 11,630 | +351% | 0 | 0 | — |
case-01 | fail→fail | 22,158 | 30,854 | +39% | 1 | 1 | 0% | 4,114 | 13,394 | +226% | 0 | 0 | — |
case-14 | fail→fail | 13,686 | 11,600 | -15% | 1 | 1 | 0% | 2,021 | 11,433 | +466% | 0 | 0 | — |
case-02 | fail→pass | 17,369 | 12,597 | -27% | 1 | 1 | 0% | 3,171 | 11,832 | +273% | 0 | 0 | — |
case-03 | fail→fail | 23,223 | 14,742 | -37% | 1 | 1 | 0% | 4,452 | 12,242 | +175% | 0 | 0 | — |
case-04 | pass→pass | 10,501 | 11,083 | +6% | 1 | 1 | 0% | 1,889 | 11,576 | +513% | 0 | 0 | — |
case-05 | pass→pass | 12,454 | 13,046 | +5% | 1 | 1 | 0% | 2,070 | 11,697 | +465% | 0 | 0 | — |
case-06 | pass→pass | 14,646 | 10,059 | -31% | 1 | 1 | 0% | 2,428 | 11,089 | +357% | 0 | 0 | — |
case-07 | pass→pass | 17,104 | 13,446 | -21% | 1 | 1 | 0% | 2,977 | 11,710 | +293% | 0 | 0 | — |
case-09 | fail→pass | 20,639 | 15,862 | -23% | 1 | 1 | 0% | 2,852 | 12,355 | +333% | 0 | 0 | — |
case-10 | pass→pass | 9,679 | 7,560 | -22% | 1 | 1 | 0% | 1,645 | 10,875 | +561% | 0 | 0 | — |
case-11 | pass→pass | 14,384 | 12,257 | -15% | 1 | 1 | 0% | 2,310 | 11,557 | +400% | 0 | 0 | — |
case-12 | pass→pass | 13,547 | 12,420 | -8% | 1 | 1 | 0% | 2,269 | 11,625 | +412% | 0 | 0 | — |
case-13 | pass→pass | 11,737 | 11,896 | +1% | 1 | 1 | 0% | 1,966 | 11,485 | +484% | 0 | 0 | — |
case-15 | fail→pass | 17,803 | 14,456 | -19% | 1 | 1 | 0% | 3,049 | 12,210 | +300% | 0 | 0 | — |
case-16 | pass→pass | 15,521 | 11,510 | -26% | 1 | 1 | 0% | 2,538 | 11,544 | +355% | 0 | 0 | — |
case-17 | pass→fail | 12,993 | 11,657 | -10% | 1 | 1 | 0% | 2,195 | 11,521 | +425% | 0 | 0 | — |
case-18 | fail→pass | 16,366 | 15,273 | -7% | 1 | 1 | 0% | 2,876 | 12,241 | +326% | 0 | 0 | — |
case-19 | fail→fail | 20,697 | 18,529 | -10% | 1 | 1 | 0% | 3,712 | 12,800 | +245% | 0 | 0 | — |
case-20 | fail→pass | 18,919 | 19,153 | +1% | 1 | 1 | 0% | 3,246 | 12,904 | +298% | 0 | 0 | — |
case-21 | fail→fail | 10,678 | 13,994 | +31% | 1 | 1 | 0% | 1,740 | 11,948 | +587% | 0 | 0 | — |
case-22 | pass→pass | 7,991 | 8,813 | +10% | 1 | 1 | 0% | 1,411 | 11,088 | +686% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.