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, 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)
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"""
...
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