Skip to content

Triggers

Triggers define how a workflow is started automatically when Mantle runs in server mode. This page covers setting up cron and webhook triggers.

Setting Up Cron Triggers

A cron trigger executes a workflow on a recurring schedule. Add a triggers section to your workflow YAML:

name: daily-report
description: Generate and email a daily summary report

triggers:
  - type: cron
    schedule: "0 9 * * 1-5"

steps:
  - name: fetch-metrics
    action: http/request
    params:
      method: GET
      url: https://api.internal.com/metrics/daily

  - name: summarize
    action: ai/completion
    credential: my-openai
    params:
      model: gpt-4o
      prompt: "Summarize these metrics into 5 bullet points: {{ steps.fetch-metrics.output.body }}"

Apply the workflow to register the trigger:

mantle apply daily-report.yaml
# Applied daily-report version 1

The cron scheduler picks up the trigger the next time it polls (within 30 seconds). The workflow runs every weekday at 9 AM.

Cron Expression Syntax

The schedule field uses standard 5-field cron syntax:

minute (0-59)
hour (0-23)
day of month (1-31)
month (1-12)
day of week (0-6, Sunday=0)

Common patterns:

ScheduleExpression
Every minute* * * * *
Every 5 minutes*/5 * * * *
Every hour on the hour0 * * * *
Daily at midnight0 0 * * *
Weekdays at 9 AM0 9 * * 1-5
First of every month at noon0 12 1 * *
Every 15 minutes during business hours*/15 9-17 * * 1-5

Updating a Cron Schedule

Edit the schedule field in your YAML and re-apply:

# Change from every 5 minutes to every 15 minutes
mantle apply daily-report.yaml
# Applied daily-report version 2

The scheduler picks up the updated schedule on the next poll cycle.

Removing a Cron Trigger

Delete the triggers section from the YAML (or remove the specific trigger entry) and re-apply:

mantle apply daily-report.yaml
# Applied daily-report version 3

The trigger is deregistered. The workflow is still available for manual execution with mantle run or the REST API.

Setting Up Webhook Triggers

A webhook trigger executes a workflow when an HTTP POST request arrives at a configured path. The request body is available as trigger.payload in CEL expressions.

name: deploy-notifier
description: Post a Slack notification when a deploy completes

triggers:
  - type: webhook
    path: "/hooks/deploy-notifier"

steps:
  - name: notify-slack
    action: http/request
    params:
      method: POST
      url: https://hooks.slack.com/services/T00/B00/xxx
      body:
        text: "Deployed {{ trigger.payload.repo }}@{{ trigger.payload.sha }} to {{ trigger.payload.environment }}"

Apply the workflow:

mantle apply deploy-notifier.yaml
# Applied deploy-notifier version 1

Trigger it from your CI pipeline or any HTTP client:

curl -X POST http://localhost:8080/hooks/deploy-notifier \
  -H "Content-Type: application/json" \
  -d '{
    "repo": "my-app",
    "sha": "abc1234",
    "environment": "production"
  }'

The server starts a new execution and the full JSON body is available as trigger.payload.

Accessing Webhook Payload Data

The trigger.payload variable contains the parsed JSON body. Access nested fields with dot notation in template strings or bracket notation in if expressions:

# In template strings (params):
url: "{{ trigger.payload.callback_url }}"
prompt: "Analyze this event: {{ trigger.payload }}"

# In if expressions:
if: "trigger.payload.action == 'opened'"

Setting Up Email Triggers

An email trigger polls an email mailbox and executes a workflow each time a new message matching the filter arrives. The trigger runs continuously when Mantle is in server mode.

name: email-inbox-triage
description: AI-powered email classification and organization

triggers:
  - type: email
    mailbox: company-inbox
    folder: INBOX
    filter: unseen
    poll_interval: 30s

steps:
  - name: classify
    action: ai/completion
    credential: my-openai
    params:
      model: gpt-4o
      system_prompt: "Classify this email as: important, actionable, newsletter, or spam."
      prompt: |
        From: {{ trigger.from }}
        Subject: {{ trigger.subject }}
        Body: {{ trigger.body }}
      output_schema:
        type: object
        properties:
          category:
            type: string
            enum: [important, actionable, newsletter, spam]
        required: [category]

  - name: move-email
    action: email/move
    credential: company-inbox
    params:
      uid: "{{ trigger.uid }}"
      target_folder: "{{ steps.classify.output.json.category }}"

Apply the workflow to start polling:

mantle apply email-inbox-triage.yaml
# Applied email-inbox-triage version 1

Email Trigger Configuration

The email trigger type polls a mailbox for messages matching a filter and executes the workflow for each message found.

Configuration:

FieldTypeRequiredDescription
typestringYesMust be email.
mailboxstringYesCredential name for the email account (IMAP-compatible).
folderstringNoFolder to monitor (e.g., INBOX, Archive). Default: INBOX.
filterstringNoFilter messages: all, unseen, recent, flagged. Default: unseen.
poll_intervalstringNoHow often to check for new messages (e.g., 30s, 5m). Default: 60s.

Trigger Context Variables

When an email trigger fires, the following variables are available in CEL expressions and template strings:

VariableTypeDescription
trigger.message_idstringUnique message ID.
trigger.uidnumberIMAP UID (for use with email actions).
trigger.fromstringSender email address.
trigger.tostringPrimary recipient(s).
trigger.ccstringCC recipients.
trigger.subjectstringMessage subject.
trigger.bodystringMessage body (plaintext).
trigger.datestringMessage date (RFC 3339 timestamp).
trigger.headersmapFull message headers.
trigger.flagsarrayIMAP flags (e.g., ["seen", "flagged"]).

Example — conditional logic based on sender:

steps:
  - name: handle-vip-email
    action: ai/completion
    credential: my-openai
    if: "trigger.from == '[email protected]'"
    params:
      model: gpt-4o
      prompt: "This is from the CEO. Provide an executive summary:\n\n{{ trigger.body }}"

  - name: notify-team
    action: slack/send
    credential: slack-bot
    if: "trigger.from == '[email protected]'"
    params:
      channel: "#executive-inbox"
      text: "New email from {{ trigger.from }}: {{ trigger.subject }}"

At-Least-Once Delivery

The email trigger marks messages as seen after firing the workflow for each message. If the Mantle process crashes or is restarted mid-poll, messages that were fetched but not yet marked may be re-triggered on the next poll cycle. This means email-triggered workflows may receive the same message more than once.

Design email-triggered workflows to be idempotent. A reliable approach is to deduplicate on trigger.message_id:

steps:
  - name: check-duplicate
    action: postgres/query
    credential: my-database
    params:
      query: "INSERT INTO processed_emails (message_id) VALUES ($1) ON CONFLICT DO NOTHING"
      args:
        - "{{ trigger.message_id }}"

  - name: process-email
    action: ai/completion
    if: "steps['check-duplicate'].output.rows_affected > 0"
    # ... rest of workflow

Connection Management

Email triggers maintain persistent IMAP connections to reduce authentication overhead. By default, Mantle pools up to 5 concurrent connections per mailbox credential. This limit is a compile-time default in v0.3.0 and is not runtime-configurable via mantle.yaml.

Provider Limits

Email providers have different concurrency limits. Plan your poll intervals accordingly:

ProviderMax ConcurrentRecommendation
Gmail15Up to 15 concurrent connections; use poll_interval: 30s for many workflows
Microsoft 36520Higher concurrency limit; safe for aggressive polling
Generic IMAPVariesCheck with your email host; conservative: 5 concurrent

Example — multiple workflows, Gmail:

If you have 3 email workflows on Gmail, each connecting once per poll:

# workflow-1.yaml
triggers:
  - type: email
    mailbox: my-gmail
    folder: INBOX
    poll_interval: 60s  # Check every minute

# workflow-2.yaml
triggers:
  - type: email
    mailbox: my-gmail
    folder: Archive
    poll_interval: 60s

# workflow-3.yaml
triggers:
  - type: email
    mailbox: my-gmail
    folder: Drafts
    poll_interval: 60s

Each workflow gets its own connection, but they share the pool. If latency is a concern, stagger poll intervals.