@agentled/cli
Version:
CLI for Agentled — manage workflows, apps, and knowledge from the command line. Zero context-window cost for AI agents.
124 lines (102 loc) • 5.56 kB
Markdown
# 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`.