Skip to content

Reply to emails with an LLM

This is the canonical trigger workflow, built end to end: someone emails an address, a model reads the message and the documents attached to it, and the result goes back to whoever sent it. Four nodes, no code, no input parameters.

Input (no parameters) → LLM "extract" → Tool "send_email" → Output
reads {{ trigger.* }} — the email itself
  1. New workflow. Call it something like Email → Extract → Reply. Drag on an Input node and leave its parameter list empty — this workflow takes nothing from the Run dialog.

  2. Add an LLM node (id extract). Pick a capable text model. Leave max tokens and temperature at their defaults. Set the system prompt to:

    You extract structured, factual information from business email. Never invent values.

    and the prompt to:

    You are an assistant that reads an inbound email (and its attachments) and extracts what matters.
    From: {{ trigger.from_name }} <{{ trigger.from }}>
    Subject: {{ trigger.subject }}
    Date: {{ trigger.date }}
    --- EMAIL BODY ---
    {{ trigger.text }}
    {% if trigger.attachments %}--- ATTACHMENTS ---
    {% for a in trigger.attachments %}### {{ a.filename }} ({{ a.content_type }}, {{ a.size }} bytes)
    {% if a.text %}{{ a.text }}{% else %}(no extractable text — image or binary){% endif %}
    {% endfor %}{% endif %}--- TASK ---
    Reply in Markdown with exactly these sections:
    1. **Summary** — 2–3 sentences on what this email is about.
    2. **Key facts** — bullets: people, organisations, amounts, dates, reference numbers, deadlines. Quote exact values.
    3. **Action items** — who needs to do what (or "None").
    4. **Attachments** — one line per attachment saying what it contains (or "None").
    Be concrete. If something is missing or unclear, say so. No greeting, no sign-off.

    The {% for %} loop is plain Jinja — every attachment’s extracted text lands in the prompt, so an emailed PDF invoice is read by the model without any parsing code.

  3. Add a Tool node (id reply) with server __builtin__ and tool send_email. Arguments:

    {
    "to": "{% if trigger.from != '[email protected]' %}{{ trigger.from }}{% endif %}",
    "subject": "Re: {{ trigger.subject }}",
    "body": "{{ extract.text }}\n\n---\n_Extracted automatically by Catalyst._",
    "format": "markdown"
    }

    to is the sender. The if is a small courtesy for the Send test button: its synthetic event comes from [email protected], and an empty to makes send_email fall back to your own address — so a test reply lands in your inbox instead of bouncing off a fake one.

  4. Add an Output node so the run records what happened:

    from → {{ trigger.from }}
    subject → {{ trigger.subject }}
    attachments → {{ trigger.attachments | length }}
    extracted → {{ extract.text }}
    email_result → {{ reply.result }}
  5. Wire Input → extract → reply → Output and Save.

  1. Click Triggers in the toolbar → New TriggerEmail.

  2. Optionally restrict Allowed senders (e.g. @yourcompany.com). Leave Extract text from attachments on. Create.

  3. Copy the address from the row — it looks like [email protected].

  • Send test on the trigger row fires a fake email through the real path. Within a few seconds you’ll see an accepted line under Deliveries, a run in History with an Email badge, and — because of the to fallback above — a reply in your own inbox.
  • Then do it for real: email the address from your mail client with a PDF attached. The reply comes back to the address you sent from, and the run’s trigger output shows the attachment with its extracted text.
  • Spreadsheets, properly. The model sees a capped text preview of an emailed .xlsx; for totals or joins add a Python node — the file is already at ./inputs/attachments/, so pd.read_excel(glob.glob('./inputs/attachments/*.xlsx')[0]) gives you real numbers.
  • Route instead of reply. Add a Condition on {{ trigger.subject }} or on something the LLM extracted, and send different mail for invoices vs. everything else.
  • Look at the picture. For emailed screenshots, put {{ trigger.attachments.0.url }} in a vision model’s Images field — the model sees the attachment instead of reading text.
  • Feed a parameterised workflow. If the workflow already has inputs, keep them and map {{ trigger.subject }} / {{ trigger.attachments.0.url }} onto them in the trigger’s Map from event box; the same workflow then runs from a schedule, the Run dialog, or mail.