AWS alerts to your phone: CloudWatch alarms and EventBridge events

Updated October 17, 2026

There are two ways to get AWS alerts on your phone with NextMsg. For CloudWatch alarms (and anything else that publishes to SNS, such as AWS Budgets), subscribe a tiny Lambda function to an SNS topic. For EventBridge events (a failed pipeline, a stopped ECS task, an EC2 state change), use an API destination: no code at all. Both come as CloudFormation templates below.

First, install the NextMsg app and copy your send key from Settings.

CloudWatch alarms: SNS and Lambda

This template creates an SNS topic and a Lambda function that formats each message and sends it to NextMsg. Alarm messages become a short line such as "🚨 HighCPU is ALARM" with the reason; other SNS messages are sent with their subject.

AWSTemplateFormatVersion: "2010-09-09"
Description: Forward messages on an SNS topic (CloudWatch alarms and others) to NextMsg

Parameters:
  NextMsgKey:
    Type: String
    NoEcho: true
    Description: NextMsg send key (nextmsg_live_...)

Resources:
  AlertsTopic:
    Type: AWS::SNS::Topic

  NotifyRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal: { Service: lambda.amazonaws.com }
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  NotifyFunction:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: nodejs22.x
      Handler: index.handler
      Role: !GetAtt NotifyRole.Arn
      Timeout: 10
      Environment:
        Variables:
          NEXTMSG_KEY: !Ref NextMsgKey
      Code:
        ZipFile: |
          exports.handler = async (event) => {
            for (const record of event.Records) {
              const { Subject, Message } = record.Sns;
              let text = Subject ? `${Subject}\n${Message}` : Message;
              try {
                const alarm = JSON.parse(Message);
                if (alarm.AlarmName) {
                  const icon = { ALARM: "🚨", OK: "✅" }[alarm.NewStateValue] || "ℹ️";
                  text = `${icon} ${alarm.AlarmName} is ${alarm.NewStateValue}\n${alarm.NewStateReason}`;
                }
              } catch {}
              const res = await fetch("https://api.nextmsg.app/v1/send", {
                method: "POST",
                headers: {
                  Authorization: `Bearer ${process.env.NEXTMSG_KEY}`,
                  "Content-Type": "application/json",
                },
                body: JSON.stringify({ message: [...text].slice(0, 640).join(""), channel: "aws" }),
              });
              if (!res.ok) throw new Error(`NextMsg ${res.status}: ${await res.text()}`);
            }
          };

  TopicSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref AlertsTopic
      Protocol: lambda
      Endpoint: !GetAtt NotifyFunction.Arn

  InvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref NotifyFunction
      Principal: sns.amazonaws.com
      SourceArn: !Ref AlertsTopic

Outputs:
  TopicArn:
    Description: Use this topic as the action of any CloudWatch alarm
    Value: !Ref AlertsTopic

Save it as nextmsg-sns.yaml and deploy. Reading the key with read -s keeps it out of your shell history:

read -rs NEXTMSG_KEY
aws cloudformation deploy --template-file nextmsg-sns.yaml --stack-name nextmsg-alerts \
  --parameter-overrides NextMsgKey="$NEXTMSG_KEY" --capabilities CAPABILITY_IAM
aws cloudformation describe-stacks --stack-name nextmsg-alerts \
  --query "Stacks[0].Outputs[?OutputKey=='TopicArn'].OutputValue" --output text

Use the topic ARN as the alarm action, in the console or with the CLI:

aws cloudwatch put-metric-alarm --alarm-name HighCPU \
  --namespace AWS/EC2 --metric-name CPUUtilization --statistic Average \
  --dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
  --period 300 --evaluation-periods 2 --threshold 90 --comparison-operator GreaterThanThreshold \
  --alarm-actions TOPIC_ARN --ok-actions TOPIC_ARN

Test the whole path without waiting for an alarm:

aws sns publish --topic-arn TOPIC_ARN --subject "Test" --message "Hello from AWS"

EventBridge events: an API destination

EventBridge can call NextMsg directly. The connection stores the key as an Authorization: Bearer header (EventBridge keeps it in AWS Secrets Manager), and an input transformer turns the event into a NextMsg message. This example alerts on failed CodePipeline executions:

AWSTemplateFormatVersion: "2010-09-09"
Description: Send failed CodePipeline executions to NextMsg through an EventBridge API destination

Parameters:
  NextMsgKey:
    Type: String
    NoEcho: true
    Description: NextMsg send key (nextmsg_live_...)

Resources:
  NextMsgConnection:
    Type: AWS::Events::Connection
    Properties:
      AuthorizationType: API_KEY
      AuthParameters:
        ApiKeyAuthParameters:
          ApiKeyName: Authorization
          ApiKeyValue: !Sub "Bearer ${NextMsgKey}"

  NextMsgDestination:
    Type: AWS::Events::ApiDestination
    Properties:
      ConnectionArn: !GetAtt NextMsgConnection.Arn
      InvocationEndpoint: https://api.nextmsg.app/v1/send
      HttpMethod: POST
      InvocationRateLimitPerSecond: 1

  InvokeRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal: { Service: events.amazonaws.com }
            Action: sts:AssumeRole
      Policies:
        - PolicyName: invoke-nextmsg
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: events:InvokeApiDestination
                Resource: !GetAtt NextMsgDestination.Arn

  PipelineFailedRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source: [aws.codepipeline]
        detail-type: [CodePipeline Pipeline Execution State Change]
        detail:
          state: [FAILED]
      Targets:
        - Id: nextmsg
          Arn: !GetAtt NextMsgDestination.Arn
          RoleArn: !GetAtt InvokeRole.Arn
          RetryPolicy:
            MaximumRetryAttempts: 3
          InputTransformer:
            InputPathsMap:
              pipeline: $.detail.pipeline
              region: $.region
            InputTemplate: |
              {"message": "❌ Pipeline <pipeline> failed in <region>", "channel": "aws"}

Deploy it the same way (--template-file nextmsg-eventbridge.yaml --stack-name nextmsg-pipelines). To watch something else, change only the EventPattern and the message. For example, stopped ECS tasks:

      EventPattern:
        source: [aws.ecs]
        detail-type: [ECS Task State Change]
        detail:
          lastStatus: [STOPPED]
      # ...
            InputPathsMap:
              group: $.detail.group
              reason: $.detail.stoppedReason
            InputTemplate: |
              {"message": "ECS task in  stopped: ", "channel": "aws"}

Notes

  • Keep the key secret: the stack parameter is NoEcho. In the Lambda version the key is a plain environment variable, visible to anyone who can read the function's configuration; for stricter setups, load it from AWS Secrets Manager or SSM Parameter Store instead.
  • Retries: EventBridge retries 429 and 5xx responses, so the template limits it to 3 attempts. SNS retries a failed Lambda invocation on its own.
  • Regions: alarms publish to an SNS topic in their own Region, so deploy the SNS stack in each Region you have alarms in (AWS billing alarms live in us-east-1).
  • Limits: messages are cut to 640 characters. The free plan includes 100 messages a month; a noisy alarm can use that quickly, so Pro (unlimited, fair use 5,000 a day) suits busy accounts.

More guides