Configuration

The Email contact channel allows agents to send emails and collect responses through email threads. Configure an email channel using the EmailContactChannel model:

from humanlayer import ContactChannel, EmailContactChannel

email_with_compliance = ContactChannel(
    email=EmailContactChannel(
        address="compliance@example.com",
        context_about_user="an email with the compliance team",
        subject="Re: Compliance Review",  # Optional - custom subject line
    )
)

Email Address

The address field must be a valid email address that will receive the messages.

Context

The optional context_about_user field helps the LLM understand who it’s emailing:

# Good context examples
"an email with the compliance team"
"an email with the user you are helping"
"an email with the head of marketing"

Usage

Use the email channel with either require_approval or human_as_tool features:

# With require_approval
@hl.require_approval(contact_channel=email_with_compliance)
def create_linear_ticket(title: str, assignee: str, description: str, project: str, due_date: str) -> str:
    """create a ticket in linear"""
    ...

import langchain_tools

# With human_as_tool in langchain
tools = [
    langchain_tools.StructuredTool.from_function(
        hl.human_as_tool(
            contact_channel=email_with_compliance,
        )
    ),
]

Or you can pass the contact_channel to the HumanLayer instance:

hl = HumanLayer(contact_channel=email_with_compliance)

If you pass a channel to the HumanLayer instance, you don’t need to pass it to the require_approval or human_as_tool features. If you pass it to both, the channel in the require_approval or human_as_tool will take precedence.

Custom Email Templates

You can provide custom Jinja2 templates to fully control the email body HTML. The template type is automatically detected based on whether it’s used with require_approval or human_as_tool.

Function Call Template Example

For function calls that need approval, your template has access to the function name, arguments, and approval actions:

from humanlayer import ContactChannel, EmailContactChannel

function_call_template = ContactChannel(
    email=EmailContactChannel(
        address="compliance@example.com",
        context_about_user="an email with the compliance team",
        template="""
            <html>
            <body>
            <h1>Function Approval Required</h1>

            <div style="background: #f5f5f5; padding: 15px; border-radius: 5px;">
                <h3>Function: {{ event.spec.fn }}</h3>
                <p>Arguments:</p>
                <pre>{{ event.spec.kwargs | tojson(indent=2) }}</pre>
            </div>

            <div style="margin-top: 20px;">
                <a href="{{ urls.base_url }}?approve=true"
                   style="background: #4CAF50; color: white; padding: 10px;
                          text-decoration: none; border-radius: 5px; margin-right: 10px;">
                    Approve
                </a>

                {% if event.spec.reject_options %}
                    {% for option in event.spec.reject_options %}
                    <a href="{{ urls.base_url }}?reject=true&option={{ option.name }}"
                       style="background: #f44336; color: white; padding: 10px;
                              text-decoration: none; border-radius: 5px; margin-right: 10px;">
                        {{ option.title or option.name }}
                    </a>
                    {% endfor %}
                {% else %}
                    <a href="{{ urls.base_url }}?reject=true"
                       style="background: #f44336; color: white; padding: 10px;
                              text-decoration: none; border-radius: 5px;">
                        Reject
                    </a>
                {% endif %}
            </div>
            </body>
            </html>
        """
    )
)

@hl.require_approval(contact_channel=function_call_template)
def create_ticket(title: str, description: str) -> str:
    """Create a new ticket"""
    ...

Human Contact Template Example

For human-as-tool contacts, your template has access to the message and response options:

human_contact_template = ContactChannel(
    email=EmailContactChannel(
        address="support@example.com",
        context_about_user="an email with the support team",
        template="""
            <html>
            <body>
            <h1>Agent Needs Input</h1>

            <div style="background: #f5f5f5; padding: 15px; border-radius: 5px;">
                <p style="font-size: 16px;">{{ event.spec.msg }}</p>
            </div>

            {% if event.spec.response_options %}
            <div style="margin-top: 20px;">
                <p>Please select one of these responses:</p>
                {% for option in event.spec.response_options %}
                <a href="{{ urls.base_url }}?option={{ option.name }}"
                   style="display: block; background: #2196F3; color: white;
                          padding: 10px; text-decoration: none; border-radius: 5px;
                          margin-bottom: 10px;">
                    {{ option.title or option.name }}
                    {% if option.description %}
                    <br>
                    <small style="opacity: 0.8">{{ option.description }}</small>
                    {% endif %}
                </a>
                {% endfor %}
            </div>
            {% else %}
            <div style="margin-top: 20px;">
                <p>Reply to this email with your response</p>
            </div>
            {% endif %}
            </body>
            </html>
        """
    )
)

tools = [
    langchain_tools.StructuredTool.from_function(
        hl.human_as_tool(
            contact_channel=human_contact_template,
        )
    ),
]

Template Variables

Both types of templates receive these variables:

  • event - The full event object (function call or human contact)
  • urls.base_url - The URL for approval/response actions
  • type - Either “v1beta2.function_call” or “v1beta2.human_contact”

If no template is provided, the default HumanLayer email template is used.

For a complete TypeScript example of email templates, see the email templates example.

Email Threading

By default, every human contact or function call will trigger a new standalone email thread.

However, if you’re building agents that are kicked off by email runs, you likely want the email responses to be collected in a single thread.

You can do this by using the in_reply_to_message_id and references_message_id parameters to the EmailContactChannel, using the inbound email’s Message-ID header as the value.

Below is an example where the inbound email is sent by the same human who will be responding to approval/human_as_tool requests.

def handle_inbound_email(raw_email_content: str, headers: dict) -> str:
    message_id = headers["Message-ID"]

    email_with_threading = ContactChannel(
        email=EmailContactChannel(
            address=headers["From"], # send agent messages to whomever initiated the email thread
            context_about_user="an email thread with the user you're assisting",
            in_reply_to_message_id=message_id, # reply to the inbound email
            references_message_id=message_id, # reference the inbound email
        )
    )

    hl = HumanLayer(contact_channel=email_with_threading)

    run_agent_in_response_to_email(
        base_prompt="You are a helpful compliance assistant, please handle this email",
        raw_email_content=raw_email_content,
        tools=[
            some_readonly_tool,
            hl.require_approval(some_risky_tool),
            hl.human_as_tool(),
        ]
    )

You can also use the helper method EmailContactChannel.in_reply_to() to create a channel that replies to an existing email:

email_channel = EmailContactChannel.in_reply_to(
    from_address=headers["From"],
    subject=headers["Subject"],
    message_id=headers["Message-ID"],
    context_about_user="an email thread with the user you're assisting",
)

Next Steps