GenieOS · Email for developers

Email is a template and a call.

The marketing team designs the artefact; your agent fires it by ID with variables. No brief yet? Compose one, render the HTML back, test it on yourself and send. The design never lives in your payload, which is why the payload stays three lines long.

compose.tsts
// no template yet? author one from a briefconst draft = await compose_template({  brief: "Launch email: your health data finally has a home",  campaign: "the-directory",}); // HTML back, for your own preview pipelineconst { html } = await render_template({ template: draft.id }); // to yourself, before anyone elseawait send_test({ template: draft.id, to: "[email protected]" });
The surface
  • read_template_schemathe shape before you compose against it
  • compose_templateauthor a template from a brief
  • create_templateor from your own structure, block by block
  • render_templateHTML back, for your own preview
  • send_testto yourself, before anyone else
  • send_transactionalone recipient, now
  • list_sends · read_sendwhat went, to whom, and what happened
  • list_sequences · enroll_in_sequence · read_sequence_run · cancel_sequence_runlifecycle, driven by your own events
  • propose_template_changeyour agent suggests, the marketer acknowledges before it ships
IN-APP · JUDGEThe agent drafted it. You look at it.
V1Your health data finally has a home launch
✓ INSERTED

NEW RELEASE

Your health data finally has a home.

✕ DeselectDeleteAdd blockAdd image

BLOCK

Image

SOURCE URL *

https://storage.googleapis.com/mail-geni…

ALT TEXT

Fueld

Shown when the image fails to load.

LINK URL

https://…

W

140

H

auto
px ⌄

BORDER STYLE

NoneSolidDashedDotted

I've written your email from the campaign brief — subject, copy and an on-brand hero image. Tell me what to tweak and I'll refine it.

⏎ to send

A rendered email beats its JSON.

render_template hands your agent the HTML, and that is enough for a preview pipeline or a snapshot test. It is not enough to know whether the email is any good. Hierarchy, weight, the first line against the imagery: those are judgements, and no return type carries them.

So the editor sits one click from every send. Open what the agent composed and it looks the way an inbox will show it, on brand by construction because it was assembled from the same memory. If a line needs a nudge, nudge it there and run send_test again.

propose.tsts
// the one call on this surface that cannot ship aloneawait propose_template_change({  template: "trial-ending",  change: "Lead with the logging streak they would lose, not the price",  evidence: "42% of expiring trials hold a 7-day streak",}); // it lands in the marketer's queue.// it ships when they acknowledge it, and not before.

The call that cannot ship on its own.

propose_template_change is the deliberate odd one out. Every other tool on this page does what it says immediately. This one files a suggestion against a template the marketing team owns, reasoning attached, and then it stops. The change ships when a marketer acknowledges it, and not a moment before.

That is a design decision, not a missing scope. An agent that can silently rewrite the welcome email is an agent nobody lets near the welcome email. Because the boundary is built into the tool itself, you can give your agents real authority over sends and sequences while the templates stay owned by the people accountable for them.

IN-APP · HAND OVERThe library the marketing team owns. Your agent targets it by ID.
AssetsFueld

ASSETS · LIBRARY

Workspace asset library

Shared with your whole team. Brand-approved assets are marked official. Drop or paste files anywhere · drag tiles between folders · filter by tag.

GenerateStock photoIcon kitCloud storageWorkspacePersonalUpload
FOLDERLanding page imagerySearch names, tags, descriptions...Newest first
Type:VectorsPhotosAI generatedStockCloud storage

The library is theirs. The ID is yours.

Templates live in a library split into a marketing side and a transactional side, and the transactional shelf comes stocked: trial ending, payment failed, subscription cancelled, sign in from new device. Your agent targets any of them by ID, so the send in your codebase never contains a line of design.

In the app, every template carries an API details button sitting right beside Send and Edit. That button is the whole two-door argument shipped as product: the marketer designs and owns the artefact, and the exact call your agent needs is printed on it.

lifecycle.tsts
// your event, your predicate, one enrolmentconst run = await enroll_in_sequence({  sequence: "activation-nudge",  contact: "[email protected]",  reason: "users.activated_at still null after 7 days",}); // control stays live mid-flightawait read_sequence_run({ run: run.id }); // and out again, the moment your data says stopawait cancel_sequence_run({ run: run.id });

Lifecycle, driven by your own events.

Sequences are drafted in the app by talking: describe the change and the draft updates, no dragging. What your side owns is the enrolment. Fire enroll_in_sequence on your own predicate, a null activated_at after seven days, a trial with three days left, and the sequence takes it from there.

Control stays live mid-flight. read_sequence_run tells you where a contact is; cancel_sequence_run pulls them out the moment your data says the nudge is no longer needed. The sequence reacts to reality because reality is what enrolled it.

When this happens
  • WhenYou tag a release v*

    ThenA changelog email to everyone still on the previous version

    • compose_template
    • enroll_in_sequence
  • WhenA feature flag reaches 100%

    ThenAn announcement to the cohort that did not have it

    • create_list
    • compose_template
  • WhenA Sentry issue crosses your user threshold

    ThenA transactional email to exactly the affected users, status and ETA

    • create_list
    • send_transactional

Your product already knows when to send.