---
title: "Email"
description: "Send emails and collect responses via email threads"
icon: "envelope"
---

## Configuration

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

```python
from humanlayer import ContactChannel, EmailContactChannel, HumanLayer

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

hl = HumanLayer(contact_channel=email_with_compliance)
```

<Frame>
  <img
    src="/images/email-create-linear-ticket.png"
    alt="HumanLayer Email Flow"
  />
</Frame>

### 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:

```python
# 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:

```python
# 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:

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

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

```python
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](https://github.com/humanlayer/humanlayer/tree/main/examples/ts_email_templates).

## 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.

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

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

- [Configure response options](/core/customize-response-options)
- [Try composite channels](/channels/composite-channels)
- [See a complete FastAPI email example](https://github.com/humanlayer/humanlayer/tree/main/examples/fastapi-email)
