Skip to content

feat(workflows): support async workflow tool handlers - #233

Merged
Tsuyoshi Ushio (TsuyoshiUshio) merged 7 commits into
mainfrom
tsuyoshiushio-investigate-dynamic-workflow-async
Sep 23, 2026
Merged

Tsuyoshi Ushio (TsuyoshiUshio) merged 7 commits into
mainfrom
tsuyoshiushio-investigate-dynamic-workflow-async

Conversation

@TsuyoshiUshio

@TsuyoshiUshio Tsuyoshi Ushio (TsuyoshiUshio) commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Closes #139 and https://github.com/Azure/azure-functions-bucees-planning/issues/1303

Summary

@workflow_tool now accepts async def handlers. Workflow tools can call async SDKs and other I/O-bound APIs without a synchronous wrapper.

Why this change is small

The first Dynamic Workflows version ran the tool Activity as a synchronous function and called handler(args) directly. An async handler returned a coroutine that Durable could not serialize, so the registry rejected async def handlers.

The retry work (#193) changed the tool Activity to async def and added invoke_handler():

  • It awaits async handlers.
  • It runs synchronous handlers in a worker thread.

But the registry still rejected async handlers. This PR removes that old check.

The orchestrator does not change. It still uses def + yield, which the Python Durable SDK requires. Async code runs only inside the Activity.

Changes

  • workflows/registry.py: remove the inspect.iscoroutinefunction rejection and update the docstrings.
  • Tests:
    • The registry accepts an async handler.
    • Discovery keeps an async @workflow_tool in the handler catalog.
    • The tool Activity awaits an async handler and returns the correct result, with and without a plan retry policy.
    • An async WorkflowRetryableError starts a Durable retry. An unexpected error message is hidden. A result that is not JSON is a contract failure. (Async timeout already had a test.)
  • Docs: docs/workflows.md (async example), FRD 0004 handler contract, and a comment in the incident-triage sample.

Validation

  • ruff check src tests: pass
  • mypy src: pass
  • pytest tests: 1281 passed

Not included

This PR does not add an end-to-end test that runs on the Functions host (#139 asks for one). I can add it in a follow-up PR.

The workflow tool Activity already awaits coroutine handlers, but the registry still rejected async def handlers from the v1 design. Remove that rejection, add tests for async success, failure, and non-JSON results on the legacy and policy-aware paths, and update the docs.

Refs #139

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 22, 2026 23:53
Make fetch_deploys an async def handler that awaits a simulated deploy-history API call, and test that it runs through the workflow tool Activity.

Refs #139

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

The schema-wrapper invocation issue and documented guidance inconsistencies remain, and host-level end-to-end coverage is still needed.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

This PR adds support for async workflow tool handlers while preserving synchronous execution and retry behavior.

Changes:

  • Accepts and awaits async handlers.
  • Adds registration, discovery, Activity, retry, error, and result-validation tests.
  • Updates workflow documentation, FRD guidance, and sample comments.
File Summary Final review notes
tests/​test_workflow_registry.py Tests async registration. —
tests/​test_workflow_native_retry.py Tests Activity execution and retries. Nit (1 vote): Add Functions-host/Durable end-to-end coverage.
tests/​test_workflow_activity.py Tests async failures and result contracts. —
tests/​test_discovery_tools.py Tests async tool discovery. —
src/​azure_functions_agents/​workflows/​registry.py Removes async-handler rejection. Moderate (1 vote): Schema-wrapped dual-decorated tools can fail with TypeError; adapt the workflow adapter and add an invocation test. Nit (1 vote): Update the stale async-guard comment.
samples/​workflow-incident-triage/​src/​tools/​incident_tools.py Updates sample guidance. —
docs/​workflows.md Documents async handlers and usage. Nits (2 votes, 1 vote): Update contradictory front-matter guidance and clarify timeout cancellation differences.
docs/​frds/​0004-dynamic-workflows.md Updates handler requirements. Nit (1 vote): Revise contradictory dual-decorator guidance.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/workflows.md
Convert workflow argument dictionaries to schema models for both decorator orders. Keep synchronous handlers on worker threads and await async handlers. Update the authoring guidance.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@TsuyoshiUshio

Copy link
Copy Markdown
Contributor Author

Fixed the schema-wrapper invocation issue in 5d9f0fe. Workflow discovery now selects a dictionary-to-model adapter for tools that use both decorators. Both decorator orders work with synchronous and async handlers. Synchronous handlers stay on a worker thread, and the normal MAF wrapper is unchanged. Added regression coverage for invocation, model validation, decorator order, and thread selection. Updated the related authoring and timeout guidance. Functions-host/Durable end-to-end coverage was not added in this update.

@larohra Laveesh Rohra (larohra) left a comment

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.

Could you add a Functions-host/Durable end-to-end test that exercises an async @workflow_tool through the deployed workflow path?

The current tests invoke the generated Activity directly and validate the registry/catalog behavior, but they do not verify that the Functions host can discover the handler, register the workflow, schedule the Durable Activity, await the async result, serialize it, and complete the workflow successfully. This PR changes the registry → catalog → Activity boundary, so a host-level test is important to catch integration issues that unit tests cannot expose.

This is also part of issue #139's acceptance criteria. Please add the test under the existing workflow E2E coverage, or link a follow-up that explicitly tracks it if it cannot be included in this PR.

Run the workflow-incident-triage sample under func start and complete a workflow in which the async fetch_deploys tool and synchronous tools feed summarize_findings through Durable.

Refs #139

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@TsuyoshiUshio

Copy link
Copy Markdown
Contributor Author

Laveesh Rohra (@larohra) Added a Functions-host/Durable E2E test in 903be15: tests/endtoend/test_workflow_async_tool_e2e.py. It starts the workflow-incident-triage sample with func start, builds the orchestration input through the production start_workflow path, and runs the workflow through the Durable HTTP API. In the workflow, the async fetch_deploys tool and the synchronous fetch_logs and fetch_metrics tools feed summarize_findings. The test requires Completed and checks the async result that flows downstream. The test does not use a model and runs in the existing pytest -m e2e tests/endtoend job. It passed locally with Azurite. For a check against the old runtime, the same host run failed at the async Activity with an authorization error, while the synchronous Activities succeeded. Detailed failure, timeout, and non-JSON cases stay in unit tests.

Keep polling when the new instance is briefly not visible to the status API, and use a dedicated Durable task hub so other E2E hosts cannot affect the run.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the model-free Durable check with an agent-driven E2E. The test sends a chat prompt to the workflow-incident-triage sample through Foundry, lets the agent author the plan and call start_workflow, and confirms that the async fetch_deploys handler completes in the Durable Activity. Status polling tolerates one slow or dropped request and includes host output on failure.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@TsuyoshiUshio

Copy link
Copy Markdown
Contributor Author

Laveesh Rohra (@larohra) Follow-up: I changed the E2E test in dc0acc0. It now runs end to end through a live agent. It does not call the Durable HTTP API directly.

tests/endtoend/test_workflow_async_tool_e2e.py now does these steps:

  1. It starts the workflow-incident-triage sample with func start, Azurite, and the Foundry provider settings. The E2E pipeline supplies these settings through FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL. When no provider is set, the test is skipped, as test_samples_agentic.py is.
  2. It sends a chat prompt to /agents/main/chat. The agent writes the plan and calls start_workflow.
  3. It polls /agents/main/workflows for the session until the workflow is terminal. Then it checks that the workflow is Completed and that the output contains the result of the async fetch_deploys handler, which the Durable Activity awaited.

Status polling continues after one slow or dropped request until the overall deadline. If the test fails, the message includes the host output. The test uses a dedicated task hub (AsyncToolE2E). Local runs with live Foundry: 6 of 6 passed, about 30 s each.

Comment thread tests/endtoend/test_workflow_async_tool_e2e.py Outdated
Rename the E2E module to test_workflow_tools_e2e.py, because the workflow runs both sync and async handlers. The test now also checks the results of the sync fetch_logs and fetch_metrics tools.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@TsuyoshiUshio
Tsuyoshi Ushio (TsuyoshiUshio) merged commit 68ba17a into main Sep 23, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support async workflow tool handlers

3 participants