AWS Builder Center

The warning that night was a dog: building a Hindi flood alert loop on AWS

How Step Functions, Polly, IoT Core and Cedar turned a river forecast into a spoken warning that every villager confirms, and what fought back along the way.

Built for Environmental Hacks (WeMakeDevs × AWS), Heat and Water track. Live:  https://main.d15r7ktz99l46x.amplifyapp.com  · Code:  https://github.com/Prashant-thakur77/pukaar 
On the night of 30 June 2025, cloudburst after cloudburst hit Mandi district in Himachal Pradesh. There was a red alert for the district. But in Syathi village, what woke people up was not a warning.
It was a family's dog, barking at a crack in the wall with water seeping through. They woke their neighbours, and more than 60 people got out before the hillside came down. A villager later told The Tribune: "Can we trust a dog to warn us next time? We need proper early warning systems."
We're three students, and that question became our project. Pukaar (पुकार, "the call") is a flood early-warning loop for five villages in Mandi. It turns a river forecast into a spoken Hindi warning, and it doesn't stop until each person has confirmed they heard it.


A warning nobody hears is not a warning

We started out thinking the hard part was prediction. It isn't. The Himalaya has forecasts; what fails is the last mile.
  • Nepal, September 2024: the weather department sent 4,093,795 alert SMS, and 250 people still died.
  • Nepal, 26 August 2026: a glacier collapsed at 8:37 am. The SMS went out at 9:15, 38 minutes later, only in Nepali, with nothing about where to go. A resident: "The SMS and the flood came together."
  • Himachal Pradesh: 1 of India's 338 Central Water Commission flood-forecasting stations (December 2023).
The research is blunt: a warning only works once it is received, understood and acted on, and in field studies people strongly preferred voice to text. So we set one rule: every warning is spoken in Hindi, and every person confirms it.

The loop, end to end

  1. Every 15 minutes, the sweep reads rain and river forecasts for each village, plus river-gauge readings.
  1. Code sets the level, never a model. It uses IMD's rain bands (64.5 / 115.6 / 204.5 mm in 24 h), each village's own 1984–2024 river percentiles, two-source agreement and hysteresis.
  1. A Hindi alert is drafted, and a checker rejects any number or place that isn't in the decision trace.
  1. A district officer approves it with one tap. No answer? It moves to the next officer.
  1. Amazon Polly speaks it, and Telegram delivers the voice with one button: "मिल गया" (got it). Anyone who hasn't tapped it is sent the alert again.
  1. Afterwards it asks "are you safe?", shows the nearest open shelter with a slope-aware walking route, and lets the village head mark a road as blocked.
On the July 2023 Beas floods, the back-test gave Gohar its first Watch 72 hours before the river's peak day. Rain alone gave about 19 hours of lead; adding each village's own river record gave at least 48.

The architecture is a chain of jobs, not a list of logos

All of it is one AWS SAM stack. Nothing runs between requests.
Job
Service
Why this one
Wake up every 15 minutes
EventBridge Scheduler → Lambda
Zero retries plus a dead-letter queue: a stale sweep is worse than a skipped one
Hear the river
AWS IoT Core
One X.509 certificate per gauge, a policy pinned to its own topic, a topic rule straight into Lambda
Remember, exactly once
DynamoDB
Conditional writes mean a village can never get the same alert twice
Wait for a human
Step Functions
 waitForTaskToken  pauses until an officer decides
Draft safely
Bedrock + Guardrails (Strands Agents)
Guardrails screen every villager report before any agent reads it
Speak and listen
Polly + Transcribe (hi-IN)
Voice out, voice reports in
Feel live
AppSync Events
Approvals reach every open screen in about a second
Everything else
Cognito, API Gateway, S3, SES, Parameter Store, CloudWatch, Amplify
Sign-in, storage, after-action e-mail, secrets, 10 alarms, the web app

A human decides before a village is woken at 3 am

This is the part of AWS we'd recommend to anyone building something life-critical. One Step Functions execution per alert pauses at a task, hands Lambda a token, and waits:
JSON
"AskOfficer": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
"Parameters": {
"FunctionName": "${WorkflowFunctionArn}",
"Payload": { "step": "ask_officer", "alert_id.$": "$.alert_id", "task_token.$": "$$.Task.Token" }
},
"TimeoutSecondsPath": "$.config.approval_timeout_seconds"
}
The officer taps Approve on the console, or straight from a phone notification. The API calls  SendTaskSuccess , and the workflow carries on to Polly and delivery. On timeout it asks the next officer. A critical alert that nobody approves still goes out, flagged as unapproved. Every execution doubles as a visual audit trail.
📷 Figure 2. The officer console's one-tap approval

AI may draft. It may never decide.

We wanted a rule that holds even if a model misbehaves, so it's written in Cedar (the open-source policy engine), checked on every action, and every allow or deny is logged:
cedar
@id("agents-never-send")
forbid (
principal is Pukaar::Agent,
action in [ Pukaar::Action::"approve", Pukaar::Action::"deliver",
Pukaar::Action::"auto_send", Pukaar::Action::"block_road" ],
resource
);
A village head (pradhan) who tries to approve another village's alert is denied, and the audit log records which policy said no.

What fought back

1. Bedrock said "Operation not allowed" for the whole build.
  • Symptom: every model call on our account was refused, with no reason given.
  • What we did: because the level is set by rules, losing the model never stops a warning. A fixed, tested Hindi template takes over and is labelled  rule-fallback  everywhere. When access arrives, a 6-hourly probe flips the agents to "Live" by itself.
  • The surprise: Bedrock Guardrails'  ApplyGuardrail  works without model access, so our safety layer went live before our AI did. For a life-safety alert, we now think template-first is the right default anyway.
2. The bug that passed every test: our landing page said "1 alert sent".
  • Symptom: there had been no flood, so where did a real alert come from?
  • Cause: it was a drill we had sent to a real phone. The counter counted every delivered, non-replay alert, and every test passed, because no test had imagined a drill.
  • Fix: drills and replays are now counted apart from real alerts, with 25 new tests. If an early-warning system misreports its own record, nobody should trust it.
3. The 3D valley silently showed nothing.
  • Cause: Amplify's single-page-app rewrite answered requests for our  .bin  terrain tiles with  index.html .
  • Fix: add  bin  to the rewrite exceptions. There was no error anywhere, just an empty valley.
4. Browsers got 401 before they even asked.
  • Cause: the HTTP API's JWT authorizer on the catch-all route also guarded CORS preflight  OPTIONS  requests.
  • Fix: an explicit  OPTIONS /{proxy+}  route with no authorizer, plus a test that fails if the preflight route ever disappears.
Bonus: on a flaky home connection,  sam deploy  kept failing to upload our ~228 MB Lambda images. We pushed the images to ECR ourselves with retries, pointed the template at them, and SAM had nothing left to upload.

What's real, and what isn't yet

  • Live on AWS today: the sweep, rules, Step Functions approvals, Polly, Telegram delivery and acknowledgement, Guardrails, the IoT Core path, AppSync, the CAP 1.2 feed and the roll-call.
  • Simulated, and labelled so: river gauges. The hardware design is in the repo; no gauge is installed yet.
  • Pending: Bedrock drafting (model access).
  • Not built: SMS and voice calls in India (DLT registration; Amazon Connect can't call Indian numbers).
  • We make no claim about lives saved.
Some numbers, all measured:


Villages watched
5, every 15 minutes
Report page on slow 3G
4.6 s, 129 kB
Approval to open screens (AppSync)
~75 ms
CloudWatch alarms
10
Tests
425 backend + 136 frontend, including tests that fail on any IAM  * 

What we'd tell ourselves on day one

  1. Find out where the warning dies, not where it starts. Our best feature is a button that says "मिल गया".
  1. Let code decide and people approve; use AI only where a wrong answer can't hurt anyone.
  1. Test the claims, not just the code. Our tests read the real  template.yaml , and one even checks that the README states the right number of alarms.

What's next

We keep building. The river-gauge path already runs on AWS IoT Core; next are real gauges under the bridges:
  • IoT Core for LoRaWAN, for bridges with no phone signal;
  • IoT Greengrass, for a siren at the gauge that works offline;
  • SageMaker with Timestream, to learn each river's warning signs;
  • voice calls to basic phones, and a link into India's national alert system.
We want to build this to scale, until every village on a Himalayan river hears its warning in time.
Try it in 30 seconds: open the live site, choose any village, and press "Listen" to hear a real Polly alert. Or watch the pitch video in the repo.
Team: Prashant Thakur, Puneet Yadav, Prashant Bahuguna.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article