AWS Builder Center
JalSakshi: proving whether village tap water really came, with a missed call and AWS Step Functions

JalSakshi: proving whether village tap water really came, with a missed call and AWS Step Functions

We built a phone check-in on AWS that asks village families, on any keypad phone, whether water came today. A complaint closes only when those same families confirm it's fixed. Here's the architecture, the real data from our launch, and what fought back.

In Chhattisgarh, 7,603 villages report "tap water in every home" under Jal Jeevan Mission. Only 6,594 are certified by their Gram Sabha. That leaves about a thousand villages waiting on a decision, just as the state hands these water schemes over to Gram Panchayats.
Source: JJM IMIS Har Ghar Jal report, data as on 7 Oct 2026.

So we built JalSakshi (जल साक्षी, "water witness") for the Environmental Hacks Heat & Water track.
🎬 Watch it work:

JalSakshi is a phone number for the village. Any phone, Hindi or Chhattisgarhi, no app.
  1. Families join by phone. A missed call gets a call back; the family hears a short notice in their language, presses 1 to agree, and says their name and mohalla. Consent is stored in an append-only record.
  1. Every evening, a short call: did water come today, for how long, was it clean?
  1. A missed call at any hour reports a problem. Press 1 for no water, 2 for dirty water, or 3 to say it in your own words.
  1. The pump operator gets a call for each complaint: 1 for fixed, 2 to 6 for a reason, or 7 for "I can't fix this alone", which calls the Sarpanch with the operator's own words. After 48 hours with no fix, the Sarpanch is called automatically.
  1. A complaint closes only when the same families confirm on the phone that water is back.
The pump operator gets a call for every complaint.

The Panchayat secretary sees everything in a simple English console: complaints with place and area, an AI overview with a suggested next step, analytics, and a Gram Sabha evidence sheet.
The Panchayat secretary's view: complaints, families and evidence in one place.

The one rule that makes the evidence worth something.


The architecture

Everything runs in ap-south-1 (Mumbai) and is defined in AWS CDK (Python): four stacks, 18 Lambda functions, two Step Functions workflows, one DynamoDB table.
People use a keypad phone or a web page. Every decision behind the call runs on AWS in Mumbai.

The phone side is carried by Vobiz (an Indian +91 number). Vobiz calls our API Gateway webhooks at every step of a call, and a Lambda answers with the next prompt. Prompts are voiced once by Sarvam (Hindi voice "Ritu"), stored in S3 and served by CloudFront, so a call never waits for speech to be generated.

The trick: a phone call is just a task token

The evening check-in is one Step Functions run per village, started by EventBridge Scheduler at the village's own time in Asia/Kolkata. A Map state calls every family who agreed. Each call is a  waitForTaskToken  task: the workflow pauses until a person presses a key on a keypad phone, and the IVR Lambda resumes it.
The evening check-in.

In CDK, the call step looks like this (simplified):
Python
place_call = tasks.LambdaInvoke(
self, "PlaceCall",
lambda_function=place_call_fn,
integration_pattern=sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
payload=sfn.TaskInput.from_object({
"task_token": sfn.JsonPath.task_token,
"input": sfn.JsonPath.entire_payload,
}),
task_timeout=sfn.Timeout.duration(Duration.minutes(10)),
)
and when the family presses a key, the IVR Lambda hands the answer back:
Python
sfn.send_task_success(taskToken=token, output=json.dumps({"answered": True}))
No polling, no database flags: the workflow simply waits for a human.

A complaint is a workflow too

When the answers say NO_SUPPLY or DIRTY (decided by plain, tested rules, never by AI), a second workflow, TicketFlow, takes over. It calls the pump operator and waits on another task token. Its 48-hour heartbeat timeout is our escalation to the Sarpanch, with no extra code.
The complaint loop.


A missed call, at any hour

A missed call is rejected, so it costs the family nothing, and JalSakshi calls straight back. Who the caller is decides the menu: a registered family gets the complaint menu, the pump operator gets their open complaint, and a new number gets the consent call.
A missed call, step by step. A spoken complaint goes to S3, Sarvam transcribes it, and Bedrock reads it.


Cedar decides who may be called

Calling people is serious, so permissions are Cedar policies, not if-statements scattered in code. Two of the real rules:
cedar
// A family that declined or withdrew is never called again, unless it gives a missed call itself.
@id("no-calls-after-withdrawal")
forbid (principal, action == Action::"PlaceCall", resource)
when {
["DECLINED", "WITHDRAWN"].contains(resource.consent_status) && !context.caller_initiated
};
// Calls back after missed calls: at most 5 a day per number, so nobody can be made to spam.
@id("callback-limit")
forbid (principal, action == Action::"PlaceCall", resource)
when { context.caller_initiated && context.callbacks_today >= 5 };
A complaint also cannot close without a quorum of families saying yes. Every deny comes back as a plain reason the secretary can read.

Real data from the launch

JalSakshi is live in Kutelabhatha, Durg (Chhattisgarh), with its first users around the water pump near the IIT Bhilai campus. Every number below is read from our DynamoDB table.
33 real phone calls and 13 missed calls between 9 and 11 Oct 2026.

Our favourite record is this one. The operator pressed "fixed" two minutes after the complaint. The family was called back and said no. The complaint reopened by itself, the operator explained the problem by voice, fixed it for real, and only then did the family confirm.
Without the families' call-back, this complaint would have closed at 01:43 with no water.

When everything goes right, it is fast: on 11 Oct a dirty-water complaint went from opened to "closed, families confirmed" in 2 minutes 11 seconds.

AI that advises, never decides

We used Amazon Bedrock where language is messy:
  • Claude Haiku 5.5 (falling back to Haiku 4.5, then Nova 2 Lite, then a template) reads spoken complaints after Sarvam transcribes them, extracts a family's name and mohalla, reads a photo of a handwritten village register (vision), and writes a short overview of each complaint for the secretary.
  • Strands Agents wraps the voice-note reader, with the same model chain.
For "what should the Panchayat do next?", we tried decision models: Jev (TypeSafe), then OpenAI Decisions, then fixed rules if neither is at least 40% sure. They receive only codes and counts, never names, numbers or anyone's words.
Our 8-case test. Small, but it showed both models agree on the clear cases and split on the unclear ones.

The rule we never break: a model never changes a status. Statuses, call permissions and closing a complaint are code and Cedar. The secretary presses every button.
What fought back
  • "Invalid Answer XML" on our first real consent call. One empty attribute ( finishOnKey="" ) in the call-control XML. It had passed every test, because our first successful call had used a throwaway prototype. Lesson: test the real path with a real phone early.
  • Haiku 5.5 rejects  temperature . The same Converse call that worked for Haiku 4.5 and Nova failed with a ValidationException. And because 5.5 reasons before answering, a small  maxTokens  returned no text at all. We now skip temperature for those models and give them room to think.
  • An IAM permission killed a check-in run at 1 am. One Lambda inside the Map state couldn't read a parameter from SSM. The AccessDenied surfaced only at run time and failed the whole run. Fixed the grant, and made that lookup fail-safe.
  • Two calls at once. The operator's call sometimes arrived while the family's call was still open. A 45-second Wait state before notifying fixed it.
  • Chhattisgarhi. There's no Chhattisgarhi voice model, so our Chhattisgarhi prompts are written in Devanagari and spoken by the Hindi voice. Families choose their language on their first call.
What it costs
About $0.35 a month per village of 50 families, in AWS and AI usage. Phone minutes are extra.]


AWS we used

  • AWS open source: Cedar, AWS CDK, Strands Agents, Powertools for AWS Lambda.
  • AWS services: Step Functions, EventBridge Scheduler and EventBridge, Lambda, API Gateway, DynamoDB, Bedrock, S3, CloudFront, Cognito, Systems Manager Parameter Store, SQS, CloudWatch, X-Ray, IAM, Certificate Manager.

What's next

More Panchayats (each one chooses its call languages), a native speaker's pass over the Chhattisgarhi prompts, and a Gram Sabha sheet that a Panchayat can take straight into its certification meeting.
The moment that closes a complaint: water is back, and the family says so.

If a tap is marked done but runs dry, the families should be the ones who say so. JalSakshi just makes sure someone asks them.
Built by Galaba Vamsi and Varun Rao for WeMakeDevs × AWS Environmental Hacks.


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