Skip to content

[BREAKING] Python: Add bulk executor registration method to WorkflowBuilder#3565

Closed
holtvogt wants to merge 17 commits intomicrosoft:mainfrom
holtvogt:feat/simplify-executor-registration-3551
Closed

[BREAKING] Python: Add bulk executor registration method to WorkflowBuilder#3565
holtvogt wants to merge 17 commits intomicrosoft:mainfrom
holtvogt:feat/simplify-executor-registration-3551

Conversation

@holtvogt
Copy link
Copy Markdown
Contributor

@holtvogt holtvogt commented Jan 31, 2026

Motivation and Context

Registering many executors currently needs repetitive register_executor() calls. This PR replaces the existing API with a dict-based bulk registration method (register_executors()) to reduce boilerplate and verbosity. This is most useful in complex workflows with many executors.

Description

Replaced the existing executor registration API with a bulk registration API on WorkflowBuilder that accepts a dict[str, Callable[[], Executor | Awaitable[Executor]]], reducing boilerplate when registering multiple executors. This PR also adds async factory support to workflows. Executor factories may now return either an Executor or an Awaitable[Executor], which is resolved via build_async().

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

Reference

Copilot AI review requested due to automatic review settings January 31, 2026 21:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a bulk executor registration method to the Python WorkflowBuilder class to reduce boilerplate when registering multiple executors. The change addresses issue #3551 by providing a dict-based API as an alternative to multiple individual register_executor() calls.

Changes:

  • Added register_executors() method that accepts a dict[str, Callable[[], Executor]] for bulk registration
  • Added unit test test_register_executors_bulk() to verify the new method works correctly and supports method chaining

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
python/packages/core/agent_framework/_workflows/_workflow_builder.py Added register_executors() method with proper validation, documentation, and examples following existing patterns
python/packages/core/tests/workflow/test_workflow_builder.py Added basic test for bulk registration functionality and chaining behavior

Comment thread python/packages/core/tests/workflow/test_workflow_builder.py Outdated
Comment thread python/packages/core/tests/workflow/test_workflow_builder.py Outdated
Comment thread python/packages/core/agent_framework/_workflows/_workflow_builder.py Outdated
@TaoChenOSU
Copy link
Copy Markdown
Contributor

Hi, thank you for your contribution!

This is actually something we wanted to improve too!

Comment thread python/packages/core/agent_framework/_workflows/_workflow_builder.py Outdated
Introduces build_async() to support async executor factories while minimizing
code duplication. The sync and async build paths differ only in how they
instantiate executors (sync raises on awaitables, async awaits them).
All other logic (validation, edge resolution, workflow creation) is
extracted into shared helpers to ensure maintainability and avoid
duplicating the logic.
@holtvogt
Copy link
Copy Markdown
Contributor Author

holtvogt commented Feb 5, 2026

@moonbox3 The async support involved some heavier lifting, mainly to avoid duplicating existing logic and to cleanly separate reusable helpers. Does this approach look reasonable, or would you prefer handling the duplication differently? Let me know what you think!

@moonbox3 moonbox3 added workflows Related to Workflows in agent-framework breaking change Introduces changes that are not backward compatible and may require updates to dependent code. labels Feb 6, 2026
@moonbox3 moonbox3 changed the title Python: Add bulk executor registration method to WorkflowBuilder [BREAKING] Python: Add bulk executor registration method to WorkflowBuilder Feb 6, 2026
Copy link
Copy Markdown
Contributor

@moonbox3 moonbox3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. Some comments/questions.

if inspect.isawaitable(instance):
# Close un-awaited coroutines to avoid runtime warnings or memory leaks
if hasattr(instance, "close"):
instance.close() # type: ignore[reportGeneralTypeIssues]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if instance.close() ever throws, the ValueError is never raised. Should we wrap it in a try/except?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we intentionally overwrite the error from close() using a finally block instead?

Copy link
Copy Markdown
Contributor Author

@holtvogt holtvogt Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth logging close() failures at WARNING level in an except block to explicitly flag possible resource leaks. This seems safer than silently swallowing the error. Thoughts?

if hasattr(instance, "close"):
instance.close() # type: ignore[reportGeneralTypeIssues]
raise ValueError("Async executor factories were detected. Use build_async() instead.")
factory_name_to_instance[name] = instance
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a isinstance(instance, Executor) check here like:

if not isinstance(instance, Executor):
      raise TypeError(
          f"Factory '{name}' returned {type(instance).__name__} instead of an Executor."
      )

Same for the new async path?

Also, when a factory in a large dict raises an exception during build(), the error has no indication of which factory name caused it. A try/except that adds the factory name would help improve debuggability, I think.

start_executor = self._start_executor

deferred_edge_groups: list[EdgeGroup] = []
executor_ids_seen: set[str] = set()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a duplicate ID is found, the error says which ID collided but not which two factory names produced it. What if we use a dict[str, str] (id -> name) so we could say: "ID 'agent' from factory 'B' conflicts with factory 'A'"?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have tests for:

  • mixed sync+async factories with build_async()?
  • coroutine .close() actually called on sync build with async factory?
  • build_async() when async factory raises exception?
  • multiple register_executors() calls with non-overlapping names?
  • whitespace-only names (like " ") failing as rejected?

Comment thread python/packages/core/agent_framework/_workflows/_workflow_builder.py Outdated
Comment thread python/packages/core/agent_framework/_workflows/_workflow_builder.py Outdated
@markwallace-microsoft
Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework/_workflows
   _workflow_builder.py3144286%249, 518, 619, 626–627, 730, 733, 738, 740, 747, 750–754, 756, 820, 897, 900, 962–963, 1119, 1134–1141, 1143, 1146, 1148–1150, 1192, 1205, 1479–1480, 1484–1486
TOTAL16400192788% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
3994 221 💤 0 ❌ 0 🔥 1m 8s ⏱️

holtvogt and others added 8 commits February 6, 2026 10:01
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
For duplicate IDs, the new messages specify the conflicting factories,
improving debugging. Added tests to verify that conflicts between
factory-produced and directly added executors raise appropriate errors.
@holtvogt
Copy link
Copy Markdown
Contributor Author

holtvogt commented Feb 6, 2026

@moonbox3 Thanks for the detailed feedback and the sharp eye. I addressed the points and will happily hand this back to you for another round 🔍

@holtvogt holtvogt requested a review from moonbox3 February 6, 2026 15:56
| _FanInEdgeRegistration
] = []
self._executor_registry: dict[str, Callable[[], Executor]] = {}
self._executor_registry: dict[str, Callable[[], Executor | Awaitable[Executor]]] = {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we allow async factories? This will force us to have an async build method.

I intentionally left out async factories when adding the factory pattern because I'd much prefer a single build method.

AzureAIAgent requires async methods to create or retrieve an agent, which makes this very tricky.

The AIProjectClient does support none-async agent creation. Should we add support for that too? @eavanvalkenburg

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it's nuanced,if you want the agent with Foundry to be created them you need the async 'create_agent' method, but you can always use the non-async 'get_agent' method, and then we create on the portal at first run, so I wouldn't let that determine this design because I agree, having non asyn build is preferred. @dmytrostruk can share additional insights, he did most of the work

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I suggested an asynchronous factory is because of the need for AzureAIClient. We need this to be an easy experience - why are we forcing users to first have to define an agent in the portal, AND then be able to use it in code? How is that a good dev experience?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AIProjectClient does support none-async agent creation. Should we add support for that too?

I don't think we can do this, using sync methods will block the event loop, can impact performance etc. I also don't think we need to force users to first have to define an agent in the portal. As @eavanvalkenburg mentioned, we have this ability in AzureAIClient to create an agent on first await agent.run() instead of doing it before the ChatAgent instance is created, which could be useful here, but from AzureAIClient perspective - it's unexpected design, which already created some confusion around it, so we should definitely revisit.

But I also don't see a problem to have async factory. There are local agents as well as remote agents, and there should be a mechanism to create both, so you can work with them.

@holtvogt holtvogt force-pushed the feat/simplify-executor-registration-3551 branch from ead3f1c to 6648453 Compare February 6, 2026 19:46
@moonbox3
Copy link
Copy Markdown
Contributor

Hi @holtvogt, we appreciate all the work and time you put into this PR. We're going to go a different direction by removing the factory methods. We will do this in a different PR. I will close this PR right now.

@moonbox3 moonbox3 closed this Feb 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Introduces changes that are not backward compatible and may require updates to dependent code. python workflows Related to Workflows in agent-framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: Simplify executor registration with dict-based API

7 participants