Gmail
Send, search, read, draft and label mail as a connected Google account.
The Gmail app reads, sends, drafts and labels mail as a connected Google account.
Gmail may not be in your palette, and that is not a bug.
Gmail needs a Google permission that Google classifies as restricted, which an app may only ask
for after security review. Elizon’s own review is not finished yet. Until it completes, Gmail is
off on Elizon's hosted service. A self-hosted ELIZON using its own Google OAuth client can
turn Gmail on by adding google-gmail to WORKFLOW_APPS_ENABLED and restarting. On Elizon’s
hosted service it will appear once the review completes.
Email content is untrusted. Read this before you connect an Agent to it.
Anyone who can email this account can put text in front of your workflow, and they can write that
text to be read by a language model. Treat a message's subject, bodyText, bodyHtml and
every attachment as something a stranger wrote with your Agent's instructions in mind — the
same warning the Email trigger carries, for the same
reason.
In practice: do not give an Agent that reads mail a tool that sends, pays, deletes, publishes or grants access without a Human Review node between the Agent's decision and the action. An Agent with a read-only tool that gets talked into the wrong answer produces a wrong answer; the same Agent with a Send an email step produces an email sent in your company's name.
| Operation | What it does |
|---|---|
| Message · Send an email | Sends as this account, with attachments from earlier steps. Cannot be undone. |
| Message · Search messages | Gmail's own search syntax. Answers ids — see below. |
| Message · Get a message | One message: who, what, its text, what is attached. Can store the attachments. |
| Message · Change a message's labels | Adds and removes labels, including marking read or unread and archiving. |
| Draft · Create a draft | Puts the email in Drafts instead of sending it, for a person to read over and send. |
| Label · List labels | Every label with the ids the other operations need. Run it once and keep the ids. |
Each operation asks for its own permission
Gmail is the one app where the permissions differ per operation, and that is deliberate: a workflow that only sends the weekly digest has no business reading the mailbox. Grant the account only what your workflow uses, under Settings → Connections:
| The step | Needs |
|---|---|
| Send an email | Gmail (send) |
| Search / Get a message | Gmail (read-only) |
| Change a message's labels | Gmail (read and change) |
| Create a draft | Gmail (drafts) |
| List labels | Gmail (labels) |
A step whose permission the account has not granted says which one before the run, by name.
Elizon never asks for https://mail.google.com/, the permission that grants everything at once.
Sending
To, Cc and Bcc take one address per line, or comma-separated. Fill in Message (plain text), Message (HTML), or both — a message with both lets each reader's mail app pick the one it prefers.
The email is always sent as the connected account. There is no "From" field: Gmail refuses to send as an address the account has not verified as an alias, so a field for it would be a field that mostly fails.
Attachments takes files from earlier steps — {{ upstream.<nodeId>.output.file }} — and takes
several at once. Anything that produces a file works: a Convert to File, a Drive download,
an HTTP Request set to download, a form upload, an email attachment you just read. The whole
message, attachments included, must stay under 35 MB, which is what Gmail accepts; a step that
would exceed it is refused before anything is sent. For anything larger, upload to Drive and share
a link.
Reply inside conversation id puts the message in an existing thread. Gmail requires the subject to match that conversation's and refuses the send otherwise, so carry the subject through from the message you are replying to.
Search answers ids, on purpose
Gmail's search returns id and threadId per match and nothing else — not the sender, not the
subject. That is Gmail's own answer, and Elizon does not quietly turn one search into one request
per result behind your back.
So read them the way the canvas already reads a list:
- A Gmail step called
find, Message · Search messages, Search set tois:unread has:attachment from:acme.example. - A Loop over
{{ upstream.find.output.result }}. - Inside the loop, a Gmail step, Message · Get a message, Message id set to
{{ loop.item.id }}.
Search takes exactly what you would type in Gmail's own search box — label:invoices,
newer_than:7d, subject:"service report", has:attachment, is:unread. It is required: an
empty search would match every message in the mailbox, so a Search that came out empty is
refused rather than answered.
Getting a message, and its attachments
Get a message flattens what Gmail sends — the headers, the plain-text body, the HTML body — into one object, so there is no MIME tree to walk in an expression:
{{ upstream.<nodeId>.output.result.subject }}
{{ upstream.<nodeId>.output.result.from }}
{{ upstream.<nodeId>.output.result.bodyText }}
{{ upstream.<nodeId>.output.result.attachments }}Attachments has two settings. Just list them (the default) tells you what is attached —
filename, contentType, sizeBytes — and costs nothing extra. Store them as files fetches
each one and gives you a real workflow file:
{{ upstream.<nodeId>.output.result.attachments.0.file }}which anything that takes a file takes: an AI Prompt attachment, a Drive upload, an FTP upload, another email. They are kept for 30 days and deleted with the run, like every other file a workflow makes. At most ten attachments are stored in one step; a message with more is refused rather than silently giving you the first ten, so leave the setting on Just list them and fetch the ones you want.
Labels, and "mark as read"
Gmail has no read flag. A message is unread because it carries the UNREAD label, which is why
Change a message's labels is also where you mark one read — set Read or unread and Elizon
adds or removes UNREAD for you. Archiving is the same idea: remove INBOX.
Labels are named by id, not by the name you see in Gmail. Gmail's own are INBOX, UNREAD,
STARRED, IMPORTANT, SPAM, TRASH and the CATEGORY_* ones; yours look like Label_12. Run
Label · List labels once and keep the ids you need. A label name typed where an id belongs is
refused by name, because Gmail's own answer ("Invalid label: Invoices") does not mention that the
two are different things.
SENT and DRAFT cannot be set: Gmail applies both itself. A change that names nothing at all is
refused too — Gmail accepts it and answers OK, so the step would otherwise go green having done
nothing.
A worked example
Triage an invoice and file it.
- A Gmail step called
find, Message · Search messages, Search set tois:unread subject:invoice has:attachment. - A Loop over
{{ upstream.find.output.result }}. - Inside: a Gmail step called
read, Message · Get a message, Message id set to{{ loop.item.id }}and Attachments set to Store them as files. - An AI Prompt step with an Attachment row set to
{{ upstream.read.output.result.attachments.0.file }}, extracting the supplier, the amount and the due date. - A Human Review node — the Agent read text a stranger wrote, and the next step writes to your register (see the warning above).
- A Data Table upsert of the approved record.
- A Gmail step, Message · Change a message's labels, Message id set to
{{ upstream.read.output.result.id }}, Read or unread set to Mark as read, Add labels set to the id of yourInvoiceslabel and Remove labels set toINBOX.
Step 7 is what makes the search in step 1 drain: tomorrow's run finds tomorrow's invoices.
Operations
The App step's own settings — account, resource, operation, and { value, valueMode } per input — are described in the Apps overview. These are this app's operations.
| Operation | What it does | Inputs (bold = required) | Permission |
|---|---|---|---|
message.send | Send an email. Cannot be undone. | to, cc, bcc, replyTo, subject, bodyText, bodyHtml, attachments, threadId | gmail.send |
message.search | Search messages | query, includeSpamTrash, maxResults | gmail.readonly |
message.get | Get a message | messageId, attachments | gmail.readonly |
message.modify | Change a message’s labels | messageId, addLabels, removeLabels, readState | gmail.modify |
draft.create | Create a draft | to, cc, bcc, replyTo, subject, bodyText, bodyHtml, attachments, threadId | gmail.compose |
label.list | List labels | — | gmail.labels |
Example
name: Find invoices
description: Finds unread invoice emails.
graph:
nodes:
- id: start
type: trigger_manual
position: { x: 0, y: 0 }
data: { description: 'Run it from Studio.' }
- id: find-invoices
type: app
position: { x: 280, y: 0 }
data:
app: google-gmail
version: 1.0.0
resource: message
operation: search
connection: google-account
inputs:
query: { value: 'is:unread subject:invoice has:attachment', valueMode: fixed }
maxResults: { value: '20', valueMode: fixed }
edges:
- { id: start-to-find-invoices, source: start, target: find-invoices }In a Package
The step travels whole; the account becomes a credential Slot the installer binds one of their own Google accounts to. See Apps in a Package.
slots:
- id: google-account
type: credential
scheme: oauth2
app: google
compliance:
providers: [google]- Travels:
app,version,resource,operation,inputs - Converted:
connectionId→connection(thecredentialSlot) - Never carried: none