LandfallDocs
Triggering integrations · CloudWatch

Opening a war room from a CloudWatch alarm

When a CloudWatch alarm goes into ALARM, Landfall opens a war room for it, or attaches to the room that alarm already has open. The alarm reaches Landfall through SNS and a small forwarding Lambda.

This page is not about reading telemetry; see Connecting AWS to Landfall. The two are independent.

CloudWatch alarm
state change
→
SNS → Lambda
forwards the alarm JSON
→
Landfall
opens or attaches an incident

How it works

Landfall accepts an automated trigger on POST /triggers, the same public ingest endpoint Datadog, Coralogix and Sentry use. It is authenticated by your organization's bearer ingest token, which identifies the organization. Landfall recognizes a CloudWatch alarm by an AlarmName or NewStateValue string at the top level of the body, which is the JSON CloudWatch publishes to an SNS topic on an alarm action.

The alarm's ARN is its identity, so one alarm keeps to one room while that room is open. Plain SNS HTTPS subscriptions can't attach an Authorization header, so the recipe below puts a small forwarding Lambda between SNS and Landfall.

What you'll need

  • An admin seat on your Landfall organization, to mint the ingest token.
  • AWS credentials that can create an SNS topic, a Lambda function and its execution role.
  • At least one CloudWatch alarm already configured. This guide doesn't create one.
  • The Lambda runs on Node.js 20 or later, which has a built-in fetch.

1. Mint the ingest token

If you already minted one for Datadog, Coralogix or Sentry, reuse it. The same token works for every source on this endpoint. The token has no screen in the app yet, so it is two API calls, made once with your own Landfall account.

  1. Get a session token or an API key

    If your organization signs in with email and password, sign in with curl:

    Terminal
    SESSION=$(curl -s -X POST https://api.landfalls.ai/auth/login \
      -H "Content-Type: application/json" \
      -d '{"email":"<you>","password":"<your password>","orgSlug":"<your-org>"}' \
      | jq -r .accessToken)

    If your organization signs in through SSO, there is no password to hand curl. Create a personal API key instead, under Your settings → API keys → New key. The key starts with lf_key_ and is shown once. Use the whole key as $SESSION.

    Terminal
    SESSION='<the whole key, starting lf_key_>'

    Either way the credential acts as you, so you must be an admin of the organization. Revoke the API key from the same panel afterwards if you made it only for this.

  2. POST /o/<your-org>/triggers/ingest-token

    Terminal
    curl -X POST https://api.landfalls.ai/o/<your-org>/triggers/ingest-token \
      -H "Authorization: Bearer $SESSION"
Shown exactly once

The response is { "token": "…" }. It is a separate, long-lived credential, scoped to opening incidents in this one organization and nothing else. Landfall stores only its hash, so a lost token means minting a new one, which immediately invalidates the old one; there is no dual-valid window. Keep it out of the Lambda's code.

2. Point CloudWatch at Landfall

  1. aws sns create-topic

    If your alarms already notify an SNS topic, skip this and reuse that topic.

    Terminal
    aws sns create-topic --name landfall-incident-triggers
  2. Add the topic to your alarm's actions

    --alarm-actions notifies the topic when the alarm goes into ALARM. Add the same topic to --ok-actions if you want recoveries recorded on the room, and to --insufficient-data-actions if you want missing data noted there too.

    Terminal
    aws cloudwatch put-metric-alarm --alarm-name <your-alarm> \
      --alarm-actions <the topic arn> \
      --ok-actions <the topic arn> \
      <the rest of your existing alarm definition>
  3. Deploy the forwarder Lambda

    The function forwards the alarm's own JSON unchanged. It already carries every field Landfall reads.

    index.mjs
    export const handler = async (event) => {
      const token = process.env.LANDFALL_INGEST_TOKEN;
    
      for (const record of event.Records) {
        // CloudWatch's own alarm-notification JSON, already the shape Landfall
        // expects, forwarded as-is rather than re-parsed or rebuilt.
        const alarmPayload = record.Sns.Message;
    
        const res = await fetch('https://api.landfalls.ai/triggers', {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${token}`,
            'Content-Type': 'application/json',
          },
          body: alarmPayload,
        });
    
        if (!res.ok) {
          // Log and move on: one bad delivery shouldn't fail the whole batch.
          console.error('Landfall trigger ingest failed', res.status, await res.text());
        }
      }
    };
    Terminal
    zip function.zip index.mjs
    
    aws lambda create-function --function-name landfall-trigger-forwarder \
      --runtime nodejs20.x --handler index.handler --zip-file fileb://function.zip \
      --role <an execution role with basic Lambda logging permissions> \
      --environment "Variables={LANDFALL_INGEST_TOKEN=<the token from step 1>}"
    
    aws lambda add-permission --function-name landfall-trigger-forwarder \
      --statement-id sns-invoke --action lambda:InvokeFunction \
      --principal sns.amazonaws.com --source-arn <the topic arn>
    
    aws sns subscribe --topic-arn <the topic arn> \
      --protocol lambda --notification-endpoint <the function arn just created>
URLPOST https://api.landfalls.ai/triggers
HeaderAuthorization: Bearer <your ingest token>
BodyThe SNS record's Message, unchanged
StatesALARM, and optionally OK and INSUFFICIENT_DATA

What CloudWatch's fields become

CloudWatch fieldIn Landfall
AlarmNameThe incident title
AlarmArnThe alarm's identity. Without it, AWSAccountId, Region and AlarmName together
NewStateValueALARM opens or repeats, OK is a recovery, INSUFFICIENT_DATA is recorded as no data
NewStateReasonThe summary on the incident's first timeline event
StateChangeTimeWhen it happened; with the state, it also identifies this one delivery
Trigger.MetricNameThe severity hint: a name containing 5xx or error proposes SEV2, anything else SEV3
Trigger.DimensionsThe incident's entity. A DistributionId dimension names a CloudFront distribution, a TargetGroup or LoadBalancer a target group, and a Service or ServiceName a service
Trigger.ThresholdRecorded in the severity basis of a SEV2 proposal

3. Test it

Publish a test notification straight to the topic:

Terminal · Worked example
aws sns publish --topic-arn <the topic arn> \
  --message '{
    "AlarmName": "landfall-test-alarm",
    "NewStateValue": "ALARM",
    "NewStateReason": "Threshold crossed",
    "StateChangeTime": "2026-01-01T00:00:00.000Z",
    "Trigger": {
      "MetricName": "5xxErrorRate",
      "Namespace": "AWS/CloudFront",
      "Dimensions": [{ "name": "DistributionId", "value": "E123ABC" }]
    }
  }'

Within a few seconds a new incident titled landfall-test-alarm appears in your organization's Incidents list with the AWS CloudWatch badge, proposed at SEV2 because the metric name contains 5xx. The incident is about CloudFront distribution E123ABC, its first timeline event carries the reason Threshold crossed, and the proposed severity records its basis, cloudwatch state=ALARM metric=5xxErrorRate. Publish the same message again and nothing new is recorded, because the same state change time and state mark it as a redelivery.

Then test a real alarm end to end, for example with aws cloudwatch set-alarm-state --alarm-name <your-alarm> --state-value ALARM --state-reason "Testing Landfall".

With AWS also connected as a source, Beacon can chart the alarm's metric inside the room.

app.landfalls.ai · War room · Acme 42
A war room for incident Acme 42 in the Acme org, SEV2 and investigating. The canvas shows a 5xx rate chart for eu-west-1 and us-east-1 with a deploy v2.14.0 marker and an alert line at 4.5%. The widget Info panel on the right lists Source AWS CloudWatch, Operation getMetricData, Account 123456789012, and a query with namespace AWS/CloudFront, metricName 5xxErrorRate and stat Average.
A room in the demo org Acme. The Info panel of Beacon's chart names AWS CloudWatch as the source and reads the same CloudFront 5xxErrorRate metric as the test alarm above. 123456789012 is an example account. The alarm trigger opens the room; charting the metric needs the AWS connection.

Repeats and recovery

The alarm's ARN is the attach identity. While a room for that alarm is open, a later ALARM is recorded on it as a repeat and opens nothing new. An OK is recorded on that room as a recovery and never opens an incident. INSUFFICIENT_DATA is recorded on an open room, or ignored when there is none. A redelivered message, with the same state change time and state, is recognized and not recorded twice.

A recovery never resolves the room. The alarm clearing is a fact about the alarm, and closing the war room is a person's decision. Once a person resolves the room, the alarm's next ALARM opens a new one. An ALARM that would open a room while an active maintenance window matches it is suppressed, and no room opens.

Troubleshooting

What you seeWhat it means
401 Missing trigger ingest tokenThe request had no Authorization header. Check the Lambda's LANDFALL_INGEST_TOKEN variable.
401 Invalid trigger ingest tokenThe token is wrong, or a newer mint replaced it. The two look the same. Mint a new token and update the Lambda.
422 Unrecognized trigger payloadThe body has neither AlarmName nor NewStateValue. Check that the Lambda forwards record.Sns.Message, not the whole SNS record. No incident is created.
"outcome": "ignored", "reason": "no-open-incident"An OK or INSUFFICIENT_DATA arrived and that alarm has no open room
"outcome": "suppressed"An active maintenance window matched the alarm, so no room opened
Nothing arrives at allThe alarm does not list the topic for that state, or the Lambda is not subscribed to the topic or lacks the invoke permission. The function's own logs show a Landfall trigger ingest failed line for any delivery Landfall refused.
Two rooms for one alarmThe first room was resolved before the second ALARM, or a hand-built payload without AlarmArn changed its account, region or name between sends

Reference

EndpointPOST https://api.landfalls.ai/triggers
AuthAuthorization: Bearer <ingest token> identifies the organization directly; there is no separate org path parameter
Issue or rotatePOST https://api.landfalls.ai/o/<your-org>/triggers/ingest-token as an admin. Shown once; minting again invalidates the previous token
Recognized byAn AlarmName or NewStateValue string at the top level of the body
Attach identityAlarmArn, or AWSAccountId + Region + AlarmName when the ARN is missing
Each deliveryALARM opens a room (201) or attaches to the open one (200). OK records a recovery and INSUFFICIENT_DATA records no data, on an open room only. Never auto-resolves
Rejected payloads422, no incident created; a missing or invalid token is a 401
Incident badgeAWS CloudWatch
Also acceptsDatadog, Coralogix and Sentry payloads on the same endpoint with the same token. See Datadog, Coralogix and Sentry.

How every trigger shares this pipeline: Integrations architecture.