microsoft / microsoft/agent-framework
.NET: Python: [Bug]: SwitchCaseEdgeGroup swallows case-condition errors and silently routes the message to the default branch
@eavanvalkenburg is already working on this.
Since Sep 18, 2026.
- Dominant language
- Python
- Stars
- 13.6k
- Forks
- 2.3k
- Avg merge
- 2d 45m
- Merged PRs (30d)
- 358
Description
Description
SwitchCaseEdgeGroup builds a selection_func that wraps every case predicate in a bare
except Exception and only logs a warning
(_edge.py):
def selection_func(message: Any, targets: list[str]) -> list[str]:
for case in cases:
if isinstance(case, SwitchCaseEdgeGroupDefault):
return [case.target_id]
try:
if case.condition(message):
return [case.target_id]
except Exception as exc: # pragma: no cover - defensive logging
logger.warning("Error evaluating condition for case %s: %s", case.target_id, exc)
raise RuntimeError("No matching case found in SwitchCaseEdgeGroup")
A predicate that raises is therefore treated exactly like a predicate that returned False:
iteration continues and the message lands on the default branch. The workflow completes
successfully and the only signal is a WARNING log line, so a typo or a missing attribute in a
routing predicate turns into silently wrong routing rather than a visible failure.
What I expected: the error surfaces, the same way it does for every other edge predicate.
This is inconsistent with the rest of the routing layer:
-
Edge.should_routedocuments the opposite policy for the very same concept, and deliberately
does not catch:Any exception raised by the callable is deliberately allowed to surface to the caller to
avoid masking logic bugs. -
FanOutEdgeRunner.send_messagealready has a path built for a selection function that raises —
it marks the edge-group spanEDGE_GROUP_DELIVERY_STATUS = EXCEPTIONand re-raises. The
except Exceptioninsideselection_funcmakes that path unreachable for switch-case groups,
so the failure is missing from telemetry too. -
The .NET switch-case predicate path (
SwitchBuilder.cs/WorkflowBuilder.CreateConditionFunc)
has no equivalent catch.
The block is marked # pragma: no cover, so no test exercises it today.
Code Sample
import asyncio
from dataclasses import dataclass
from agent_framework import Case, Default, Executor, WorkflowBuilder, WorkflowContext, handler
@dataclass
class Order:
kind: str
class Source(Executor):
@handler
async def run(self, order: Order, ctx: WorkflowContext[Order]) -> None:
await ctx.send_message(order)
class Sink(Executor):
def __init__(self, id: str):
super().__init__(id=id)
self.seen: list[Order] = []
@handler
async def run(self, order: Order, ctx: WorkflowContext) -> None:
self.seen.append(order)
def is_high_priority(order: Order) -> bool:
# Realistic routing-predicate bug: the attribute does not exist on this payload.
return order.priority == "high"
async def main() -> None:
src, high, fallback = Source(id="src"), Sink("high"), Sink("fallback")
wf = (
WorkflowBuilder(start_executor=src)
.add_switch_case_edge_group(
src,
[Case(condition=is_high_priority, target=high), Default(target=fallback)],
)
.build()
)
await wf.run(Order(kind="csv"))
print("high:", len(high.seen), "fallback:", len(fallback.seen))
asyncio.run(main())
Actual output — the run succeeds and the message is routed to the default branch:
WARNING:agent_framework._workflows._edge:Error evaluating condition for case high: 'Order' object has no attribute 'priority'
high: 0 fallback: 1
Expected: the AttributeError surfaces from wf.run(...).
Package Versions
agent-framework-core: 1.18.0 (repo main, commit fa359ab)
Python Version
Python 3.13
Additional Context
While confirming the fix I found a second, related ordering difference. SingleEdgeRunner checks
_can_handle before evaluating Edge.should_route, so a predicate is only ever asked about a
message the target could actually receive. FanOutEdgeRunner calls selection_func first, so a
switch-case predicate is also invoked for messages no target can handle. The existing test
test_switch_case_edge_group_send_message_with_invalid_data passes a str into a
lambda x: x.data < 0 predicate and relies on the swallow to reach its success is False
assertion.
So removing the catch on its own would convert that undeliverable-message case from "dropped,
returns False" into a raised AttributeError. Aligning FanOutEdgeRunner with
SingleEdgeRunner — drop messages no target can handle before running the selection function —
preserves that behaviour while letting genuine predicate errors surface.
I would like to work on this, covering both parts. Happy to split the FanOutEdgeRunner ordering
change into a separate PR, or to drop it and instead adjust the existing test, if you prefer.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.