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
- Every 15 minutes, the sweep reads rain and river forecasts for each village, plus river-gauge readings.
- 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.
- A Hindi alert is drafted, and a checker rejects any number or place that isn't in the decision trace.
- A district officer approves it with one tap. No answer? It moves to the next officer.
- 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.
- 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-fallbackeverywhere. When access arrives, a 6-hourly probe flips the agents to "Live" by itself.
- The surprise: Bedrock Guardrails'
ApplyGuardrailworks 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
.binterrain tiles withindex.html.
- Fix: add
binto 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
OPTIONSrequests.
- 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
- Find out where the warning dies, not where it starts. Our best feature is a button that says "मिल गया".
- Let code decide and people approve; use AI only where a wrong answer can't hurt anyone.
- 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.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article