Sending email
A deployed app sends email from a function with the injected email() service, with no setup and no keys: 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> (skipped recipients say why: unsubscribed, blocked test/internal address, or server not configured); gipity email log lists recent sends AND skipped attempts.
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: schedule + cron:. Details (step types, template variables, limits): workflow.
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.
How email() sends work
- Every external email includes an unsubscribe link; a recipient who clicks it stops getting email from that app owner (across all their apps). Later sends to them are skipped and not charged.
- Test and internal addresses are blocked and skipped.
- A platform footer is appended to every email.
Limits
- Max 100 sends per 1 hour per project
- Max 50 recipients per
email()call - Plan batch sends accordingly - space them out if sending to many recipients
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. Then get a public URL for the transcoded version withgipity file url <path>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 (under ~500KB and ~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. Always host it (gipity file url <path>) and 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 before sending: save the HTML into the project, get its URL with
gipity file url <path>, and check it withgipity page screenshot <url>. - 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.