From 21411282bb4a0442046bf71fdc7ea012fa2c3d3d Mon Sep 17 00:00:00 2001 From: hugocasa Date: Fri, 1 May 2026 18:23:09 +0200 Subject: [PATCH] fix: route email trigger path through standard info channel (#8996) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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//attachments/" }`. 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) * chore: update ee-repo-ref for email trigger path fix Co-Authored-By: Claude Opus 4.7 (1M context) * 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) Co-authored-by: windmill-internal-app[bot] --- ...b849a6f712b4109a48d9f77da8dd0060ab1a.json} | 5 +- backend/ee-repo-ref.txt | 2 +- cli/src/guidance/core.ts | 2 +- cli/src/guidance/skills.gen.ts | 109 ++++++++++++++++++ .../schemas/email_trigger.schema.yaml | 64 ++++++++++ .../auto-generated/skills/triggers/SKILL.md | 43 +++++++ system_prompts/base/triggers.md | 43 +++++++ system_prompts/generate.py | 2 + system_prompts/utils.py | 1 + 9 files changed, 267 insertions(+), 4 deletions(-) rename backend/.sqlx/{query-bb94e3105cb1bc5d90af2bc914b579a6e821c432eac38ad877d9ff362d8ab916.json => query-4d73cefcc1b72238b7731ab52376b849a6f712b4109a48d9f77da8dd0060ab1a.json} (76%) create mode 100644 system_prompts/auto-generated/schemas/email_trigger.schema.yaml diff --git a/backend/.sqlx/query-bb94e3105cb1bc5d90af2bc914b579a6e821c432eac38ad877d9ff362d8ab916.json b/backend/.sqlx/query-4d73cefcc1b72238b7731ab52376b849a6f712b4109a48d9f77da8dd0060ab1a.json similarity index 76% rename from backend/.sqlx/query-bb94e3105cb1bc5d90af2bc914b579a6e821c432eac38ad877d9ff362d8ab916.json rename to backend/.sqlx/query-4d73cefcc1b72238b7731ab52376b849a6f712b4109a48d9f77da8dd0060ab1a.json index 0a314a59e3..3972924862 100644 --- a/backend/.sqlx/query-bb94e3105cb1bc5d90af2bc914b579a6e821c432eac38ad877d9ff362d8ab916.json +++ b/backend/.sqlx/query-4d73cefcc1b72238b7731ab52376b849a6f712b4109a48d9f77da8dd0060ab1a.json @@ -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" } diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index a668352822..88a634d48e 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -1790e8df54bb00fb6800b8f3a6a2bb4c9787380e \ No newline at end of file +26184ab7a4aadfc529dcedf038aa08d36c7ad381 diff --git a/cli/src/guidance/core.ts b/cli/src/guidance/core.ts index 768d0b46a5..fe7bc3011a 100644 --- a/cli/src/guidance/core.ts +++ b/cli/src/guidance/core.ts @@ -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 diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index e005bc10b8..2d2c0a0ced 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -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 \`@\` are delivered to the configured runnable. Set \`workspaced_local_part: true\` to namespace it per workspace (the actual recipient becomes \`-@…\`); 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//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//attachments/" } +\`\`\` + +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 = { { 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" }, diff --git a/system_prompts/auto-generated/schemas/email_trigger.schema.yaml b/system_prompts/auto-generated/schemas/email_trigger.schema.yaml new file mode 100644 index 0000000000..7415996302 --- /dev/null +++ b/system_prompts/auto-generated/schemas/email_trigger.schema.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 diff --git a/system_prompts/auto-generated/skills/triggers/SKILL.md b/system_prompts/auto-generated/skills/triggers/SKILL.md index 183f447175..81401f1fdf 100644 --- a/system_prompts/auto-generated/skills/triggers/SKILL.md +++ b/system_prompts/auto-generated/skills/triggers/SKILL.md @@ -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 `@` are delivered to the configured runnable. Set `workspaced_local_part: true` to namespace it per workspace (the actual recipient becomes `-@…`); 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//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//attachments/" } +``` + +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 diff --git a/system_prompts/base/triggers.md b/system_prompts/base/triggers.md index 5205eb2d4f..d97688ca4d 100644 --- a/system_prompts/base/triggers.md +++ b/system_prompts/base/triggers.md @@ -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 `@` are delivered to the configured runnable. Set `workspaced_local_part: true` to namespace it per workspace (the actual recipient becomes `-@…`); 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//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//attachments/" } +``` + +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 diff --git a/system_prompts/generate.py b/system_prompts/generate.py index 7e6d23fd13..9c9da74081 100644 --- a/system_prompts/generate.py +++ b/system_prompts/generate.py @@ -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: diff --git a/system_prompts/utils.py b/system_prompts/utils.py index 779fbc56bb..152df2e1d9 100644 --- a/system_prompts/utils.py +++ b/system_prompts/utils.py @@ -49,6 +49,7 @@ SCHEMA_MAPPINGS = { ('SqsTrigger', 'sqs_trigger'), ('GcpTrigger', 'gcp_trigger'), ('AzureTrigger', 'azure_trigger'), + ('EmailTrigger', 'email_trigger'), ], 'schedules': [ ('Schedule', 'schedule'),