A tool-calling agent can turn a model's request into an external action before anyone has reviewed it. Human approval in a Haystack agent pauses a selected tool call so an operator can reject it before execution or approve its arguments.

ConfirmationHook applies that decision at the before_tool hook point. A blocking strategy with AlwaysAskPolicy asks on each call to the mapped tool, including repeated calls with the same arguments. Tools without a matching strategy remain outside that approval control.

Console approval suits an agent running with an operator at its terminal. A deterministic generator and an in-memory tool make it possible to check rejection and approval without a model account, API key, or external change. The invocation count distinguishes a skipped call from a call that actually ran.

Steps to add human approval to a Haystack agent:

  1. Install Haystack 3.4.1 in an activated Python 3.12 environment.
    $ python -m pip install "haystack-ai==3.4.1"

    The code uses the Haystack 3 hook API. Python 3.12 and Haystack 3.4.1 were used for the run shown here.

  2. Create a file named approval_demo.py in your text editor.

    The file builds a local approval check before the hook is attached to a tool with external effects.

  3. Enter the imports and the in-memory tool in approval_demo.py.
    from haystack import component
    from haystack.components.agents import Agent
    from haystack.dataclasses import ChatMessage, ToolCall
    from haystack.hooks.human_in_the_loop import (
        AlwaysAskPolicy,
        BlockingConfirmationStrategy,
        ConfirmationHook,
        SimpleConsoleUI,
    )
    from haystack.tools import tool
     
    invocations = []
     
    @tool
    def record_action(label: str) -> str:
        """Record a label in memory."""
        invocations.append(label)
        return f"Recorded {label}"

    The list changes only inside the tool function. A returned rejection message alone cannot increase its length.

  4. Append a generator that requests the same tool call at the start of each run.
    @component
    class RequestGenerator:
        @component.output_types(replies=list[ChatMessage])
        def run(self, messages: list[ChatMessage], tools=None):
            if len(messages) == 1:
                reply = ChatMessage.from_assistant(tool_calls=[
                    ToolCall(
                        tool_name="record_action",
                        arguments={"label": "approval-demo"},
                        id="record-1",
                    )
                ])
            else:
                reply = ChatMessage.from_assistant(
                    "Request finished."
                )
            return {"replies": [reply]}

    This generator supplies a known request rather than calling a language model. It stops requesting tools after the agent adds the decision or tool result to the conversation.

  5. Append the approval strategy and its tool-name mapping.
    strategy = BlockingConfirmationStrategy(
        confirmation_policy=AlwaysAskPolicy(),
        confirmation_ui=SimpleConsoleUI(),
    )
    confirmation = ConfirmationHook(
        confirmation_strategies={"record_action": strategy},
    )

    The mapping must match the registered tool's name. A tool omitted from this mapping has no approval strategy here. For a real tool, approval permits its side effects; rejection does not undo earlier actions. Arguments injected from agent state are not shown by this confirmation UI, so a prompt alone does not review those values.

  6. Append the agent constructor with the confirmation hook registered before tool execution.
    agent = Agent(
        chat_generator=RequestGenerator(),
        tools=[record_action],
        hooks={"before_tool": [confirmation]},
    )

    For an existing agent, the configured chat generator and tools remain in place. The confirmation hook belongs in its existing before_tool list, and its strategy mapping names the sensitive tool. SimpleConsoleUI needs interactive terminal input; a web service needs an approval interface appropriate to its request handling.

  7. Append two agent runs that display the invocation count and tool response.
    for attempt in (1, 2):
        print(f"Request {attempt}")
        result = agent.run(messages=[
            ChatMessage.from_user("Record approval-demo.")
        ])
        print("Tool invocations:", len(invocations))
        for message in result["messages"]:
            for tool_result in message.tool_call_results:
                print(tool_result.result)
  8. Save approval_demo.py.
  9. Run approval_demo.py from its directory.
    $ python approval_demo.py
  10. Reject request 1 by entering n at the confirmation prompt.
    Confirm execution? (y=confirm / n=reject / m=modify): n
    Feedback message (optional):
  11. Leave the optional feedback empty by pressing Enter.
    Tool invocations: 0
    Tool execution for 'record_action' was rejected by the user.

    The zero count shows that the rejected request did not enter the tool function.

  12. Approve request 2 by entering y at the confirmation prompt.
    Request 2
    
    --- Tool Execution Request ---
    Tool: record_action
    Description: Record a label in memory.
    Arguments:
      label: approval-demo
    ------------------------------
    Confirm execution? (y=confirm / n=reject / m=modify): y
  13. Verify that the invocation count is 1 and the tool result contains the approved label.
    Tool invocations: 1
    Recorded approval-demo

    A count of 1 after rejection followed by approval shows that only the approved call ran. A different count or a missing result means the approval behavior has not been established.

Share this guide