Human input for agent workflows¶
Being able to request human input for data input, decision verification, or action permission is an important part of many agent-powered workflows. Graph-based workflows in ADK can include human in the loop (HITL) nodes specifically built for obtaining input from humans as part of a workflow. These nodes do not require artificial intelligence (AI) models to run, which can make the input process more predictable and reliable.
Get started¶
You can implement a human input node in a graph using the RequestInput class and a text prompt for the user. The following code example shows how to add a human input node to a Workflow graph:
from google.adk.events import RequestInput
from google.adk import Workflow
def step1(): # Human input step
yield RequestInput(message="Enter a number:")
def step2(node_input):
return node_input * 2
root_agent = Workflow(
name="root_agent",
edges=[('START', step1, step2)],
)
In this code example, step1 pauses the execution of the agent until the
system receives an input from a user. Once the system receives input from the
user, that input is passed to the next node.
In ADK Go, human-in-the-loop (HITL) input is obtained through the
tool-confirmation mechanism rather than graph-based RequestInput nodes.
A tool signals that it needs human approval by setting
RequireConfirmation: true in functiontool.Config. The framework
automatically emits an adk_request_confirmation FunctionCall event to
the client. The client displays the confirmation prompt and sends back a
FunctionResponse with "confirmed": true or "confirmed": false.
// DoubleNumberArgs holds the input for the doubleNumber tool.
type DoubleNumberArgs struct {
Number int `json:"number" jsonschema:"description=The number to double."`
}
// DoubleNumberResults holds the output of the doubleNumber tool.
type DoubleNumberResults struct {
Result int `json:"result"`
}
// doubleNumber is a tool that doubles the given number.
// Because RequireConfirmation is true, the framework automatically pauses
// execution and emits an "adk_request_confirmation" event to the client before
// running the tool. The client must reply with a FunctionResponse confirming
// or denying the action.
func doubleNumber(ctx agent.ToolContext, args DoubleNumberArgs) (DoubleNumberResults, error) {
return DoubleNumberResults{Result: args.Number * 2}, nil
}
// newSimpleHITLAgent creates an LLM agent with a tool that always requires
// user confirmation before it executes.
func newSimpleHITLAgent(ctx context.Context) (agent.Agent, error) {
model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{})
if err != nil {
return nil, fmt.Errorf("failed to create model: %w", err)
}
doubleNumberTool, err := functiontool.New(
functiontool.Config{
Name: "double_number",
Description: "Doubles the given number. Requires user approval before running.",
RequireConfirmation: true, // Pause and ask for human approval on every call.
},
doubleNumber,
)
if err != nil {
return nil, fmt.Errorf("failed to create tool: %w", err)
}
return llmagent.New(llmagent.Config{
Name: "double_number_agent",
Model: model,
Instruction: "You are a helpful assistant. When asked to double a number, use the double_number tool.",
Tools: []tool.Tool{doubleNumberTool},
})
}
Configuration options¶
Human input nodes can use the RequestInput class with the following configuration options:
message: Text provided to the user to explain the human input request.payload: Structured data to be used as part of the human input request.response_schema: A data structure the human response must conform to.
Note: Response schema input limitations
For the response_schema setting, the RequestInput class does not automatically reformat human responses to fit a specified data structure. The human response must be provided in the specified format. For a better user experience, consider providing a user interface to collect structured data or use an Agent node to conform unstructured data to the format required.
In ADK Go, the HITL tool-confirmation flow is configured through
functiontool.Config and the agent.ToolContext interface. The equivalent
configuration options are:
RequireConfirmation(boolinfunctiontool.Config): Set totrueto make the framework always pause and ask for user approval before executing the tool. Equivalent to Python'smessageparameter.hint(argument toctx.RequestConfirmation): A human-readable string explaining why approval is needed, displayed to the user in the confirmation prompt.payload(argument toctx.RequestConfirmation): Any structured data to include alongside the confirmation request, allowing the client to render additional context for the user.
The client receives the adk_request_confirmation FunctionCall event and
must respond with a FunctionResponse whose body includes
"confirmed": true or "confirmed": false. After the response is received,
the framework re-invokes the tool function with ctx.ToolConfirmation()
returning the user's decision.
Note: Structured response from the client
ADK Go does not automatically parse or validate the structure of the
user's response payload. If your tool needs structured feedback (for
example, a selection from a list), include a UI or an agent node to
collect and validate that data before passing it back as the
FunctionResponse payload.
Human input examples¶
The following code examples demonstrate more detailed human input requests.
Request input with a hint message¶
The following code sample shows how to construct a RequestInput object in a workflow node, including a response schema:
async def initial_prompt(ctx: Context):
"""Ask the user for itinerary information"""
input_message = """
This is an interactive concierge workflow tasked with making you a great
itinerary for you in your city of choice. If you give some details about
yourself or what you are generally looking for I can better personalize
your itinerary.
For example, input your:
City (Required),
Age,
Hobby,
Example of attraction you liked
"""
yield RequestInput(message=input_message, response_schema=str)
The following code sample shows how to call ctx.RequestConfirmation inside
a tool function with a descriptive hint. The tool checks
ctx.ToolConfirmation() first; if no confirmation has arrived yet it
emits the pause request, otherwise it acts on the user's decision:
// BookFlightArgs holds the input for the bookFlight tool.
type BookFlightArgs struct {
Origin string `json:"origin" jsonschema:"description=Departure airport code."`
Destination string `json:"destination" jsonschema:"description=Arrival airport code."`
Date string `json:"date" jsonschema:"description=Travel date in YYYY-MM-DD format."`
}
// BookFlightResults holds the outcome of the bookFlight tool.
type BookFlightResults struct {
Status string `json:"status"`
ConfirmNumber string `json:"confirm_number,omitempty"`
}
// bookFlight is a tool that pauses for human approval before completing a
// booking. It calls ctx.RequestConfirmation with a descriptive hint message
// so that the client can display exactly what action is pending.
func bookFlight(ctx agent.ToolContext, args BookFlightArgs) (BookFlightResults, error) {
// Check whether the user has already responded to an earlier confirmation
// request for this exact tool call.
if confirmation := ctx.ToolConfirmation(); confirmation != nil {
if !confirmation.Confirmed {
return BookFlightResults{Status: "Booking cancelled by user."}, nil
}
// Confirmation received and approved — complete the booking.
return BookFlightResults{
Status: "Booking confirmed.",
ConfirmNumber: "FLT-20251031",
}, nil
}
// No confirmation yet: compose a human-readable hint and pause.
hint := fmt.Sprintf(
"The agent wants to book a flight from %s to %s on %s. Do you approve?",
args.Origin, args.Destination, args.Date,
)
if err := ctx.RequestConfirmation(hint, nil); err != nil {
return BookFlightResults{}, fmt.Errorf("failed to request confirmation: %w", err)
}
// Returning here suspends the tool; the framework re-invokes it after the
// client sends back a FunctionResponse for "adk_request_confirmation".
return BookFlightResults{Status: "Awaiting user approval."}, nil
}
// newHITLWithHintAgent creates an LLM agent whose bookFlight tool manually
// requests confirmation with a descriptive hint.
func newHITLWithHintAgent(ctx context.Context) (agent.Agent, error) {
model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{})
if err != nil {
return nil, fmt.Errorf("failed to create model: %w", err)
}
bookFlightTool, err := functiontool.New(
functiontool.Config{
Name: "book_flight",
Description: "Books a flight between two airports on a given date.",
},
bookFlight,
)
if err != nil {
return nil, fmt.Errorf("failed to create tool: %w", err)
}
return llmagent.New(llmagent.Config{
Name: "flight_booking_agent",
Model: model,
Instruction: "You are a flight booking assistant. Help the user book flights.",
Tools: []tool.Tool{bookFlightTool},
})
}
Request input with a data payload¶
The following code sample shows how to construct a RequestInput object
in a workflow node, including a payload and response schema. In
this example, the ActivitiesList is expected to be completed by an agent
node that composes a list of activities, and the get_user_feedback() node
requests feedback from the user.
class ActivitiesList(BaseModel):
"""Itinerary should be a list of dictionaries for each activity. Each
activity has a name and a description"""
itinerary: List[Dict[str, str]]
class UserFeedback(BaseModel):
"""Expected response structure from the user."""
user_response: str
async def get_user_feedback(node_input: ActivitiesList):
"""
Retrieves the user's thoughts on the agents initial itinerary in order to
either expand on, change the list, or exit the loop
"""
message = (
f"""
Here is your recommended base itinerary:\n{node_input}\n\n
Which of these items appeal to you (if any)?
"""
)
yield RequestInput(
message=message,
payload=node_input,
response_schema=UserFeedback,
)
The following code sample shows how to pass a structured payload alongside
the confirmation hint. The payload is sent to the client so it can render
the full itinerary for the user. The tool reads any structured feedback the
client includes in the FunctionResponse payload after the user responds:
// ItineraryItem represents a single activity in a travel plan.
type ItineraryItem struct {
Name string `json:"name"`
Description string `json:"description"`
}
// ReviewItineraryArgs holds the input for the reviewItinerary tool.
type ReviewItineraryArgs struct {
Itinerary []ItineraryItem `json:"itinerary" jsonschema:"description=List of activities to review."`
}
// ReviewItineraryResults holds the outcome after the user responds.
type ReviewItineraryResults struct {
Status string `json:"status"`
UserFeedback string `json:"user_feedback,omitempty"`
}
// reviewItinerary pauses for user feedback and sends a structured payload (the
// full itinerary) alongside the hint so the client can render it for the user.
func reviewItinerary(ctx agent.ToolContext, args ReviewItineraryArgs) (ReviewItineraryResults, error) {
if confirmation := ctx.ToolConfirmation(); confirmation != nil {
if !confirmation.Confirmed {
return ReviewItineraryResults{Status: "Itinerary rejected by user."}, nil
}
// Extract free-text feedback from the structured payload, if provided.
feedback := ""
if m, ok := confirmation.Payload.(map[string]any); ok {
if f, ok := m["user_feedback"].(string); ok {
feedback = f
}
}
return ReviewItineraryResults{
Status: "Itinerary approved.",
UserFeedback: feedback,
}, nil
}
hint := fmt.Sprintf(
"Here is your recommended itinerary (%d activities). Which items appeal to you?",
len(args.Itinerary),
)
// Pass the full itinerary as the payload so the client can display it.
if err := ctx.RequestConfirmation(hint, args.Itinerary); err != nil {
return ReviewItineraryResults{}, fmt.Errorf("failed to request confirmation: %w", err)
}
return ReviewItineraryResults{Status: "Awaiting user feedback."}, nil
}
// newHITLWithPayloadAgent creates an LLM agent whose reviewItinerary tool sends
// a structured payload to the client alongside the confirmation prompt.
func newHITLWithPayloadAgent(ctx context.Context) (agent.Agent, error) {
model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{})
if err != nil {
return nil, fmt.Errorf("failed to create model: %w", err)
}
reviewTool, err := functiontool.New(
functiontool.Config{
Name: "review_itinerary",
Description: "Presents the proposed itinerary to the user for feedback.",
},
reviewItinerary,
)
if err != nil {
return nil, fmt.Errorf("failed to create tool: %w", err)
}
return llmagent.New(llmagent.Config{
Name: "concierge_agent",
Model: model,
Instruction: "You are a travel concierge. Build an itinerary and present it to the user for review.",
Tools: []tool.Tool{reviewTool},
})
}