Two distinct email channels
Gipity has two separate email paths; use the one matching who the recipient should see as the sender.
| Dimension | Platform email (agent channel) | User's Gmail (user channel) |
|---|---|---|
| Agent tool | agent_email_send |
gmail_send, gmail_reply, gmail_search, gmail_read |
| Web CLI | /email send |
`/gmail send |
| Local CLI | gipity email send |
`gipity gmail send |
| Sender address | gipity@gipity.ai |
the user's own Gmail address |
| Quota / deliverability | Gipity platform quota | User's Gmail quota |
| Approval | Required for external recipients; self-sends skip it | Required per connection settings |
| Requires | Nothing extra | A connected Google OAuth account |
| Use when | The platform owns the message (reminders, heartbeat alerts, platform notifications) | The message should come from the user personally (replies to Gmail threads, outreach from their own address) |
Default is agent_email_send. Only use gmail_* when the user explicitly asks to send from their own Gmail, or when you're replying to an existing Gmail thread.
These are agent tools (chat-time). A deployed app sends email from a function with the injected email() service: declare services: ['email'] and call await email({ to, subject, text }). The platform brokers delivery - owner-billed, one credit per recipient actually sent. The From address is platform-controlled (gipity@gipity.ai) for deliverability and anti-spoofing; the app may set only the display name (fromName) and replyTo (so replies land in the owner's inbox). Unsubscribed/blocked recipients are skipped and not charged. Full reference: the app-development skill.
// functions/send-welcome.js (gipity.yaml → services: ['email'])
export default async function (ctx, { email }) {
const r = await email({
to: 'lead@example.com', // string or array (max 50)
subject: 'Thanks for signing up',
text: 'We\'ll be in touch shortly.', // and/or html
replyTo: 'you@yourco.com', // optional — replies thread to you
fromName: 'Acme Sales', // optional display name
});
return r; // { sent: 1, skipped: 0, results: [{ to, status: 'sent' }] }
}
Under gipity test, email() returns { sent: 0, skipped: 0, results: [], test: true } without sending. Verify the real send path after deploy with gipity email test <to>; list recent sends with gipity email log.
Prefer a workflow notify step when the email is event- or schedule-driven - "email me when someone submits the form" (a record insert) or a scheduled digest - so no function has to run. The complete record-triggered recipe:
# workflows/contact-notify.yaml - email the owner on each new contact-form row
name: contact-notify
trigger: record.after_insert
table: contact_messages
steps:
- name: email_owner
step_type: notify
config:
channel: email
to: owner@example.com
subject: "New contact message from {{trigger.record.name}}"
body: "{{trigger.record.email}} wrote:\n\n{{trigger.record.message}}"
plus a workflows phase in gipity.yaml (type: workflows, source: workflows) so gipity deploy reconciles it. Three requirements make the record trigger fire: the workflow needs table:, the row must be written through the Records API (not raw SQL in a function), and an anonymous-visitor form needs the table set to auth_level: public. For a scheduled email instead of a record-triggered one, swap the trigger for trigger: scheduled + cron:. Details (step types, template variables, limits): workflow.
- Omit
toonagent_email_sendto send to the user's own email (skips approval).
Plain characters only
Use plain ASCII characters only. No em dashes, en dashes, smart/curly quotes, ellipsis, non-breaking spaces, or other fancy Unicode punctuation. Use a hyphen (-), straight double quote ("), straight apostrophe ('), and three periods (...). Applies to everything you write for the user: chat responses, email drafts and bodies, file content, memory, commit messages, code comments. Exception: if the user explicitly asks for specific characters or a particular style (for example "use em dashes in this poem", "make it fancy"), follow their lead - they are in charge of their own content.
Replying to a Gmail thread
Always
gmail_readthe message you're replying to first - you need its sender, date, and full body to build the quote.The
bodyyou pass togmail_replyMUST be: the new reply text, a blank line, then a standard quote headerOn <date>, <sender> wrote:, then the prior message body line-prefixed with>. Example:Sounds good - Tuesday at 2pm works for me. On Wed, Apr 17, 2026 at 9:14 AM, Alice <alice@example.com> wrote: > Can we move the sync to Tuesday? > Let me know what time works.Quote the message you're directly replying to (the most recent one), and leave any quoted chain it already contains intact - don't strip history.
Default to including the quote. Skip it only if the user explicitly says so (e.g. "just send 'yes', don't quote").
For
subject, prefix the original subject withRe:if it isn't already.
How agent_email_send works
- External sends require user approval (auto-requested by the tool). Do NOT call the tool again for the same email - the system sends automatically after approval.
- Every email includes an unsubscribe link - recipients can block future emails from the sender.
- Self-sends (omitting
toor sending to the user's own email) skip approval. - A platform footer is automatically appended to all agent-sent emails.
Rate Limits
- Max 10 emails per 1 hour per agent
- Plan batch sends accordingly - space them out if sending to multiple recipients
Parameters
to- recipient email (optional; omit for self-send)subject- subject line (required)body- plain text body (required)html- optional HTML body, used as the rich version alongside plain text
HTML Style Rules
- Inline styles only - email clients strip
<style>tags. Every element needsstyle="...". - System fonts:
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif - Single-column layout - centered
<div>or<table>withmax-width: 600px. No flexbox/grid. - Minimal colors - dark text (#333) on white (#fff). One accent color max.
- Font sizes: 14-16px body, 20-24px headings, never below 12px.
Images
- Default: transcode before emailing. For a user-supplied image (photo, screenshot, render), in the sandbox resize to 800px max on any side and convert to JPG at quality 85.
convert input.png -resize '800x800>' -quality 85 output.jpg(the>only shrinks, never enlarges). A 1.6MB phone photo typically drops to ~100–200KB. Thenhost_filethe transcoded version and link/embed that. - Send the original only if the user asks - "send the original", "don't compress", "full resolution", etc. If the input is already small and reasonable (
file_inforeports < ~500KB and under ~1200px), you can skip the transcode. - Never SVG - email clients strip it. Convert to PNG/JPG first:
cairosvg(Python) or ImageMagick. - Never base64 - email clients strip
data:URIs. Alwayshost_fileand link:<img src="https://..." width="..." height="...">. - HTML display width: use
width="600"(or less) on the<img>- 600px is the standard single-column email layout width.
Preview
- Preview rich/styled HTML emails with
html_previewbefore sending - verify layout and images render correctly. - Simple text-only emails don't need a preview step.
- Same hosted URLs work in both preview and email.
- 3MB preview limit - compress images in sandbox if needed.