Frameworks and agents · Published 2026-10-21 · Updated 2026-10-21 · AgentiSend
Send email from LangChain for Python
AgentiSend mail from LangChain for Python goes through one @tool invoked twice, and the second call returns a refusal as code and fix.
To send email from LangChain for Python on AgentiSend, register one @tool whose invoke posts to POST /emails with the agent's own key and returns a refusal as code and fix.
Install
pip install langchain-coreThe agentisend package is standard library only, so pip install agentisend adds no other dependency.
Environment
| Variable | Required | What it is |
|---|---|---|
AGENTISEND_API_KEY | yes | The agent's key, with sending_access and a budget on it. Not your own. |
MAIL_FROM | yes | The From address, on a domain you have verified. |
AGENTISEND_BASE_URL | no | Defaults to https://api.agentisend.com. |
The tool
"""LangChain — one tool that posts to POST /emails, and a scripted agent loop.
The loop is the two calls a model would make. The first sends. The second is
a different message, so a key whose budget is one comes back as
agent_budget_exceeded. No model is called.
Install both packages with `pip install agentisend langchain-core`. The suite
runs this file against the SDK checkout; a fresh project uses the released package.
AGENTISEND_API_KEY is the agent's key, with sending_access and a budget.
MAIL_FROM is an address on a domain you have verified.
AGENTISEND_BASE_URL is optional.
"""
from __future__ import annotations
import json
import os
from langchain_core.tools import tool
from agentisend import AgentiSend, AgentiSendError, idempotency_key
agentisend = AgentiSend()
TO = "langchain-py@example.com"
UPDATE = {
"to": TO,
"subject": "Ticket 4182 — we are looking into it",
"text": "Someone from support will reply within the hour.",
"purpose": "ticket-4182-update",
}
CLOSED = {
"to": TO,
"subject": "Ticket 4182 — closed",
"text": "The ticket is closed.",
"purpose": "ticket-4182-closed",
}
@tool
def send_email(to: str, subject: str, text: str, purpose: str) -> dict:
"""Send one email to a person who asked for it.
Returns the message id, or a refusal with a code and a fix. When the fix
says a person decides, stop and report it: calling again will not change
the answer.
"""
try:
sent = agentisend.send_email(
{"from": os.environ["MAIL_FROM"], "to": to, "subject": subject, "text": text},
idempotency=idempotency_key(purpose, to),
)
except AgentiSendError as err:
return {"sent": False, "code": err.code, "fix": err.fix}
return {"sent": True, "id": sent["id"]}
def run() -> None:
"""Two tool calls, then stop when the second is refused."""
for call in (UPDATE, CLOSED):
result = send_email.invoke(call)
print(json.dumps(result))
if result.get("sent") is False:
return
if __name__ == "__main__":
run()The decorator is @tool from langchain_core.tools, with no display name argument. invoke takes one dict whose keys are to, subject, text, and purpose, and it returns a dict the agent appends as the tool message. The file calls invoke twice and does not call a model. The second dict is a refusal:
{
"error": {
"code": "agent_budget_exceeded",
"message": "This key has used 1 of its 1 recipients for the current monthly period. Each To, CC and BCC recipient counts as one email.",
"fix": "Wait for the period to reset — get_agent_budget and GET /limits/keys/:id both say when. Raising a budget is a person’s decision, made in the console; a key cannot raise its own.",
"docs_url": "https://agentisend.com/what-does-agent-budget-exceeded-mean#agent_budget_exceeded",
"retryable": false
}
}idempotency_key(purpose, to) is the header. Reusing one purpose for two different messages replays the first. The JavaScript tool, built with tool() from @langchain/core/tools, is Send email from a LangChain.js agent. The key's ceiling is in An AI agent with a budget and a loop guard. This key cannot list arrivals. GET /emails/receiving needs full access, as Receiving and replying describes.