Scheduled Events
EventFlow supports time-based event triggering for recurring tasks like cleanup jobs, reminders, and batch processing.
Basic Syntax
Use triggered: inside an event handler to specify when it should automatically fire:
on :check_expired_applications
triggered: every day at 00:00
for each application in #pending
? submitted more than 24 hours ago
application moves to #expired
emit :expiration_notice to @applicantThe triggered: Clause
The triggered: clause defines the schedule:
triggered: every day at 00:00
triggered: every hour
triggered: every monday at 09:00
triggered: every 30 minutes
triggered: every month on 1st at 00:00Schedule Patterns
| Pattern | Example | Meaning |
|---|---|---|
| Daily | every day at 00:00 | Every day at midnight |
| Hourly | every hour | Every hour on the hour |
| Weekly | every monday at 09:00 | Every Monday at 9am |
| Interval | every 30 minutes | Every 30 minutes |
| Monthly | every month on 1st at 00:00 | First of each month |
The for each Requirement
When using triggered:, the for each clause is required to specify which aggregates to process:
on :send_reminders
triggered: every day at 09:00
for each order in #awaiting_payment // Required
? created more than 12 hours ago
emit :payment_reminder to @customerWhy Required?
Scheduled events operate in a batch context - they run across multiple aggregates. The for each clause explicitly declares the iteration scope, making the batch nature clear:
// ❌ Ambiguous - which order?
on :check_expired
triggered: every day at 00:00
? order is in #pending
order moves to #expired
// ✅ Clear - iterate over all matching orders
on :check_expired
triggered: every day at 00:00
for each order in #pending
? older than 24 hours
order moves to #expiredExecution Model
When a scheduled event triggers:
- Query all aggregates matching
for eachcriteria - For each matching aggregate:
- Load aggregate history (event sourcing)
- Rebuild current state
- Evaluate guards
- Execute actions if guards pass
- Persist new events
┌─────────────────────────────────────────────────────────────┐
│ Scheduled Event Execution │
├─────────────────────────────────────────────────────────────┤
│ │
│ triggered: every day at 00:00 │
│ for each order in #pending │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Query: SELECT * FROM aggregates │ │
│ │ WHERE machine = '@order' │ │
│ │ AND current_state = '#pending' │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ order-123 order-456 order-789 │
│ │ │ │ │
│ [evaluate] [evaluate] [evaluate] │
│ [guards] [guards] [guards] │
│ │ │ │ │
│ [execute] [skip] [execute] │
│ │
└─────────────────────────────────────────────────────────────┘Complete Examples
Daily Cleanup
machine: @application
scenario: application expiry
on> :submit from @applicant
application moves to #pending
$submitted_at becomes now
on :expire_stale_applications
triggered: every day at 00:00
for each application in #pending
? $submitted_at older than 24 hours
application moves to #expired
emit :application_expired to @applicant
on :approve from @reviewer
application moves to #approvedWeekly Reports
machine: @reports
scenario: weekly reporting
on :generate_weekly_report
triggered: every monday at 09:00
for each department in #active
compile statistics for $department
emit :report_ready to @admin
with:
| department | $department_id |
| report | $report_data |Hourly Reminders
machine: @subscription
scenario: payment reminders
on :send_payment_reminders
triggered: every hour
for each subscription in #payment_overdue
? last_reminder_sent more than 6 hours ago
emit :payment_reminder to @subscriber
$last_reminder_sent becomes nowMonthly Billing
machine: @billing
scenario: monthly invoicing
on :generate_invoices
triggered: every month on 1st at 00:00
for each account in #active
? has_unbilled_usage
calculate monthly charges
emit :invoice_created to @customer
account moves to #invoicedGuards in Scheduled Events
Guards filter which aggregates are processed:
on :cleanup_abandoned_carts
triggered: every 6 hours
for each cart in #active
? updated more than 24 hours ago // Guard 1
? has items // Guard 2
? customer is not logged in // Guard 3
cart moves to #abandoned
emit :cart_abandoned to @analyticsOnly carts matching ALL guards are processed.
Multiple States in for each
You can target multiple states:
on :send_status_update
triggered: every day at 09:00
for each order in #pending, #processing, #shipped
emit :daily_status to @customerScheduled Events vs Regular Events
| Aspect | Regular Events | Scheduled Events |
|---|---|---|
| Trigger | External/internal event | Time-based |
| Target | Single aggregate | Multiple aggregates |
| Requires | Aggregate reference | for each clause |
| Execution | Single instance | Batch iteration |
Best Practices
Use Descriptive Event Names
// Good
on :expire_stale_applications
on :send_daily_summary_reports
on :cleanup_abandoned_sessions
// Avoid
on :daily_job
on :cron_taskAdd Appropriate Guards
on :cleanup_sessions
triggered: every hour
for each session in #active
? last_activity more than 30 minutes ago // Don't expire active sessions
? is not admin session // Don't expire admin sessions
session moves to #expiredConsider Timing
// Heavy operations at low-traffic times
on :generate_reports
triggered: every day at 03:00
// User-facing notifications during business hours
on :send_reminders
triggered: every day at 10:00Handle Large Batches
For very large numbers of aggregates, the runtime handles batching automatically. Your EventFlow code stays the same.