fix: route email trigger path through standard info channel (#8996)

* docs(skill): document email triggers and S3 attachments

Add an "Email triggers" section to the triggers skill covering the
local-part config, the parsed_email/raw_email/email_extra_args payload,
the URL-style extras convention, where to find trigger_path (only with
a preprocessor, at event.trigger_path), and — most importantly — that
binary attachments are uploaded to the workspace S3 bucket and surface
as `{ s3: "windmill_emails/<job_id>/attachments/<filename>" }`. Scripts
must use wmill.loadS3File / wmill.load_s3_file to read them.

Also pulls EmailTrigger into the schema mappings so a real
`email_trigger.schema.yaml` is generated, and adds Email/Azure to the
trigger kinds list in the CLI agent guidance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref for email trigger path fix

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 26184ab7a4aadfc529dcedf038aa08d36c7ad381

This commit updates the EE repository reference after PR #553 was merged in windmill-ee-private.

Previous ee-repo-ref: 318a46897a605dc9be3817901f35ba5a99a0a525

New ee-repo-ref: 26184ab7a4aadfc529dcedf038aa08d36c7ad381

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
hugocasa
2026-05-01 18:23:09 +02:00
committed by GitHub
parent 9cb777a6b6
commit 21411282bb
9 changed files with 267 additions and 4 deletions
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM email_trigger\n WHERE\n ((workspaced_local_part IS TRUE AND workspace_id || '-' || local_part = $1)\n OR (workspaced_local_part IS FALSE AND local_part = $1))\n AND ($2::TEXT IS NULL OR path != $2)\n )\n ",
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM email_trigger\n WHERE\n ((workspaced_local_part IS TRUE AND workspace_id || '-' || local_part = $1)\n OR (workspaced_local_part IS FALSE AND local_part = $1))\n AND ($2::TEXT IS NULL OR NOT (workspace_id = $3 AND path = $2))\n )\n ",
"describe": {
"columns": [
{
@@ -11,6 +11,7 @@
],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
@@ -19,5 +20,5 @@
null
]
},
"hash": "bb94e3105cb1bc5d90af2bc914b579a6e821c432eac38ad877d9ff362d8ab916"
"hash": "4d73cefcc1b72238b7731ab52376b849a6f712b4109a48d9f77da8dd0060ab1a"
}
+1 -1
View File
@@ -1 +1 @@
1790e8df54bb00fb6800b8f3a6a2bb4c9787380e
26184ab7a4aadfc529dcedf038aa08d36c7ad381
+1 -1
View File
@@ -37,7 +37,7 @@ When a new app needs to be created, YOU run \`wmill app new\` yourself with \`--
## Triggers
You MUST use the \`triggers\` skill to configure HTTP routes, WebSocket, Kafka, NATS, SQS, MQTT, GCP, or Postgres CDC triggers.
You MUST use the \`triggers\` skill to configure HTTP routes, WebSocket, Kafka, NATS, SQS, MQTT, GCP, Azure, Email, or Postgres CDC triggers.
## Schedules
+109
View File
@@ -5563,6 +5563,49 @@ Examples:
- \`u/user/webhook.http_trigger.yaml\`
- \`f/data/kafka_consumer.kafka_trigger.yaml\`
- \`f/sync/postgres_cdc.postgres_trigger.yaml\`
- \`f/inbound/orders.email_trigger.yaml\`
## Email Triggers
An email trigger routes incoming emails to a script or flow. Each trigger reserves a local-part: emails sent to \`<local_part>@<windmill_email_domain>\` are delivered to the configured runnable. Set \`workspaced_local_part: true\` to namespace it per workspace (the actual recipient becomes \`<workspace_id>-<local_part>@…\`); on Windmill Cloud this is required.
Senders may append URL-style extras to the local-part with \`+\`: \`mytrigger+foo=bar+baz=qux@…\`. They flow through to the script as \`email_extra_args\` (see below).
### Payload
The runnable receives:
- \`parsed_email\`\`{ headers, text_body, html_body, attachments[] }\`. Each \`attachment\` has \`{ headers, body }\`.
- \`raw_email\` — the raw RFC 822 message as a string, **or** an S3 object (\`{ s3: "windmill_emails/<job_id>/raw.eml" }\`) if the message exceeds 1 MiB.
- \`email_extra_args\` (optional, only when sender appended \`+key=value\` extras) — a flat object of the parsed extras.
With a preprocessor, all of the above are nested under \`event\` along with \`event.kind = "email"\` and \`event.trigger_path\` (the trigger's path). Without a preprocessor, \`trigger_path\` is **not** exposed — add a preprocessor if you need it.
### Attachments are S3 objects
Binary attachments are uploaded to the workspace S3 bucket and surface in \`parsed_email.attachments[i].body\` as:
\`\`\`json
{ "s3": "windmill_emails/<job_id>/attachments/<filename>" }
\`\`\`
To read the bytes inside a script, use the wmill SDK:
\`\`\`ts
// TypeScript
import * as wmill from "windmill-client"
const file = await wmill.loadS3File(parsed_email.attachments[0].body)
\`\`\`
\`\`\`python
# Python
import wmill
data = wmill.load_s3_file(parsed_email["attachments"][0]["body"])
\`\`\`
If the workspace has no S3 resource configured (Workspace Settings Object storage), \`body\` falls back to the string \`"configure s3 in the workspace settings to handle attachments"\`. The same applies to large \`raw_email\` bodies. Email attachment storage requires the server to be built with the \`parquet\` feature.
Text/HTML/inline parts are placed inline in \`body\` as strings.
## CLI Commands
@@ -7180,6 +7223,71 @@ required:
- azure_mode
- scope_resource_id
- subscription_name
`,
"email_trigger": `type: object
properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
labels:
type: array
items:
type: string
local_part:
type: string
workspaced_local_part:
type: boolean
error_handler_path:
type: string
error_handler_args:
type: object
description: The arguments to pass to the script or flow
retry:
type: object
properties:
constant:
type: object
description: Retry with constant delay between attempts
properties:
attempts:
type: integer
description: Number of retry attempts
seconds:
type: integer
description: Seconds to wait between retries
exponential:
type: object
description: Retry with exponential backoff (delay doubles each time)
properties:
attempts:
type: integer
description: Number of retry attempts
multiplier:
type: integer
description: Multiplier for exponential backoff
seconds:
type: integer
minimum: 1
description: Initial delay in seconds
random_factor:
type: integer
minimum: 0
maximum: 100
description: Random jitter percentage (0-100) to avoid thundering herd
retry_if:
$ref: '#/components/schemas/RetryIf'
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- local_part
`,
"gcp_trigger": `type: object
properties:
@@ -8116,6 +8224,7 @@ export const SCHEMA_MAPPINGS: Record<string, SchemaMapping[]> = {
{ name: "SqsTrigger", schemaKey: "sqs_trigger", filePattern: "*.sqs_trigger.yaml" },
{ name: "GcpTrigger", schemaKey: "gcp_trigger", filePattern: "*.gcp_trigger.yaml" },
{ name: "AzureTrigger", schemaKey: "azure_trigger", filePattern: "*.azure_trigger.yaml" },
{ name: "EmailTrigger", schemaKey: "email_trigger", filePattern: "*.email_trigger.yaml" },
],
"schedules": [
{ name: "Schedule", schemaKey: "schedule", filePattern: "*.schedule.yaml" },
@@ -0,0 +1,64 @@
type: object
properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
labels:
type: array
items:
type: string
local_part:
type: string
workspaced_local_part:
type: boolean
error_handler_path:
type: string
error_handler_args:
type: object
description: The arguments to pass to the script or flow
retry:
type: object
properties:
constant:
type: object
description: Retry with constant delay between attempts
properties:
attempts:
type: integer
description: Number of retry attempts
seconds:
type: integer
description: Seconds to wait between retries
exponential:
type: object
description: Retry with exponential backoff (delay doubles each time)
properties:
attempts:
type: integer
description: Number of retry attempts
multiplier:
type: integer
description: Multiplier for exponential backoff
seconds:
type: integer
minimum: 1
description: Initial delay in seconds
random_factor:
type: integer
minimum: 0
maximum: 100
description: Random jitter percentage (0-100) to avoid thundering herd
retry_if:
$ref: '#/components/schemas/RetryIf'
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- local_part
@@ -15,6 +15,49 @@ Examples:
- `u/user/webhook.http_trigger.yaml`
- `f/data/kafka_consumer.kafka_trigger.yaml`
- `f/sync/postgres_cdc.postgres_trigger.yaml`
- `f/inbound/orders.email_trigger.yaml`
## Email Triggers
An email trigger routes incoming emails to a script or flow. Each trigger reserves a local-part: emails sent to `<local_part>@<windmill_email_domain>` are delivered to the configured runnable. Set `workspaced_local_part: true` to namespace it per workspace (the actual recipient becomes `<workspace_id>-<local_part>@…`); on Windmill Cloud this is required.
Senders may append URL-style extras to the local-part with `+`: `mytrigger+foo=bar+baz=qux@…`. They flow through to the script as `email_extra_args` (see below).
### Payload
The runnable receives:
- `parsed_email``{ headers, text_body, html_body, attachments[] }`. Each `attachment` has `{ headers, body }`.
- `raw_email` — the raw RFC 822 message as a string, **or** an S3 object (`{ s3: "windmill_emails/<job_id>/raw.eml" }`) if the message exceeds 1 MiB.
- `email_extra_args` (optional, only when sender appended `+key=value` extras) — a flat object of the parsed extras.
With a preprocessor, all of the above are nested under `event` along with `event.kind = "email"` and `event.trigger_path` (the trigger's path). Without a preprocessor, `trigger_path` is **not** exposed — add a preprocessor if you need it.
### Attachments are S3 objects
Binary attachments are uploaded to the workspace S3 bucket and surface in `parsed_email.attachments[i].body` as:
```json
{ "s3": "windmill_emails/<job_id>/attachments/<filename>" }
```
To read the bytes inside a script, use the wmill SDK:
```ts
// TypeScript
import * as wmill from "windmill-client"
const file = await wmill.loadS3File(parsed_email.attachments[0].body)
```
```python
# Python
import wmill
data = wmill.load_s3_file(parsed_email["attachments"][0]["body"])
```
If the workspace has no S3 resource configured (Workspace Settings → Object storage), `body` falls back to the string `"configure s3 in the workspace settings to handle attachments"`. The same applies to large `raw_email` bodies. Email attachment storage requires the server to be built with the `parquet` feature.
Text/HTML/inline parts are placed inline in `body` as strings.
## CLI Commands
+43
View File
@@ -10,6 +10,49 @@ Examples:
- `u/user/webhook.http_trigger.yaml`
- `f/data/kafka_consumer.kafka_trigger.yaml`
- `f/sync/postgres_cdc.postgres_trigger.yaml`
- `f/inbound/orders.email_trigger.yaml`
## Email Triggers
An email trigger routes incoming emails to a script or flow. Each trigger reserves a local-part: emails sent to `<local_part>@<windmill_email_domain>` are delivered to the configured runnable. Set `workspaced_local_part: true` to namespace it per workspace (the actual recipient becomes `<workspace_id>-<local_part>@…`); on Windmill Cloud this is required.
Senders may append URL-style extras to the local-part with `+`: `mytrigger+foo=bar+baz=qux@…`. They flow through to the script as `email_extra_args` (see below).
### Payload
The runnable receives:
- `parsed_email``{ headers, text_body, html_body, attachments[] }`. Each `attachment` has `{ headers, body }`.
- `raw_email` — the raw RFC 822 message as a string, **or** an S3 object (`{ s3: "windmill_emails/<job_id>/raw.eml" }`) if the message exceeds 1 MiB.
- `email_extra_args` (optional, only when sender appended `+key=value` extras) — a flat object of the parsed extras.
With a preprocessor, all of the above are nested under `event` along with `event.kind = "email"` and `event.trigger_path` (the trigger's path). Without a preprocessor, `trigger_path` is **not** exposed — add a preprocessor if you need it.
### Attachments are S3 objects
Binary attachments are uploaded to the workspace S3 bucket and surface in `parsed_email.attachments[i].body` as:
```json
{ "s3": "windmill_emails/<job_id>/attachments/<filename>" }
```
To read the bytes inside a script, use the wmill SDK:
```ts
// TypeScript
import * as wmill from "windmill-client"
const file = await wmill.loadS3File(parsed_email.attachments[0].body)
```
```python
# Python
import wmill
data = wmill.load_s3_file(parsed_email["attachments"][0]["body"])
```
If the workspace has no S3 resource configured (Workspace Settings → Object storage), `body` falls back to the string `"configure s3 in the workspace settings to handle attachments"`. The same applies to large `raw_email` bodies. Email attachment storage requires the server to be built with the `parquet` feature.
Text/HTML/inline parts are placed inline in `body` as strings.
## CLI Commands
+2
View File
@@ -1432,6 +1432,7 @@ SKILL_DEFINITIONS = [
('SqsTrigger', 'sqs_trigger'),
('GcpTrigger', 'gcp_trigger'),
('AzureTrigger', 'azure_trigger'),
('EmailTrigger', 'email_trigger'),
],
},
{
@@ -1893,6 +1894,7 @@ def main():
'SqsTrigger', 'NewSqsTrigger',
'GcpTrigger',
'AzureTrigger',
'EmailTrigger', 'NewEmailTrigger',
]
for schema_name in schema_names:
if schema_name in backend_schemas:
+1
View File
@@ -49,6 +49,7 @@ SCHEMA_MAPPINGS = {
('SqsTrigger', 'sqs_trigger'),
('GcpTrigger', 'gcp_trigger'),
('AzureTrigger', 'azure_trigger'),
('EmailTrigger', 'email_trigger'),
],
'schedules': [
('Schedule', 'schedule'),