Elizon Docs
Workflows

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.

OperationWhat it does
Message · Send an emailSends as this account, with attachments from earlier steps. Cannot be undone.
Message · Search messagesGmail's own search syntax. Answers ids — see below.
Message · Get a messageOne message: who, what, its text, what is attached. Can store the attachments.
Message · Change a message's labelsAdds and removes labels, including marking read or unread and archiving.
Draft · Create a draftPuts the email in Drafts instead of sending it, for a person to read over and send.
Label · List labelsEvery 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 stepNeeds
Send an emailGmail (send)
Search / Get a messageGmail (read-only)
Change a message's labelsGmail (read and change)
Create a draftGmail (drafts)
List labelsGmail (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:

  1. A Gmail step called find, Message · Search messages, Search set to is:unread has:attachment from:acme.example.
  2. A Loop over {{ upstream.find.output.result }}.
  3. 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.

  1. A Gmail step called find, Message · Search messages, Search set to is:unread subject:invoice has:attachment.
  2. A Loop over {{ upstream.find.output.result }}.
  3. 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.
  4. 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.
  5. A Human Review node — the Agent read text a stranger wrote, and the next step writes to your register (see the warning above).
  6. A Data Table upsert of the approved record.
  7. 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 your Invoices label and Remove labels set to INBOX.

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.

OperationWhat it doesInputs (bold = required)Permission
message.sendSend an email. Cannot be undone.to, cc, bcc, replyTo, subject, bodyText, bodyHtml, attachments, threadIdgmail.send
message.searchSearch messagesquery, includeSpamTrash, maxResultsgmail.readonly
message.getGet a messagemessageId, attachmentsgmail.readonly
message.modifyChange a message’s labelsmessageId, addLabels, removeLabels, readStategmail.modify
draft.createCreate a draftto, cc, bcc, replyTo, subject, bodyText, bodyHtml, attachments, threadIdgmail.compose
label.listList labels—gmail.labels

Example

workflows/find-invoices.yaml
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.

elizon.yaml
slots:
  - id: google-account
    type: credential
    scheme: oauth2
    app: google
compliance:
  providers: [google]
  • Travels: app, version, resource, operation, inputs
  • Converted: connectionId → connection (the credential Slot)
  • Never carried: none