Files
google__adk-docs/docs/graphs/human-input.md
Alexey Kalenkevich 1203686bb4 Add TypeScript tabs to the graph workflow pages (#2167)
* Add TypeScript tabs to the graph workflow pages

The five /graphs/ pages documented graph workflows for Python and Go only,
so a TypeScript reader had to infer the API from the Python tab — which
does not translate: TypeScript has no `@node` decorator, schemas are Zod
objects rather than pydantic models, state is written through `ctx.state`
instead of returned on an event, and a user-facing message is the event's
`content` rather than a `message` field.

Every section that has a Python tab now has a TypeScript tab before the Go
one, backed by 26 snippet files under examples/typescript/snippets/graphs/.
The snippets are ported from the runnable samples in adk-js
(samples/workflows/), which already map 1:1 to these section anchors, and
they all type-check against the adk-js workflow API.

The tabs also call out the behaviours that are easy to get wrong and have
no Python equivalent: `ctx.runNode()` resolves to a node result rather than
the output, and does not throw when a child interrupts; a second event
carrying `output` silently overwrites the first; `LlmAgent.inputSchema` is
not the node's input contract inside a graph.

* Drop inline comments from the graph workflow snippets

The `//` annotations inside the snippet regions duplicated the prose that
already introduces each tab, and they were the first thing a reader saw in a
rendered sample rather than the API itself.

Removes the 83 `//` comments inside the `--8<--` regions across all 26 files.
JSDoc blocks stay, since they document what a function or schema is rather
than annotating a line; the Apache headers and the per-file orientation
comments above each region are untouched, and neither renders on the docs
site anyway.

Verified comment-only: compiling every file before and after with
`tsc --removeComments` produces byte-identical `.js` and `.d.ts` output
across all 52 emitted files.

* Use single-quoted strings in the graph workflow snippets

The 26 files landed double-quoted, which reads as a deliberate choice next to
the existing TypeScript snippets under examples/typescript/snippets/ — those
are predominantly single-quoted (49 of 68 imports). The repo has no prettier
config, so nothing enforces either style; this just stops the new directory
looking different from its neighbours.

Formatting only: `prettier --no-config --single-quote`, and every changed line
differs from the original by a quote character alone. Compiling before and
after with `tsc --removeComments` produces output whose only differences are
the same quote swaps, since tsc preserves the source quote style.

* Restore the upstream titleCase guard in the nested workflow snippet

Porting samples/workflows/routes/nested_workflow inlined the `titleCase`
helper and dropped the check that a character's uppercase form is a single
code point, along with the comment explaining why it is there. That changed
behaviour for word-initial characters whose uppercase expands: "first draft"
became "FIrst Draft" and "ßeta test" became "SSeta Test", where upstream
leaves both alone.

Restores the helper, the guard and the rationale. Because the helper sits
inside the --8<-- region, the explanation now renders on the page as well, so
the next person to touch it can see what the guard is for.

Also restores an unused `_ctx` parameter in the user_message snippet, the only
other place the port had drifted from upstream.

Verified by compiling each of the 26 snippets and its upstream counterpart at
adk-v2.0.0 with `tsc --removeComments` and comparing the emitted JavaScript:
all 26 are now semantically identical to samples/workflows/.

* Address review: plainer wording, and lift shared cautions out of the tabs

Wording, across all TypeScript tabs:

- No sentence starts with code syntax. "`route` is independent of..." becomes
  "The `route` value is independent of...", and the same for the other cases.
- Removed informal and editorial phrasing: "earn their keep", "dropped
  straight into", "reach for", "hands you", "two things to know going in",
  "kick the children off", "fails loudly".
- Spelled out "/" as "and" in the `inputSchema` and `outputSchema` sentence.
- Described the `ctx.runNode()` interrupt behaviour in full rather than only
  as "does not throw": it returns normally with `interruptIds` populated and
  `output` undefined, and an orchestrator that skips the check continues with
  a value the user never supplied.
- Explained what a JoinNode waits for instead of referring to "the barrier".
- Tied the `rerunOnResume` option back to the code sample it follows, and
  introduced the two orchestrator details by saying when they matter.

Structure:

- The "Response schema input limitations" note appeared in both the Python
  and TypeScript tabs. Replaced both with one language-neutral note after the
  code examples.
- The "Stuck JoinNode" caution appeared in all three tabs. Replaced them with
  one caution after the code examples, stating the rule that every node
  feeding a join must produce an output.
- Moved the unbounded-cycle caution out of the TypeScript tab to the end of
  the section, since it is not language specific.

Snippet header comments got the same wording pass. Verified afterwards: the
26 snippets still type-check, all 53 snippet includes resolve, and every
snippet is still semantically identical to samples/workflows/ at adk-v2.0.0.

* Apply suggestion from @joefernandez

---------

Co-authored-by: Joe Fernandez <931947+joefernandez@users.noreply.github.com>
2026-08-28 15:39:52 -07:00

10 KiB
Raw Permalink Blame History

Human input for agent workflows

Supported in ADKPython v2.0.0TypeScript v2.0.0Go v2.0.0

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

=== "Python"

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:

```python
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.

=== "TypeScript"

In ADK TypeScript v2.0.0, a human input node yields a `RequestInput`.
The `step1` node pauses the workflow until the user replies, and the
reply is passed to the next node as its input. A human-in-the-loop node
does not require a model, which makes the pause deterministic.

```typescript
--8<-- "examples/typescript/snippets/graphs/human-input/get_started.ts:get-started"
```

This implementation shows the default `rerunOnResume: false` handoff:
the interrupted node does not re-run. It completes with the user's reply
as its output. A node that calls `ctx.runNode()` needs
`rerunOnResume: true` instead. For more information, see
[human input in dynamic workflows](/graphs/dynamic/#human-input).

=== "Go"

In ADK Go v2.0.0, a HITL graph node is built with
`workflow.NewEmittingFunctionNode` and `workflow.ResumeOrRequestInput`.
This is the direct equivalent of Python's `RequestInput` node:

-   On the **first pass**, `workflow.ResumeOrRequestInput` emits a
    `session.RequestInput` event (surfaced as `Event.RequestedInput`) and
    returns `ErrNodeInterrupted`, pausing the workflow.
-   After the human replies, the node is **re-invoked from the top**
    (`RerunOnResume: &true`) and `ResumeOrRequestInput` returns the reply
    payload, which flows as typed input to the next node via `event.Output`.

```go
--8<-- "examples/go/snippets/graphs/human-input/main.go:graph-hitl-get-started"
```

Configuration options

=== "Python"

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.

=== "TypeScript"

The `RequestInput` class takes the following configuration options:

-   **`message`:** Text shown to the user explaining what is being
    asked.
-   **`payload`:** Structured data sent with the prompt, so a client can
    render additional context.
-   **`responseSchema`:** The shape the reply is expected to take. The
    schema travels on the interrupt as
    `functionCall.args.response_schema`, which a client reads to render
    a form for the reply.

The `rerunOnResume` option on the node controls what happens when the
reply arrives:

-   **`false`** (the leaf default): the reply is routed to the node's
    successor as input, bypassing the interrupted node.
-   **`true`**: the node body re-runs from the top. This setting is
    required for any node that calls `ctx.runNode()`, so it can deliver
    cached child results on resume.

=== "Go"

`session.RequestInput` carries the following fields, which map directly to
Python's `RequestInput` parameters:

-   **`InterruptID`** (`string`): A unique identifier for this pause point.
    Use a stable prefix plus a UUID to avoid collision across workflow runs.
    Equivalent to the implicit interrupt ID in Python.
-   **`Message`** (`string`): Human-readable prompt displayed to the user.
    Equivalent to Python's `message` parameter.
-   **`Payload`** (`any`): Optional structured data sent alongside the
    prompt so the client can render additional context. Equivalent to
    Python's `payload` parameter.

`workflow.NodeConfig.RerunOnResume` controls what happens on resume:

-   **`&true`**: the node body is re-run from the top; `ResumeOrRequestInput`
    returns the human's reply on the second pass. Required for nodes that
    use `ResumeOrRequestInput`.
-   **`&false`** or **`nil`** (leaf default): the reply is routed to the
    node's successor as input, bypassing the interrupted node.

!!! note "Note: Structured response from the client"

    ADK Go does not automatically parse or validate the structure of the
    human's reply payload. If your workflow needs structured feedback,
    include a UI or a downstream agent node to validate the response before
    acting on it.

!!! note "Note: Response schema input limitations"

A response schema does not reformat a human reply to fit the specified
structure. The reply must already be in that format. For a better user
experience, collect structured data in your client interface, or place an
agent node after the pause to convert the reply into the required format.

Human input examples

The following code examples demonstrate more detailed human input requests.

Request input with a message and payload

=== "Python"

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.

```python
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,
   )
```

=== "TypeScript"

The following three-node graph builds a structured itinerary, sends it
as `payload` with the prompt so a client can render it, and then acts on
the user's feedback:

```typescript
--8<-- "examples/typescript/snippets/graphs/human-input/payload_and_schema.ts:payload-and-schema"
```

=== "Go"

The following code sample shows a three-node graph: a builder node generates
a structured itinerary, a HITL node sends it as `Payload` alongside the
prompt, and a final node acts on the user's feedback. The `Payload` field
lets the client render the full itinerary for the user before they respond:

```go
--8<-- "examples/go/snippets/graphs/human-input/main.go:graph-hitl-with-payload"
```

Tool-confirmation: approval prompts in LLM agents

Tool-confirmation is a separate, LLM-agent–level mechanism for yes/no approval prompts. Unlike graph HITL nodes, tool-confirmation works inside an llmagent tool function rather than as a standalone graph node. It is useful when you want an LLM agent to pause and ask for approval before executing a specific tool call.

=== "Python"

The following code sample shows how to construct a ***RequestInput*** object
in a workflow node, including a ***response schema***:

```python
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)
```

=== "TypeScript"

Set `requireConfirmation: true` on a `FunctionTool` to make the agent
pause for approval before that tool runs. A graph human-in-the-loop node
serves a different purpose: instead of confirming a tool call, it can
start the workflow by asking the user for input. The
`responseSchema: z.string()` option requests a plain text reply:

```typescript
--8<-- "examples/typescript/snippets/graphs/human-input/initial_prompt.ts:initial-prompt"
```

=== "Go"

Set `RequireConfirmation: true` in `functiontool.Config` for a static
yes/no approval before a tool executes, or call `ctx.RequestConfirmation`
from inside the tool for a custom hint message:

```go
--8<-- "examples/go/snippets/graphs/human-input/main.go:simple-hitl"
```

For a custom hint with manual re-entry handling:

```go
--8<-- "examples/go/snippets/graphs/human-input/main.go:hitl-with-hint"
```