Triggers¶
Triggers make your Lamia scripts react to real-world events automatically. Instead of running a script manually, you define what should start it — an incoming email, a new file appearing, etc.
Writing a Triggered Script¶
Add a trigger line at the point in your script where you want to wait for
an event. The parameters you list will be filled in with information from that
event:
trigger.email_received(sender, subject, body)
# From here on, sender/subject/body are available as regular variables.
reply_to_lead(sender, subject, body)
String keyword arguments are configuration (they tell Lamia where to listen, what to filter out, etc.):
Here name and size will come from the event, while path tells Lamia
which folder to watch.
Available Events¶
Email received¶
Fields you can request: sender, subject, body, html_body, message_id,
thread_id, timestamp, attachments, labels.
Filters¶
| Filter | IMAP equivalent | Example |
|---|---|---|
to |
TO "..." |
to="pricing@company.com" |
from_domain |
FROM "@..." |
from_domain="bigcorp.com" |
subject_contains |
SUBJECT "..." |
subject_contains="invoice" |
label |
IMAP folder to poll | label="Invoices" |
File created / modified / deleted¶
trigger.file_created(name, size, content_type, path="reports/incoming")
trigger.file_modified(name, timestamp, path="shared/data")
trigger.file_deleted(name, path="archive/temp")
Fields: name, size, content_type, timestamp, metadata.
The path parameter specifies the folder or location to monitor.
Email Configuration¶
Email triggers connect via IMAP. Add the connection settings to your project's config.yaml and store the password in ~/.lamia/.env.
config.yaml¶
triggers:
email:
host: imap.gmail.com # IMAP server hostname
port: 993 # IMAP SSL port (default: 993)
username: you@gmail.com # Email address
password_env: IMAP_PASSWORD # Name of the env var holding the password
poll_interval: 30 # Seconds between mailbox checks (default: 30)
label: INBOX # IMAP folder to monitor (default: INBOX)
~/.lamia/.env¶
Tip
The password_env field holds the name of the environment variable, not the password itself. The actual password goes in ~/.lamia/.env and is never stored in project files.
Gmail¶
Works with both free Gmail accounts and Google Workspace accounts.
- Enable 2-Step Verification at myaccount.google.com/security
- Create an App Password at myaccount.google.com/apppasswords
- Select "Mail" and "Other (Lamia)"
- Google generates a 16-character password
- Add the password to
~/.lamia/.env: - Configure
config.yaml:
Google Workspace accounts work identically — the Workspace domain does not need to be tied to any GCP project or cloud infrastructure.
Outlook / Microsoft 365¶
Microsoft 365 may require an admin to enable IMAP access. Create an App Password at account.live.com/proofs/AppPassword.
Yahoo Mail¶
Generate an App Password at login.yahoo.com/account/security/app-passwords.
Any IMAP Server¶
Any email provider that supports IMAP works:
triggers:
email:
host: mail.yourcompany.com
port: 993
username: user@yourcompany.com
password_env: IMAP_PASSWORD
Deploying¶
When you deploy a script that contains trigger.* calls, Lamia automatically
sets up the event infrastructure for you.
Choosing a Mode¶
Reactive mode¶
Each event starts its own independent script execution right away.
Best for: notifications that must be handled immediately (order confirmations, alerts, time-sensitive replies).
To deploy a script that contains trigger.*s in reactive mode use:
Scheduled mode (similar to how an employee would work)¶
Events accumulate. At the scheduled time, the script wakes up, processes every pending event in parallel (one script execution per event), then sleeps until the next scheduled time.
Best for: tasks where you want to appear human (answering emails in the morning, processing daily uploads).
To deploy a script that contains trigger.*s in scheduled mode use:
This will deploy the script to the cloud and set up a cron job to run the script at 9:33 AM every day.
Multi-Step Scripts¶
A script can wait for more than one event. Each additional trigger.* call
pauses the script until that event arrives (up to 72 hours).
Every script execution is parallel and fully isolated. Each execution waits for its own event and runs independently.
Example: Enterprise Pricing Approval¶
Below is a complete script that handles pricing inquiries. Regular requests get an automated response. Enterprise requests need a human to prepare custom pricing, so the script waits for that file to appear.
from enum import Enum
from pydantic import BaseModel, Field
class LeadType(str, Enum):
ENTERPRISE = "enterprise"
STANDARD = "standard"
class LeadClassification(BaseModel):
lead_type: LeadType = Field(description="Whether this is an enterprise or standard lead")
company_slug: str = Field(description="Short lowercase identifier for the company, e.g. 'bigcorp'")
trigger.email_received(sender, subject, body, to="pricing@ourcompany.com")
classification = classify_lead(sender, subject, body) -> LeadClassification
if classification.lead_type == LeadType.STANDARD:
pricing_page = web.get_text("https://ourcompany.com/pricing")
compose_reply(sender, subject, pricing_page) -> File("drafts/reply.txt")
else:
notify_sales_team(sender, subject, classification.company_slug)
trigger.file_created(name, timestamp, path="sales/custom-pricing")
if not name.startswith(classification.company_slug):
trigger.reject()
else:
pricing_content = file.read(f"sales/custom-pricing/{name}")
compose_custom_reply(sender, name, pricing_content) -> File("drafts/reply.txt")
How this works at runtime:
- An email arrives to
pricing@ourcompany.com. Lamia starts a new, isolated execution of the script. Emails to other addresses are filtered out at the infrastructure level and never start the script. - The AI classifies the lead with a validated return type — you get a typed
LeadClassificationobject withlead_typeandcompany_slug. - Standard lead: reply is composed immediately, execution finishes.
- Enterprise lead: the script notifies the sales team and pauses (up to
72 hours) waiting for a file in
sales/custom-pricing. - When a team member uploads a file, the waiting execution resumes and checks whether the filename starts with the expected company slug.
- Wrong file:
trigger.reject()skips this event and the script keeps waiting for the next file event. - Correct file: reads the content and sends the custom reply.
If no file appears within 72 hours, the execution times out gracefully.
Best Practices¶
Use keyword filters to avoid unnecessary execution¶
Any keyword argument with a string value acts as an infrastructure-level filter. The script never starts for events that don't match. You can combine multiple filters on a single trigger:
trigger.email_received(sender, subject, body,
to="pricing@ourcompany.com",
from_domain="enterprise.com")
trigger.file_created(name, size, path="sales/invoices")
Filters are the most efficient approach — they prevent the job from launching entirely, saving both time and cost.
Use trigger.reject() when filtering depends on runtime data¶
When the filter can't be known at deploy time (e.g. it depends on a value
computed earlier in the script), use trigger.reject() to skip the event
and keep waiting for a different one:
trigger.file_created(name, timestamp, path="sales/custom-pricing")
if not name.startswith(expected_prefix):
trigger.reject()
trigger.reject() applies to the most recent trigger. It acknowledges the
event (so it won't come back to this execution) and the script continues
waiting for the next event on its private listener.
Since each execution has its own isolated listener, rejecting an event in one execution does not affect other concurrent executions — they each receive their own independent copy of every event.
Unhandled exceptions¶
If your script raises an unhandled exception after a trigger resumes, the
event is retried automatically (up to 5 attempts). After all retries are
exhausted, the event is saved as a failed event and can be inspected
via lamia trigger list --verbose.
To clear failed events for a trigger:
To stop and fully remove a trigger:
How Isolation Works¶
Each script execution that reaches a waiting point creates its own private event listener. This means:
- There is no queue contention — concurrent waits run in full isolation.
- Finished executions clean up their listeners automatically.
Listing Triggers¶
To see what triggers are configured:
Example output:
[a3f2c8b1d4e5] pricing_reply.lm (local)
event: email_received
mode: reactive
last run: 2026-07-09 status: ok
failed events: 2
The id in brackets is a random 12-character hex string assigned once when the trigger is first created.
To see details of failed events (what data they contained):
This shows the actual event payload and timestamp for each failure, so you can understand what went wrong and take action.
Viewing Logs¶
For cloud triggers, output never reaches your terminal. Take the id shown
in brackets by lamia trigger list and read the most recent run:
Local triggers print output to the terminal where they were started. Multi-stage triggers print each stage in order.
Local vs Cloud Triggers¶
Triggers run locally by default — Lamia watches for events on your machine and executes the script as a local process. No cloud setup is required.
To deploy triggers to the cloud instead, use --remote:
Cloud triggers require:
pip install "lamia-lang[cloud]"cloud.project_idset in your projectconfig.yaml
For team deployments via Git-based CI pipelines, see Git-Based Deployment.
Resource Lifecycle¶
If a script file is renamed, moved, or deleted, any trigger pointing to it
shows SOURCE_MISSING in lamia trigger list. To clean up these entries:
Unused cloud resources (containers, images) are automatically removed after 30 days of inactivity. Active schedules and triggers are never affected.