> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gavai.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Send transactional email

> Trigger a templated email on every form submission via sendEmail.

export const ErrorTable = ({errors}) => <div className="gv-error-table" role="table" aria-label="Errors and fixes">
    <div className="gv-error-table__head" role="row">
      <span className="gv-error-table__col-head" role="columnheader">Error</span>
      <span className="gv-error-table__col-head" role="columnheader">Cause</span>
      <span className="gv-error-table__col-head" role="columnheader">Fix</span>
    </div>
    {errors.map((err, i) => <div className="gv-error-table__row" role="row" key={i}>
        <code className="gv-error-table__code" role="cell">{err.code}</code>
        <div className="gv-error-table__cause" role="cell">{err.cause}</div>
        <div className="gv-error-table__fix" role="cell">{err.fix}</div>
      </div>)}
  </div>;

export const PageMeta = ({audience, readingTime, prereqs, level}) => {
  const items = [audience && ({
    key: "audience",
    label: "AUDIENCE",
    value: audience
  }), readingTime && ({
    key: "time",
    label: "READ",
    value: readingTime
  }), level && ({
    key: "level",
    label: "LEVEL",
    value: level
  }), prereqs && prereqs.length > 0 && ({
    key: "prereqs",
    label: "PREREQS",
    value: prereqs.join(" · ")
  })].filter(Boolean);
  if (items.length === 0) return null;
  return <div className="gv-page-meta" role="group" aria-label="Page metadata">
      {items.map(item => <div className="gv-page-meta__item" key={item.key}>
          <span className="gv-page-meta__label">{item.label}</span>
          <span className="gv-page-meta__value">{item.value}</span>
        </div>)}
    </div>;
};

export const BrandCard = ({eyebrow, title, href, icon, badge, cta, featured = false, horizontal = false, children}) => {
  const classes = ["gv-card", featured && "gv-card--featured", horizontal && "gv-card--horizontal"].filter(Boolean).join(" ");
  const inner = <>
      {icon && <span className="gv-card__icon" aria-hidden="true">
          <Icon icon={icon} />
        </span>}
      <div className="gv-card__main">
        {(eyebrow || badge) && <div className="gv-card__meta">
            {eyebrow && <Eyebrow label={eyebrow} tone="muted" />}
            {badge && <span className="gv-card__badge">{badge}</span>}
          </div>}
        <h3 className="gv-card__title">{title}</h3>
        {children && <div className="gv-card__body">{children}</div>}
        {cta && <span className="gv-card__cta">
            {cta}
            <svg className="gv-card__cta-arrow" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <line x1="7" y1="17" x2="17" y2="7" />
              <polyline points="7 7 17 7 17 17" />
            </svg>
          </span>}
      </div>
    </>;
  return href ? <a className={classes} href={href}>
      {inner}
    </a> : <div className={classes}>{inner}</div>;
};

export const NumberedStep = ({n, title, children}) => <li className="gv-step">
    <span className="gv-step__index">{n}</span>
    <div className="gv-step__body">
      <h3 className="gv-step__title">{title}</h3>
      <div className="gv-step__content">{children}</div>
    </div>
  </li>;

export const NumberedSteps = ({children}) => <ol className="gv-steps">{children}</ol>;

export const ArrowItem = ({children}) => <li className="gv-arrow-item">
    <span className="gv-arrow-item__arrow" aria-hidden="true">{"↳︎"}</span>
    <span className="gv-arrow-item__content">{children}</span>
  </li>;

export const ArrowList = ({children}) => <ul className="gv-arrow-list">{children}</ul>;

export const Eyebrow = ({label, tone = "default"}) => <div className={`gv-eyebrow gv-eyebrow--${tone}`}>
    <span className="gv-eyebrow__bar" aria-hidden="true" />
    <span className="gv-eyebrow__label">{label}</span>
  </div>;

<Eyebrow label="GUIDE" />

<PageMeta audience="Developers" readingTime="12 min" level="Intermediate" prereqs={["Verified sending domain", "Form block on a page", "Email template"]} />

By the end of this guide, every successful submission of a `Form` block on your page will send the lead a templated thank-you email — with their submitted values interpolated into the subject and body. gavAI sends the mail via the transactional email gateway on your behalf using your verified domain.

## Prerequisites

<ArrowList>
  <ArrowItem>A verified sending domain on your workspace. Domain verification is handled in the Builder under **Workspace → Settings → Email**; a public API endpoint for verification is not yet exposed.</ArrowItem>
  <ArrowItem>A `Form` block already on a page in draft, bound to a schema table such as `leads`.</ArrowItem>
  <ArrowItem>An email template — created in step 2 below.</ArrowItem>
</ArrowList>

## Steps

<NumberedSteps>
  <NumberedStep title="Verify your sending domain">
    gavAI sends email via the transactional email gateway, but uses your domain as the sender so replies route to you and deliverability is tied to your reputation. Verify the domain in the Builder under **Workspace → Settings → Email**. Once verification completes you will see a green checkmark in the console; every `sendEmail` call returns `domain_not_verified` until then.

    <Warning>
      Configure SPF, DKIM, and DMARC records on your sending domain. The exact record values are shown in the Builder during domain setup. Skipping these tanks deliverability — your messages will land in spam.
    </Warning>
  </NumberedStep>

  <NumberedStep title="Create a template in the Builder">
    Templates live in the Builder under **Emails → New template**. A template has:

    <ArrowList>
      <ArrowItem>An `id` you reference in the capability `args` — for example `lead-thankyou`.</ArrowItem>
      <ArrowItem>A subject line, which can include variables: `Thanks for reaching out, {{name}}`.</ArrowItem>
      <ArrowItem>A body — HTML or plain text — with Handlebars-style `{{variable}}` interpolation.</ArrowItem>
    </ArrowList>

    Example body:

    ```text theme={null}
    Hi {{name}},

    Thanks for getting in touch. We got your message and will reply to {{email}} within one business day.

    — The Acme team
    ```

    Template management via the public API is not yet exposed. Create and edit templates through the Builder UI.
  </NumberedStep>

  <NumberedStep title="Bind the Form to sendEmail">
    Patch your page so the `Form` block's `on_submit` calls `sendEmail`. The runtime fires this every time the form submits and validates.

    ```ts theme={null}
    import { Gavai } from "@gavai/sdk";

    const gv = new Gavai({ apiKey: process.env.GAVAI_API_KEY });

    await gv.pages.patch({
      workspace: "acme",
      id: "page_01H8X4N5P6Q7R8S9T0U1V2W3X4",
      operations: [
        {
          op: "replace",
          path: "/tree/children/1/props/on_submit",
          value: {
            capability: "sendEmail",
            args: {
              template: "lead-thankyou",
              to: "{{form.email}}",
            },
          },
        },
      ],
    });
    ```

    `{{form.email}}` is a runtime interpolation that resolves to the value the user typed into the `email` field on submit. The path `/tree/children/1` assumes the `Form` block is the second child of the tree root — adjust to match your actual page.
  </NumberedStep>

  <NumberedStep title="Pass form values as template variables">
    To populate `{{name}}` and `{{email}}` in the template, add a `variables` map. Each key maps to the matching placeholder in the template subject and body.

    ```ts theme={null}
    {
      capability: "sendEmail",
      args: {
        template: "lead-thankyou",
        to: "{{form.email}}",
        variables: {
          name: "{{form.name}}",
          email: "{{form.email}}",
          message: "{{form.message}}",
        },
      },
    }
    ```

    The `{{form.<field>}}` expression resolves at runtime against the submitted form. Field names must match the `fields` array on the `Form` block's props. Variables not provided in `variables` are left as-is in the rendered template — standard Handlebars behavior.
  </NumberedStep>

  <NumberedStep title="Test the delivery">
    Two ways to test:

    <ArrowList>
      <ArrowItem>**Builder preview.** Open the template in the Builder and click **Send test email**. Enter a recipient; the send uses placeholder values for every variable.</ArrowItem>
      <ArrowItem>**Live submission.** Publish the page and submit the form with a real address you control.</ArrowItem>
    </ArrowList>

    ```ts theme={null}
    await gv.pages.publish({ workspace: "acme" });
    console.log("Published. Submit the form to trigger sendEmail.");
    ```

    Expected: the email arrives within a few seconds of submission. If it does not, check **Workspace → Logs** in the Builder — `sendEmail` errors surface there with codes such as `template_not_found` or `domain_not_verified`.
  </NumberedStep>
</NumberedSteps>

## Troubleshooting

<ErrorTable
  errors={[
{
  code: "domain_not_verified",
  cause: "The sender domain has not completed verification.",
  fix: "Finish the flow under Workspace → Settings → Email; wait for the green checkmark.",
},
{
  code: "template_not_found",
  cause: "The template arg does not match any template id in the workspace.",
  fix: "Confirm the id in the Builder under Emails → Templates.",
},
{
  code: "Variables render as literal placeholders",
  cause: "A key in variables does not match the placeholder name in the template.",
  fix: "Names are case-sensitive — match the template variable name exactly.",
},
{
  code: "Email never arrives (no error logged)",
  cause: "The message reached the gateway but the receiving inbox routed it to spam.",
  fix: "Confirm SPF, DKIM, and DMARC are set; check the recipient's spam folder.",
},
]}
/>

## What you built

You verified a sending domain, authored a Handlebars template in the Builder, and wired a `Form` block's `on_submit` to `sendEmail` — passing the user's submitted fields as template variables. Leads now receive a personalised thank-you email the moment they submit, with no SMTP server to run.

## Next steps

<Columns cols={2}>
  <BrandCard title="sendEmail reference" href="/capabilities/send-email" cta="Read reference">
    Full input/output schema and error codes for the sendEmail capability.
  </BrandCard>

  <BrandCard title="Form block reference" href="/pageblocks/blocks/form" cta="Read reference">
    Props, field types, and event bindings available on the Form block.
  </BrandCard>

  <BrandCard title="Handle webhooks" href="/guides/handle-webhooks" cta="Read guide">
    React to capability outcomes server-side via gavAI webhook events.
  </BrandCard>
</Columns>
