ViteHub is still experimental. Expect bugs and breaking changes.

Email

Let an Agent send authorized plain-text email through the configured Email primitive.

email() grants an Agent one external side effect: email_send sends a plain-text message from an application-owned sender through the configured ViteHub Email primitive. Attach it only when the Agent should contact external recipients, scope exact addresses with recipients, then use policy when delivery requires approval or contextual authorization.

An approved call can contact real people and incur provider charges. Start with a short recipients allowlist, policy: 'require-approval', a provider test account, and an approved test recipient.

Configure the Email primitive first

Enable Email Definition discovery in the ViteHub preset and add one server/email.ts or server.email.ts. The Definition owns the delivery driver, provider credentials, and sender authorization.

vite.config.ts
import { defineConfig } from 'vite'
import { vitehub } from 'vite-hub'

export default defineConfig({
  plugins: [
    vitehub({ email: true }),
  ],
})

Follow Configure SMTP for the bundled adapter, or implement another provider behind the same EmailDriver contract. Keep credentials in Server Env or the deployment platform's secret store; the Capability never exposes them to the model.

Requirements

  • The application must run on Node.js 24 or later.
  • Email Definition discovery requires Vite 8 or later and exactly one supported Email Definition below the detected project root.
  • The configured provider must authorize the from address.
  • Generated Agent routes receive the Email runtime handle only while the Email Vite integration is active.

Grant the Agent permission to send

Import email() from the official Capability catalog. Set from to a sender that the configured provider authorizes.

server/agents/support.ts
import { defineAgent } from 'vite-hub/agent'
import { email } from 'vite-hub/agent/capabilities'

export default defineAgent({
  driver: { model },
  capabilities: [
    email({
      from: 'support@example.com',
      recipients: [
        'customer@example.net',
        'owner@example.com',
      ],
      policy: 'require-approval',
    }),
  ],
})

This configuration exposes one tool:

ToolSide effectResult
email_sendSends one plain-text message from the configured sender.The Email primitive's { id, driver } acceptance result.

A successful result means the active provider accepted the message and returned an ID. It does not prove inbox delivery, display, or reading.

Send a message

email_send accepts exactly three fields.

FieldTypeRequiredDescription
tostring | readonly string[]YesOne recipient address or a non-empty list. Every address must be non-empty.
subjectstringYesA non-empty subject.
textstringYesA non-empty plain-text body. Do not include credentials or other secrets.

The Capability fixes from from application configuration. The model cannot set HTML, headers, attachments, carbon-copy recipients, blind-carbon-copy recipients, or reply routing through this tool. The Capability checks that recipient strings are non-empty; the Email driver and provider still own mailbox syntax, sender authorization, and delivery rules. ViteHub does not add recipient-count, text-length, payload-size, or send-rate limits beyond non-whitespace validation. Your delivery provider owns those limits and may charge for every accepted recipient or message.

Authorize recipients

Use recipients as the hard boundary for exact addresses the Agent may contact. Every address in email_send.to must match the configured list; one address outside the list denies the entire call before the Email primitive runs, so ViteHub never partially sends a multi-recipient message.

server/agents/support.ts
email({
  from: 'support@example.com',
  recipients: [
    'customer@example.net',
    'owner@example.com',
  ],
})

ViteHub includes this list in Capability metadata and the email_send tool description, so the Agent can select a valid address without guessing. The list becomes part of the Agent's model context; include only addresses that the model is allowed to see.

Matching trims surrounding whitespace and ignores letter case, but the Capability forwards the original address values to the Email provider. Aliases, display-name forms, and other address variations remain different strings unless they appear explicitly in recipients. Set recipients: [] to deny all sends, or omit recipients when static recipient restriction belongs elsewhere.

The optional policy is an additional gate after this allowlist. It cannot widen recipients: a configured policy: 'allow' still denies an address outside the list, while policy: 'require-approval' prompts only for an address that passed the list. Without policy, allowed recipients send immediately; a policy callback can apply contextual authorization by returning allow, deny, require-approval, or retryable-failure.

Read Runtime policy, approvals, and traces before enabling unattended delivery.

Handle failures without duplicate delivery

The Capability forwards the Email primitive result and error unchanged. It does not retry.

Capability-owned validation and runtime failures happen before the Email driver runs, so these failures cannot have delivered a message:

FailureWhen it occurs
Invalid or missing fromemail() rejects the Agent Definition during construction.
Non-array recipients, or a blank, non-string, or sparse entryemail() rejects the Agent Definition during construction.
Missing Email primitiveCapability resolution rejects before the Agent Driver receives email_send.
Runtime handle without send()Capability resolution rejects before the Agent Driver receives email_send.
Empty to, subject, or textTool execution rejects before calling the Email primitive.
Recipient outside recipientsPolicy denies the entire tool call before calling the Email primitive.

After the Capability calls the Email primitive, handle EmailError.code according to the delivery state:

FailureWhat to do
invalid-messageFix the message before retrying. The driver was not called.
not-configuredEnable the Email integration and confirm exactly one Email Definition is discoverable.
authenticationFix provider credentials or sender authorization before retrying.
rate-limitApply an application-owned backoff or queue policy.
network or timeoutTreat delivery as uncertain. Check provider delivery logs before retrying, because the provider may already have accepted the message.
providerInspect protected server logs and provider delivery records. Never expose cause to the model.

ViteHub-produced EmailError.message values are safe for the public runtime boundary. ViteHub-wrapped provider failures remain in cause for protected server-side diagnostics and may contain addresses, credentials, or response content; custom drivers must preserve the same rule.

Keep Dynamic Markdown application-owned

email_send is plain-text-only by design. It does not render model-authored Markdown into HTML because renderEmailMarkdown() accepts trusted templates and does not sanitize authored HTML or trusted fragments.

When a product needs branded HTML, compose a trusted template in application code and call the Email primitive directly, or expose a Custom Capability with a narrow set of escaped template values. Do not pass unrestricted model output into a trusted HTML fragment.

Driver support

Agent DriverSupport
Model-backedReceives email_send after the Email primitive resolves.
Harness-backedRuntime requirements apply; model-facing Email tools are not passed by default.
Custom-run-backedReceives the resolved tool set; driver.run decides whether and when to call email_send.

Inspect and verify

Open the Agent in DevTools and confirm its tool list contains only email_send for this Capability. The Capability should report write mode and an email primitive requirement.

For the first delivery, use an approved test recipient and a test or sandbox provider account. Approve the call, confirm the tool returns a non-empty id, then verify the same message in provider delivery logs or the recipient mailbox.

Options

OptionTypeDefaultDescription
fromstringRequiredNon-empty application-owned sender passed to every message. The provider still validates and authorizes it.
recipientsreadonly string[]undefined (no static allowlist)Exact recipient allowlist. Every to address must match; an empty list denies all sends.
policyAgentToolPolicyDecision | function"allow"Optional approval or authorization policy for email_send.

email() has no mode option because Email exposes no read operation. The Capability always reports mode: 'write' so inspection and policy tooling can identify the side effect.

Reference

Copyright © 2026