Alright, let’s talk about the brains of the EventBridge operation: Rules. If the event bus is the chaotic, noisy town square where events are shouted into the void, rules are the hyper-specific town criers you’ve hired to listen for only the exact kind of shouts you care about and then run off to tell another service what to do. They’re how you impose order on the chaos.

A rule does two things: it filters and it routes. It sits on an event bus, scrutinizes every event that passes by, and if the event matches the rule’s criteria, the rule forwards it to a target. The two most powerful ways to filter are by using a pattern or a schedule.

The Event Pattern: Your Event Bouncer

Think of an event pattern as the detailed spec sheet you give to a nightclub bouncer. “Let in anyone with a "type" of "Order.Confirmed", but only if their "detail-type" is "CustomerOrder" and their "detail" object has a "paymentMethod" of "CREDIT_CARD" and a "value" greater than 100.” The bouncer (the rule) checks every event (person) against this spec sheet and only lets the matching ones through to the VIP room (your target).

The pattern uses JSON to define these matching criteria. Here’s the magic: it’s not just simple equality matching; you can use prefix matching, numeric comparisons, arrays, and even the mystical exists operator. The syntax is its own little language, and it’s more powerful than it looks.

Here’s a realistic example. Let’s say you want to catch any event from your e-commerce service where an order failed due to being a fraudulent transaction.

{
  "source": ["com.mycompany.ecommerce"],
  "detail-type": ["OrderPaymentFailed"],
  "detail": {
    "reason": ["FRAUD_DETECTED"],
    "orderValue": [{"numeric": [">", 500]}]
  }
}

This pattern will catch events where:

  • The source is exactly com.mycompany.ecommerce (note it’s an array; you can list multiple sources).
  • The detail-type is exactly OrderPaymentFailed.
  • Inside the detail object, the reason is FRAUD_DETECTED AND the orderValue is a number greater than 500.

Why it works this way: The designers wanted something more flexible than a simple string but less complex than a full-blown programming language. This JSON-based pattern matching is declarative, which means AWS can optimize the heck out of it on their end. They’re not running a Lambda function to check each event; they’re using highly optimized pattern-matching logic.

Pitfall #1: The Case-Sensitivity Trap. This is the one that gets everyone. Every string match in an EventBridge pattern is case-sensitive. Always. If your service emits "FRAUD_detected" (lowercase ’d’), the pattern looking for "FRAUD_DETECTED" will ignore it. Your events and your patterns must have identical casing. I don’t love this design choice, but it is what it is. Be meticulous.

Pitfall #2: The Missing Field Silence. If an event doesn’t have a field your pattern is looking for, it simply doesn’t match. The rule doesn’t throw an error; it just quietly moves on. This is the most common cause of “my rule isn’t triggering!” debugging sessions. Always double-check the exact structure of your events using the EventBridge bus’s built-in archive or a dead-letter queue to see what’s actually being sent.

Scheduled Rules: Your Time Machine

Sometimes, you don’t want to react to an event; you want to cause one on a specific schedule. This is where scheduled rules come in. They are the Swiss Army knife for cron jobs in the serverless world.

You can define a schedule using either a cron expression or a rate expression. A rate is simpler (“every 5 minutes”), while cron gives you absolute control (“every Tuesday at 9:00 PM GMT”).

Here’s a rule defined in AWS CDK (Python) that triggers a Lambda function every morning at 6 AM UTC to run a daily report.

from aws_cdk import (
    aws_events as events,
    aws_events_targets as targets,
    aws_lambda as lambda_,
    core
)

class DailyReportStack(core.Stack):
    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # The Lambda function to run
        report_lambda = lambda_.Function(self, "DailyReportFunction",
            runtime=lambda_.Runtime.PYTHON_3_9,
            handler="index.handler",
            code=lambda_.Code.from_asset("lambda")
        )

        # Create a scheduled rule
        rule = events.Rule(self, "DailyReportRule",
            schedule=events.Schedule.cron(
                minute="0",
                hour="6",
                month="*",  # every month
                day="*",    # every day
                year="*"    # every year
            )
        )

        # Add the Lambda function as the rule's target
        rule.add_target(targets.LambdaFunction(report_lambda))

Why it works this way: Under the hood, AWS is running its own distributed cron service. Your rule is just a customer of that service. When the time comes, the service generates a synthetic event and puts it on your event bus, which your rule then picks up and routes to your target. The event looks like this:

{
  "version": "0",
  "id": "abc123...",
  "detail-type": "Scheduled Event",
  "source": "aws.events",
  "account": "123456789012",
  "time": "2023-10-27T06:00:00Z",
  "region": "us-east-1",
  "resources": ["arn:aws:events:us-east-1:123456789012:rule/DailyReportRule"],
  "detail": {}
}

Notice the detail object is empty. This is a key point: scheduled events have no custom detail. If your target needs context, you have to hardcode it in the target itself or use a InputTransformer on the rule to inject a constant value, which is a bit clunky but gets the job done.

Best Practice: Use UTC, Always. The cron scheduler runs in UTC. If you set a rule for 9 PM, it means 9 PM UTC. Forgetting this has caused more than one developer to accidentally run a expensive batch job at 2 PM local time instead of 2 AM. Learn from our collective pain. Set all your schedules in UTC and convert in your head. It’s the law of the land.