SendGrid Email ExecModule
Overview
SendGridEmailModule submits one email through Twilio SendGrid's Mail Send v3 endpoint. It supports plain-text and HTML bodies, dynamic templates, to/cc/bcc recipients, reply-to addressing, and bounded base64 attachments.
Version 2.0 replaces the catalog's empty input/output metadata and weak delivery boundary. The module now requires a READY SendGrid IntegrationAccount, an explicit verified sender, bounded and unique recipients, strict content/template rules, safe attachment basenames and MIME types, a 10 MB serialized request ceiling, a 10-second connection timeout, and a 30-second read timeout. A send is accepted only when SendGrid returns HTTP 202 and a valid X-Message-Id header.
Email submission is an external, non-idempotent side effect. The module never retries an HTTP, timeout, or ambiguous network failure automatically because SendGrid may have accepted the email before the workflow observed the failure.
Usage
- Create a restricted SendGrid API key with Mail Send permission.
- Verify the sender identity or authenticated domain used by
fromin SendGrid. - Store the API key only in the encrypted
apiKeyfield of a ValkyrAIIntegrationAccount, set the account toREADY, and bind it throughExecModuleConfig.authConfig.integrationAccount. - Add
SendGridEmailModuleto a workflow and supply at least onetoaddress plus the verifiedfromaddress. - For a content send, provide
subjectand at least one ofplain_textorhtml. - For a dynamic-template send, provide
template_idand optionaldynamic_data; do not also provideplain_textorhtml. - Require outbound approval where policy demands it and retain the returned message ID for provider reconciliation.
Inputs
| Name | Type | Required | Description | Local constraints |
|---|---|---|---|---|
to | array of strings | Yes | Primary recipients. | Non-empty valid email array. |
cc | array of strings | No | Carbon-copy recipients. | Valid email array when present. |
bcc | array of strings | No | Blind-carbon-copy recipients. | Valid email array when present. |
from | string | Yes | Verified sender email. | Valid email, at most 254 characters. |
reply_to | string | No | Reply-to email. | Valid email, at most 254 characters. |
subject | string | Conditional | Subject for content sends. | Required without template_id; at most 998 Unicode code points. |
template_id | string | No | Dynamic template ID. | d- followed by exactly 32 hexadecimal characters. |
dynamic_data | object | No | Dynamic-template variables. | Requires template_id; at most 100,000 serialized UTF-8 bytes. |
plain_text | string | Conditional | Plain-text content. | At least one content body is required without template_id; at most 1,000,000 code points. |
html | string | Conditional | HTML content. | At least one content body is required without template_id; at most 1,000,000 code points. |
attachments | array of objects | No | Base64 file attachments. | At most five files, each at most 5 MB decoded, with a safe basename and optional bounded MIME type. |
The combined to, cc, and bcc count is limited to 100. Addresses must be unique across all three fields using case-insensitive comparison. The complete serialized provider request is limited to 10 MB. mock is rejected and cannot fabricate delivery.
Each attachment has this shape:
{
"filename": "receipt.pdf",
"content_base64": "JVBERi0xLjQK...",
"type": "application/pdf"
}
filename must be a basename without /, \, or control characters. type defaults to application/octet-stream.
Outputs
| Name | Type | When present | Description |
|---|---|---|---|
sendgrid.message.id | string | Success | Safe X-Message-Id returned with SendGrid's 202 response. |
sendgrid.message.recipientCount | integer | Success | Total unique to, cc, and bcc recipients submitted. |
sendgrid.message.status | string | Always | ACCEPTED after verified provider acceptance or ERROR on failure. |
ACCEPTED means SendGrid accepted the request for processing; it does not prove final delivery. The runtime ExecModule is also marked GOOD or ERROR. Recipient addresses, message bodies, attachment content, API keys, and provider response bodies are not written to error messages or WorkflowState.
IntegrationAccount Requirements
| Setting | Requirement |
|---|---|
| Provider | Twilio SendGrid Mail Send API v3 |
| Authentication | Restricted API key with Mail Send permission |
| Status | READY |
accountName | Human-readable SendGrid account or sending domain identity |
apiKey | Encrypted API key beginning with SG. |
| Relationship | Bind through ExecModuleConfig.authConfig.integrationAccount |
Never place the API key in payload fields, headers stored by a workflow, URLs, logs, examples, or documentation. Sender verification, authenticated-domain policy, unsubscribe requirements, suppression lists, and account-level compliance remain provider and tenant responsibilities.
Configuration
Illustrative normalized configuration:
{
"version": "2.0.0",
"authConfig": {
"authStrategy": 1,
"integrationAccount": "integration-account:sendgrid-transactional"
},
"payloadConfig": {
"parameters": "{\"to\":[\"customer@example.com\"],\"from\":\"billing@example.com\",\"subject\":\"Order A-1042\",\"plain_text\":\"Your order is ready.\"}"
}
}
The account value is a symbolic secure reference. Persisted workflows use the generated IntegrationAccount relationship, never a plaintext API key.
Operations
SendGridEmailModule performs one POST /v3/mail/send operation with two supported shapes:
| Shape | Required provider fields | Side effect |
|---|---|---|
| Content email | Recipients, verified from, subject, and plain_text, html, or both | Submits one email with explicit body content. |
| Dynamic-template email | Recipients, verified from, template_id, and optional dynamic_data | Submits one email rendered by a SendGrid dynamic template. |
One execution creates one SendGrid personalization containing all configured recipients. Bulk campaign orchestration, contact lists, marketing campaigns, template management, sender verification, delivery-event ingestion, suppression management, scheduled delivery, and provider-side cancellation are separate capabilities and are not implemented here.
Errors and Failure Modes
| Failure | Cause | Retry guidance |
|---|---|---|
| Validation failure | Missing or malformed recipients/sender, duplicate addresses, missing content, conflicting template content, invalid template ID, unsafe attachment, or size overflow. | Correct the configuration; no SendGrid request was sent. |
| IntegrationAccount failure | No bound account, non-READY status, blank key, or key without the SG. prefix. | Bind or repair the SendGrid account; no request was sent. |
| HTTP 400 | SendGrid rejects recipient, sender, content, template, or attachment data. | Correct the request and verify sender/template state before a new attempt. |
| HTTP 401/403 | Invalid key, missing Mail Send permission, or account restriction. | Rotate or reauthorize the IntegrationAccount. |
| HTTP 429 | Provider rate limit. | Reconcile provider activity, honor rate-limit guidance, and schedule a deliberate new attempt. |
| HTTP 5xx or network failure | Provider or transport failure with an ambiguous acceptance boundary. | Do not retry blindly; inspect SendGrid Email Activity first. |
| Invalid success response | Status is not 202, or X-Message-Id is missing, malformed, or contains unsafe characters. | Treat as failure and inspect provider compatibility; success is not recorded. |
Provider response bodies can contain recipient data or credential-like details. HTTP failures therefore expose only the status code and non-retry warning. The original provider exception is not retained as the emitted error cause.
Example
Submit one dynamic-template order confirmation:
{
"to": ["customer@example.com"],
"from": "billing@example.com",
"reply_to": "support@example.com",
"template_id": "d-0123456789abcdef0123456789abcdef",
"dynamic_data": {
"orderNumber": "A-1042",
"status": "ready"
}
}
Expected result after SendGrid returns a verified acceptance response:
{
"sendgrid.message.id": "provider-message-id",
"sendgrid.message.recipientCount": 1,
"sendgrid.message.status": "ACCEPTED"
}
Notes
- Pagination: not applicable; one execution submits one Mail Send request.
- Limits: local limits are intentionally stricter and deterministic. SendGrid applies final account, plan, recipient, attachment, message-size, template, and content rules.
- Rate limits: HTTP
429is surfaced without retry. The module does not persist rate-limit headers. - Idempotency: Mail Send is non-idempotent. A workflow retry can produce duplicate email, and WorkflowState is not treated as durable provider deduplication across ambiguous failures.
- Delivery semantics: HTTP
202plusX-Message-Idproves provider acceptance only. Final delivery, bounce, block, deferral, spam-report, unsubscribe, and open/click events require SendGrid Event Webhook or Email Activity reconciliation. - Attachments: content must already be base64 encoded. The module validates base64 and local decoded/file/payload limits but does not fetch remote files.
- API constraints: dynamic-template sends reject
plain_textandhtmlso content is not silently ignored.dynamic_datais rejected withouttemplate_id. - Destructive behavior: submission is an external outbound write. This module cannot recall or compensate an accepted email.
- Observability: EventLogs record validation, recipient count, verified acceptance ID, or sanitized failure. Addresses, content, attachment bytes, API keys, and provider bodies are excluded from failures.
- Unverified boundary: deterministic tests cover IntegrationAccount binding, exact request construction, recipient/content/attachment limits, template mapping, provider-response verification, non-retry behavior, metadata serialization, and secret redaction. Live SendGrid execution requires separately authorized credentials and is not exercised in repository tests.
See Twilio SendGrid's official Mail Send API reference and Dynamic Templates documentation for current provider behavior.