Pai includes a built-in email tool that lets agents send emails without any configuration. The platform holds the sending credentials; agents only supply the recipient, subject, and body.
- Sender address is fixed — always
bot@pairun.dev. Agents cannot change it. - Send-only — agents cannot read, list, or search email.
- No opt-in required —
send_emailis available in every session agent and every service Agent out of the box.
Using send_email in a session agent
The send_email tool is a built-in, enabled by default — the same as bash or web_search. Just use it in your system prompt:
apiVersion: pai.io/v1
kind: Agent
metadata:
name: daily-report
spec:
models:
- anthropic/claude-sonnet-4-6
system: |
You are a daily reporting agent. Fetch the day's metrics, write a concise
summary, and email it to the address provided in the task prompt.
The agent will have access to send_email automatically. No tools: block needed.
Disable email if not needed
If your agent should not be able to send email, disable it explicitly:
tools:
- type: send_email
enabled: false
Tool reference
| Parameter | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Recipient email address |
subject | string | Yes | Email subject line (max 998 chars) |
body | string | Yes | Plain-text email body |
html | string | No | Optional HTML version. If provided, recipients with HTML-capable clients see this instead of the plain-text body |
attachments | array | No | Files to attach. Each entry: {path, filename?}. The harness reads the file from disk, base64-encodes, and forwards through the email service. The screenshot tool uses this internally for the email_to= flow |
The tool returns "Email sent to <address> (status 202)" on success, or an [error] string on failure.
Attachments example
The agent decides to call:
send_email(
to="me@example.com",
subject="Today's report",
body="See attached.",
attachments=[{"path": "/workspace/report.pdf"}]
)
All three email backends (SMTP, Resend, SendGrid) support attachments — pick one per the Platform operator setup section.
Example A: one-shot task — CSV analysis with emailed report
This example walks through an agent that reads a CSV file, analyses it, and emails the results. No custom container image needed.
1. Create the Agent
# csv-reporter.agentdef.yaml
apiVersion: pai.io/v1
kind: Agent
metadata:
name: csv-reporter
spec:
models:
- anthropic/claude-sonnet-4-6
system: |
You are a data analysis agent. When given a CSV file path and a recipient
email address, you will:
1. Read the file with the `read` tool
2. Run Python with `bash` to compute key statistics (row count, column
summary, min/max/mean for numeric columns, top values for categoricals)
3. Write a clean markdown report to /workspace/report.md
4. Send the report as the email body using `send_email`
5. Confirm what was sent, including the file path and recipient
Format the email body in plain text with clear sections and a summary
table. Keep it under 2000 words.
packages:
pip:
- pandas
tools:
- type: bash
- type: read
- type: write
- type: send_email
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
pai apply -f csv-reporter.agentdef.yaml
2. Run a one-shot task
pai run analyse-q1 \
--agent csv-reporter \
--task "Analyse /workspace/sales-q1.csv and send the report to finance@example.com"
3. Stream the output
pai sessions logs analyse-q1 --follow
You'll see the agent work through the steps:
[harness] session=analyse-q1 title="Analyse /workspace/sales-q1.csv..."
Reading /workspace/sales-q1.csv...
Running pandas analysis...
Writing report to /workspace/report.md...
Sending email to finance@example.com...
Email sent to finance@example.com (status 202)
Done. Report saved to /workspace/report.md and emailed to finance@example.com.
4. Check the session status
pai sessions get analyse-q1
# NAME PHASE TOKENS STARTED COMPLETED
# analyse-q1 Complete 4821 2024-03-15 09:12:01 2024-03-15 09:12:34
Example B: scheduled weekly digest
This example creates an agent that runs every Monday morning, researches a topic using web search, and emails a digest. No CSV or volume needed.
1. Create the Agent
# weekly-digest.agentdef.yaml
apiVersion: pai.io/v1
kind: Agent
metadata:
name: weekly-digest
spec:
models:
- anthropic/claude-sonnet-4-6
system: |
You are a weekly digest agent. Each time you run, you will:
1. Use web_search to find the 5 most significant recent developments on
the topic provided in the task prompt
2. Use web_fetch (max_bytes=20000) to read the 3 most relevant articles
3. Write a 400–600 word digest in this format:
Subject: Weekly Digest — {topic} — {date}
TL;DR (2-3 sentences)
Top stories:
1. **Title** — one paragraph summary. Source: URL
2. ...
3. ...
Trends to watch:
- bullet point
4. Send the digest using send_email with:
- to: the recipient address from the task prompt
- subject: the subject line from the digest
- body: the full digest text
Always finish by confirming the email was sent and summarising the digest.
tools:
- type: web_search
- type: web_fetch
- type: send_email
tokens:
maxPerDay: 500000
maxPerRequest: 16000
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
2. Create the scheduled Agent
# weekly-digest.agent.yaml
apiVersion: pai.io/v1
kind: Agent
metadata:
name: weekly-digest-job
spec:
type: task
agentDefinition: weekly-digest
schedule: "0 8 * * 1" # Every Monday at 8:00am UTC
title: >
Research the latest developments in AI infrastructure and
send a digest to team@example.com
3. Apply both resources
pai apply -f weekly-digest.agentdef.yaml
pai apply -f weekly-digest.agent.yaml
kubectl get agent weekly-digest-job
# NAME TYPE PHASE SCHEDULE LAST RUN AGE
# weekly-digest-job task Scheduled 0 8 * * 1 Complete 2d
4. Trigger a manual run to verify
kubectl create job --from=cronjob/pai-cron-weekly-digest-job \
manual-$(date +%s) -n pai-system
Then stream its logs:
# Get the job pod name
kubectl get pods -n pai-system -l job-name --sort-by=.metadata.creationTimestamp | tail -1
pai sessions logs weekly-digest-job --follow
Using send_email in a service Agent
For persistent service Agents (bring-your-own-image), the platform injects PAI_EMAIL_ENDPOINT into every agent container. Call it directly:
import os, json, urllib.request
def send_email(to: str, subject: str, body: str, html: str | None = None):
payload = json.dumps({"to": to, "subject": subject, "body": body, "html": html}).encode()
req = urllib.request.Request(
os.environ["PAI_EMAIL_ENDPOINT"],
data=payload,
headers={
"Content-Type": "application/json",
"X-Pai-Workload-Name": os.environ.get("PAI_WORKLOAD_NAME", ""),
"X-Pai-Workload-Namespace": os.environ.get("PAI_WORKLOAD_NAMESPACE", ""),
},
method="POST",
)
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read())
The endpoint is http://pai-email.pai-system.svc.cluster.local/send — available in-cluster on all namespaces.
Rate limiting
Each workload is limited to 100 emails per day by default. This is configurable by platform operators via the PAI_EMAIL_DAILY_LIMIT environment variable on the email service deployment.
When the limit is reached, the tool returns an error and the HTTP endpoint responds with 429 Too Many Requests.
Platform operator setup
Before agents can send email, an operator must deploy the email service and configure a sending backend.
Deploy the service
kubectl apply -f platform/manifests/email-deploy.yaml
Configure a backend
SMTP (Gmail, AWS SES SMTP relay, Mailgun, Postmark, etc.):
kubectl create secret generic pai-email-smtp \
--from-literal=SMTP_HOST=smtp.example.com \
--from-literal=SMTP_PORT=587 \
--from-literal=SMTP_USER=bot@pairun.dev \
--from-literal=SMTP_PASSWORD=<password> \
--from-literal=SMTP_TLS=starttls \
-n pai-system
Resend (recommended for new deployments):
kubectl create secret generic pai-email-resend \
--from-literal=RESEND_API_KEY=re_... \
-n pai-system
Then set PAI_EMAIL_BACKEND=resend in the Deployment.
SendGrid:
kubectl create secret generic pai-email-sendgrid \
--from-literal=SENDGRID_API_KEY=SG.... \
-n pai-system
Then set PAI_EMAIL_BACKEND=sendgrid in the Deployment.
Restrict recipient domains (optional)
To prevent agents from emailing arbitrary addresses, set PAI_EMAIL_ALLOWED_DOMAINS on the Deployment:
- name: PAI_EMAIL_ALLOWED_DOMAINS
value: "example.com,corp.io"
Attempts to send to other domains return 400 Bad Request.
Metrics
The email service exposes Prometheus metrics at /metrics (port 8080):
| Metric | Labels | Description |
|---|---|---|
pai_emails_sent_total | workload, backend | Successfully sent emails |
pai_emails_rejected_total | workload, reason | Rejected sends (rate limit, domain policy) |
pai_email_errors_total | workload, backend | Delivery failures |