# Email Workflows — Triggers, Dedup & the Composed Email Step

> Loaded on demand from the Agentled skill. Read this for any workflow that
> reads or sends email. Pairs with the `08-composed-email-approval` pattern
> (`agentled examples 08-composed-email-approval`).

## Trigger choice: polling vs event

**Default to Schedule trigger + label-based dedup** for all email intake workflows (deal flow, triage, review, digest). Only propose an App Event trigger when the user explicitly needs sub-minute latency.

| User asks for | Trigger |
|---------------|---------|
| "process inbound emails", "triage daily", "review pitches" | **Schedule** (polling) |
| "as soon as", "real-time", "within X seconds/minutes" | **App event** |

## Canonical email polling pattern

```
schedule trigger → GMAIL_FETCH_EMAILS (-label:processed newer_than:1d) → loop: [process] → GMAIL_ADD_LABEL (mark processed) → milestone
```

Step order:
1. **`GMAIL_CREATE_LABEL`** — create/get the `processed` label (idempotent, returns label ID)
2. **`GMAIL_FETCH_EMAILS`** — query `-label:processed newer_than:1d` (or wider window as needed)
3. **Loop** — process each email (AI analysis, KG storage, enrichment, etc.)
4. **`GMAIL_ADD_LABEL`** — apply `{{steps.ensure-label.id}}` to mark email done (dedup gate)

## Label ID rule (prevents `400: Invalid label`)

Gmail requires **label IDs** (e.g., `Label_3456789012345`), not display names (e.g., `"processed"` or `"agentled"`).

**Always resolve via `GMAIL_CREATE_LABEL`** and reference its returned `id`:
```json
{ "stepInputData": { "label_id": "{{steps.ensure-label.id}}" } }
```
Never pass a string label name directly to `GMAIL_ADD_LABEL`.

See `docs/workflows/triggers.md` for the full decision framework, query examples, and common mistakes.

---

## Email Step Pattern (AI Draft → Approve → Send)

Email steps use a single `aiAction` step (never separate "draft" + "gmail send" appAction steps). The AI drafts the email, a human approves, then the platform sends it.

### 1. Outreach Profile Input Page

When a workflow sends emails, add an outreach profile input page to `context.inputPages` so the user can configure sender identity:

```json
{
  "title": "Outreach Profile",
  "pathname": "outreach-profile",
  "configuration": {
    "contextKey": "outreachProfile",
    "shortDescriptionFields": ["name", "fromEmail"],
    "fields": [
      { "name": "name", "label": "Sender Name", "type": "text", "required": true },
      { "name": "fromEmailLabel", "label": "From Name", "type": "text", "required": true },
      { "name": "fromEmail", "label": "From Email", "type": "connected_emails_selector_multiple", "required": true },
      { "name": "replyToEmail", "label": "Reply-To Email (optional)", "type": "text" }
    ]
  }
}
```

### 2. Composed Email Step

```json
{
  "id": "send_email",
  "type": "aiAction",
  "name": "Send Email",
  "pipelineStepPrompt": {
    "type": "email",
    "template": "Draft a personalized email...\n{{steps.previous_step.data}}\nFormat the body as email-safe HTML with one <p> per visual paragraph.\nReturn JSON ONLY per schema.",
    "responseStructure": {
      "email": {
        "from": "{{context.outreachProfile.fromEmail}}",
        "to": "recipient@example.com",
        "subject": "Email subject line",
        "body": "Email body (email-safe HTML)",
        "bodyType": "html"
      }
    },
    "responseType": "json"
  },
  "renderer": {
    "type": "Email",
    "config": { "fromContextKey": "outreachProfile" }
  },
  "onApproval": {
    "action": "schedule-email",
    "executedText": "Email sent by {{name}} at {{date}}",
    "scheduledText": "Email scheduled to be sent for {{date}} by {{name}}",
    "failedText": "Email failed to send."
  },
  "integrations": [{
    "type": "oneOf",
    "label": "Email",
    "connectorType": "email",
    "options": [
      { "name": "Gmail", "url": "https://gmail.com", "isUserAccountConnectionRequired": true },
      { "name": "Outlook", "url": "https://outlook.com", "isUserAccountConnectionRequired": true }
    ],
    "selectionHint": "preferConnected"
  }],
  "creditCost": 10,
  "next": { "conditions": { "approvalRequired": true } }
}
```

### Key Requirements

- **Always** include `outreachProfile` input page when using email
- `pipelineStepPrompt.type: "email"` — tells the system this is an email step
- `renderer.config.fromContextKey: "outreachProfile"` — links renderer to sender profile
- `onApproval.action: "schedule-email"` — triggers the actual send; without it, approval does nothing
- `next.conditions.approvalRequired: true` — blocks the pipeline until human approval
- Email body must be email-safe HTML (`<p>`, `<br>`, `<a>`, `<strong>` — no CSS, no scripts)
- Preserve readable email structure in `email.body`: use separate `<p>` blocks for the greeting, each paragraph, links/CTA, and sign-off; do not return the whole email as one plain-text paragraph, and do not rely on newline-only formatting because email clients collapse it.
- **Never** use separate "draft" + "gmail send" appAction steps for outreach
- Set an output page `displayConfig.executionNameTemplate` that names the entity and the outreach state. For single-entity outreach, use `{{entityName}} - Email Drafted` before approval and `{{entityName}} - Email Sent` or `{{entityName}} - Contacted` after the post-send status step, e.g. `Joe Dan - VC Firm Name - Email Drafted`; add `{{today}}` only when a short date helps. For batch runs, summarize counts instead, e.g. `26 May - 4 sourced - 3 contacted`.
