All work
Jan 30 to Feb 1, 2026/ Civic tech · Automation/ EU-funded MEDAIGENCY / AI4Purpose/ 🏆 1st place · Student Track

WhatsApp Fire-Reporting

Wildfire reporting over the app people already have open. A stateful WhatsApp bot walks a witness through the four things responders actually need: where, how big, who is at risk, what it looks like, and hands dispatch a structured report with real coordinates on the end of it. It took first place in the Student Track.

Interactive demo

A real reporting conversation, replayed turn by turn

twilio whatsapp · +961 · session f2a91c
Starttrigger word
Location+ geocode
Severityspread + risk
Confirmread back
Dispatchhand off

Session idle · state: START

At a glance

Result
🏆 First place, Student Track
My role
Conversation flow + dispatch backend
Stack
n8n · Twilio · FastAPI

The state machine

Why "yes" means different things at different steps

START trigger word LOCATION → geocode webhook SEVERITY spread + casualties CONFIRM read back DISPATCH report # confidence < 0.6: ask again, do not guess "no": re-open the answers
State is keyed to the phone number, so a reply three minutes later still lands in the right step.

Code

The endpoint my teammates depended on

state/interpret.js the same word, read per state

export function interpret(message, state) {
  const text = message.body.trim().toLowerCase();

  switch (state) {
    case "SEVERITY":
      // here "no" answers "is anyone trapped?"
      return { casualties: text === "no" ? "none" : "reported" };

    case "CONFIRM":
      // here the same "no" means the summary is wrong, reopen it
      return text === "yes" ? { next: "DISPATCH" } : { next: "SEVERITY" };

    default:
      return { next: "LOCATION" };
  }
}
workflows/geocode_webhook.js a stable contract, including failure

export async function handler(req) {
  const { pin, text } = req.body;

  // a shared pin is already coordinates; typed text has to be resolved
  if (pin) return ok(pin.lat, pin.lng, 1.0);

  const hit = await geocode(text, { region: "lb" });

  // never hand dispatch a confident-looking wrong pin
  if (!hit || hit.confidence < 0.6) {
    return { status: "needs_confirmation", query: text };
  }
  return ok(hit.lat, hit.lng, hit.confidence);
}

It became a shared dependency mid-hackathon, so the low-confidence case had to be an explicit response rather than a guess.

Inside the repo

Structure

  • A1.jsonexported n8n conversation workflow
  • app.pyFastAPI “Emergency Dispatch Coordinator”
  • server.jswebhook relay
  • whatsapp.htmlconversation UI mock for the demo
  • custom_dashboard.htmlresponder dashboard
  • fire_stations.jsonstation locations for dispatch
  • dispatch_plan.jsongenerated dispatch output
  • requirements.txt / package.jsonpython + node deps

Skills, in context

Where each one actually showed up

n8n Every inbound message is its own run, so multi-turn state needed an explicit session store keyed on the sender.
Twilio WhatsApp transport: a shared pin and a typed address arrive shaped differently and had to normalise.
Webhooks The geocoding endpoint above, including its defined low-confidence response.
Conversation design Location asked first: an abandoned report is still useful with coordinates, useless without.
Next project Driver-Aggressiveness Index