Skip to content

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:

flow
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 @applicant

The triggered: Clause

The triggered: clause defines the schedule:

flow
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:00

Schedule Patterns

PatternExampleMeaning
Dailyevery day at 00:00Every day at midnight
Hourlyevery hourEvery hour on the hour
Weeklyevery monday at 09:00Every Monday at 9am
Intervalevery 30 minutesEvery 30 minutes
Monthlyevery month on 1st at 00:00First of each month

The for each Requirement

When using triggered:, the for each clause is required to specify which aggregates to process:

flow
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 @customer

Why 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:

flow
// ❌ 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 #expired

Execution Model

When a scheduled event triggers:

  1. Query all aggregates matching for each criteria
  2. 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

flow
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 #approved

Weekly Reports

flow
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

flow
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 now

Monthly Billing

flow
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 #invoiced

Guards in Scheduled Events

Guards filter which aggregates are processed:

flow
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 @analytics

Only carts matching ALL guards are processed.

Multiple States in for each

You can target multiple states:

flow
on :send_status_update
  triggered: every day at 09:00

  for each order in #pending, #processing, #shipped
    emit :daily_status to @customer

Scheduled Events vs Regular Events

AspectRegular EventsScheduled Events
TriggerExternal/internal eventTime-based
TargetSingle aggregateMultiple aggregates
RequiresAggregate referencefor each clause
ExecutionSingle instanceBatch iteration

Best Practices

Use Descriptive Event Names

flow
// Good
on :expire_stale_applications
on :send_daily_summary_reports
on :cleanup_abandoned_sessions

// Avoid
on :daily_job
on :cron_task

Add Appropriate Guards

flow
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 #expired

Consider Timing

flow
// 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:00

Handle Large Batches

For very large numbers of aggregates, the runtime handles batching automatically. Your EventFlow code stays the same.

Released under the MIT License.