Principle:Microsoft Agent framework Human in the Loop Request
Template:Microsoft Agent framework Sidebar
Overview
Human-in-the-Loop Request is the principle governing how a workflow executor pauses its execution to request information from an external system or human operator. An executor calls ctx.request_info(request_data, response_type) to emit a request_info WorkflowEvent and suspend processing. The workflow transitions into the IDLE_WITH_PENDING_REQUESTS state and remains there until the requested response is provided externally, at which point execution resumes from the point of suspension.
| Property | Value |
|---|---|
| Principle Name | Human-in-the-Loop Request |
| SDK | Microsoft Agent Framework |
| Repository | Microsoft Agent Framework |
| Source Reference | python/packages/core/agent_framework/_workflows/_workflow_context.py:L367-398
|
| API | WorkflowContext.request_info(request_data, response_type, *, request_id)
|
| Import | Access via WorkflowContext parameter in @handler methods
|
| Domains | Workflow_Engine, Human_in_the_Loop |
Description
Within the Microsoft Agent Framework's workflow engine, executors sometimes cannot proceed without information that is not available inside the workflow itself. This information might come from a human operator (e.g., confirming a decision, supplying credentials, or providing clarification) or from an external system (e.g., a third-party API that requires out-of-band authorization). The Human-in-the-Loop Request principle formalizes this pause-and-resume pattern as a first-class workflow primitive.
When an executor determines that it needs external input, it calls ctx.request_info(request_data, response_type) on its WorkflowContext. This call performs two actions:
- It constructs a
WorkflowEventof type"request_info", carrying therequest_datapayload, arequest_id(either caller-supplied or auto-generated as a UUID), and the expectedresponse_type. - It delegates to
RunnerContext.add_request_info_event(), which registers the event as a pending request and emits it into the event stream.
Once emitted, the workflow engine detects that there are unresolved pending requests and transitions the workflow state to IDLE_WITH_PENDING_REQUESTS. In this state, the workflow does not schedule further supersteps or executor invocations. It remains idle until an external caller provides responses via Workflow.run(responses={"request_id": response_value}).
Theoretical Basis
Pause-and-Resume Semantics
The request_info mechanism implements a cooperative suspension model. Unlike preemptive interruption, the executor explicitly chooses to yield control by calling ctx.request_info(). This makes the suspension point deterministic and auditable: the caller knows exactly which executor paused, what data it requested, and what response type it expects.
This design follows the continuation-passing pattern: the workflow's state (including the pending request registry) serves as the continuation, and the external response acts as the value that resumes the continuation. The framework persists this state in checkpoints, enabling durable workflows that survive process restarts.
State Machine Transition
The workflow state machine includes two request-related states:
| State | Description |
|---|---|
IN_PROGRESS_PENDING_REQUESTS |
The workflow is still executing (other executors may be active), but at least one request_info event has been emitted and is awaiting a response.
|
IDLE_WITH_PENDING_REQUESTS |
All executors have completed their current processing. The workflow has stopped and is waiting exclusively for external responses to pending requests before it can resume. |
The transition from IN_PROGRESS_PENDING_REQUESTS to IDLE_WITH_PENDING_REQUESTS occurs when the last active executor finishes its handler and there are still unresolved requests. When responses arrive, the framework matches them by request_id to the originating executor and dispatches the response through the executor's @response_handler.
Request Validation
Before emitting the event, request_info checks whether the executor has a @response_handler defined for the given (request_type, response_type) pair. If no matching handler exists, a warning is logged advising the developer to define one. The request is still emitted (it is not silently dropped), but any response that arrives will have no handler to process it.
This validation-with-warning approach follows the fail-open with diagnostics principle: it avoids hard failures in development while providing clear guidance about misconfiguration.
Request ID Correlation
Each request carries a request_id that uniquely identifies it within the workflow run. If the caller does not supply one, a UUID is auto-generated. This ID is used for three purposes:
- Event correlation: The
WorkflowEventcarries therequest_id, enabling external systems to match requests to responses. - Runner context tracking: The
RunnerContextstores pending requests in a_pending_request_info_eventsdictionary keyed byrequest_id. - Checkpoint restoration: When a workflow is resumed from a checkpoint, responses can be supplied by
request_id, ensuring they reach the correct executor.
Usage
Basic Request
from agent_framework import Executor, handler, WorkflowContext
class ApprovalExecutor(Executor):
@handler
async def need_approval(self, msg: str, ctx: WorkflowContext) -> None:
await ctx.request_info(
request_data="Please approve this action",
response_type=bool,
request_id="approval_request",
)
Streaming Detection
from agent_framework import Workflow
workflow = Workflow(...)
async for event in workflow.run("start", stream=True):
if event.type == "request_info":
print(f"Request from: {event.source_executor_id}")
print(f"Request ID: {event.request_id}")
print(f"Data: {event.data}")
Supplying Responses
# Resume the workflow by providing the response keyed by request_id
result = await workflow.run(
responses={"approval_request": True},
)
I/O Contract
Inputs
| Parameter | Type | Default | Description |
|---|---|---|---|
request_data |
object |
(required) | The payload describing what information is being requested. This is embedded in the emitted WorkflowEvent as event.data.
|
response_type |
type |
(required) | The expected Python type of the response. Used to validate the response and to match against @response_handler signatures on the executor.
|
request_id |
None | None |
Optional unique identifier for the request. If not provided, a UUID is auto-generated. Used for correlating responses back to this specific request. |
Outputs
| Output | Type | Description |
|---|---|---|
| Return value | None |
The method returns None. Its effect is the emission of a request_info WorkflowEvent into the event stream and the registration of the request as pending in the RunnerContext.
|
| Side effect: event | WorkflowEvent (type="request_info") |
A workflow event carrying request_id, source_executor_id, request_data, request_type, and response_type.
|
| Side effect: state | IDLE_WITH_PENDING_REQUESTS |
After all active executors finish, the workflow transitions to this state if unresolved requests remain. |
Related Pages
- Implementation:Microsoft_Agent_framework_WorkflowContext_Request_Info
- Workflow Streaming Execution -- streaming execution model that surfaces
request_infoevents - Workflow Result Processing -- processing results including pending requests