<?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: Davide De Sio</title>
    <description>The latest articles on DEV Community by Davide De Sio (@ddesio).</description>
    <link>https://dev.to/ddesio</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1402618%2F7b0b959e-0a8c-43b6-ad1d-90ae70727bb6.png</url>
      <title>DEV Community: Davide De Sio</title>
      <link>https://dev.to/ddesio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ddesio"/>
    <language>en</language>
    <item>
      <title>Omnichannel AI agents: sharing long-term memory between a voice and a chat agent with Amazon Bedrock AgentCore Memory, Strands and Amplify Gen 2</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Mon, 21 Sep 2026 07:10:29 +0000</pubDate>
      <link>https://dev.to/aws-builders/omnichannel-ai-agents-sharing-long-term-memory-between-a-voice-and-a-chat-agent-with-amazon-4003</link>
      <guid>https://dev.to/aws-builders/omnichannel-ai-agents-sharing-long-term-memory-between-a-voice-and-a-chat-agent-with-amazon-4003</guid>
      <description>&lt;h1&gt;
  
  
  🏃 TL;DR
&lt;/h1&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/aws-builders/your-database-is-an-ai-tool-semantic-search-with-amazon-dynamodb-vector-search-46ff"&gt;first article&lt;/a&gt; I built semantic product search on &lt;code&gt;Amazon DynamoDB&lt;/code&gt; Vector Search and gave that capability to an AI agent as a tool. In the &lt;a href="https://dev.to/aws-builders/deploying-a-real-time-voice-agent-with-agentcore-runtime-and-amplify-gen-2-45bl"&gt;second one&lt;/a&gt; I deployed the voice agent to &lt;code&gt;Amazon Bedrock AgentCore Runtime&lt;/code&gt;, inside the same &lt;code&gt;Amplify Gen 2&lt;/code&gt; backend.&lt;/p&gt;

&lt;p&gt;So now I have two agents that do the same job, help a user shop, through two different channels: a &lt;strong&gt;text chat&lt;/strong&gt; (Amplify AI Kit) and a &lt;strong&gt;voice agent&lt;/strong&gt; (Strands &lt;code&gt;BidiAgent&lt;/code&gt; using &lt;code&gt;Amazon Nova Sonic&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;They work, but they are two strangers: tell the voice agent you are into ultralight camping gear, then open the chat and ask for a recommendation: it has no idea who you are. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each conversation starts from zero, and this article is about fixing that: giving both agents a shared memory so a preference learned in one channel shows up in the other.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;That is what turns "a few agents" into an omnichannel experience.&lt;/p&gt;

&lt;p&gt;I'll use &lt;strong&gt;Amazon Bedrock AgentCore Memory&lt;/strong&gt;, and the key idea is deciding what the memory is keyed to. Let me walk through it.&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;
        dynamodbvector
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An example application using Dynamo Db Vector Search as an AI tool
    &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;Semantic Search for Text, Voice &amp;amp; Omnichannel Agents on AWS: Amazon DynamoDB Vector Search, Amplify Gen 2, Amazon AgentCore &amp;amp; Amazon Nova Sonic&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Companion posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/your-database-is-an-ai-tool-semantic-search-with-amazon-dynamodb-vector-search-46ff" rel="nofollow"&gt;Your database is an AI tool: semantic search with Amazon DynamoDB Vector Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/deploying-a-real-time-voice-agent-with-agentcore-runtime-and-amplify-gen-2-45bl" rel="nofollow"&gt;Deploying a real-time voice agent with AgentCore Runtime and Amplify Gen 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Omnichannel agents: sharing memory across a voice and a text agent with Amazon Bedrock AgentCore Memory (see &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector/./blog/blog-3.md" rel="noopener noreferrer"&gt;&lt;code&gt;blog/blog-3.md&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;A sample application that shows how to use &lt;strong&gt;Amazon DynamoDB native vector search&lt;/strong&gt; to build semantic search over application data, how to expose that capability to AI agents as a tool, how to deploy a real-time voice agent for it on &lt;strong&gt;Amazon Bedrock AgentCore Runtime&lt;/strong&gt;, and how to give a voice agent and a text agent a &lt;strong&gt;shared memory&lt;/strong&gt; so they behave as one omnichannel assistant — all inside a single AWS Amplify Gen 2 backend.&lt;/p&gt;
&lt;p&gt;It demonstrates the same…&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/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  🧠 Two kinds of memory
&lt;/h2&gt;

&lt;p&gt;Before wiring anything, it helps to separate two things that both get called "memory".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short-term memory&lt;/strong&gt; is the current conversation. The turns you and the agent just exchanged, so it can follow "make it cheaper" without asking cheaper than what. It lives and dies with the session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-term memory&lt;/strong&gt; is what survives across sessions. Not the raw transcript, but distilled knowledge: "this customer likes ultralight gear", "their budget is around 150 euros", "they camp in winter". This is the part that makes an omnichannel experience possible, because it outlives any single conversation and any single channel.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Amazon Bedrock AgentCore Memory&lt;/code&gt; gives me both. I write raw events (short-term), and it runs extraction strategies in the background that distill those events into long-term records. I get to pick which strategies run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User Preference&lt;/strong&gt; extracts subjective likes and dislikes (&lt;code&gt;prefers ultralight gear&lt;/code&gt;, &lt;code&gt;budget around 150 euros&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic&lt;/strong&gt; extracts objective facts (&lt;code&gt;bought a DayHike 25L Pack&lt;/code&gt;, &lt;code&gt;camps in winter&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a Summarization strategy, but for a shopping assistant the preferences and facts are what matter, so I'll use those two.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I only really need the long-term half
&lt;/h3&gt;

&lt;p&gt;Here's a nice consequence of the stack I'm already on: &lt;strong&gt;short-term memory is basically handled for me on both channels, so the part I actually need to add is the long-term, cross-channel one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the chat side, the Amplify AI Kit already persists the conversation to &lt;code&gt;Amazon DynamoDB&lt;/code&gt; and replays it on every turn. Following "make it cheaper" within a conversation just works, the AI Kit stores and reloads the message history automatically, no AgentCore short-term events required.&lt;/p&gt;

&lt;p&gt;On the voice side, the &lt;code&gt;BidiAgent&lt;/code&gt; keeps the live session context inside the open bidirectional stream with Nova Sonic. Within a single voice session the model already has everything it just heard, so per-session short-term memory isn't something the agent needs me to add either.&lt;/p&gt;

&lt;p&gt;So the gap that AgentCore Memory fills here is specifically the &lt;strong&gt;long-term, cross-session, cross-channel&lt;/strong&gt; one: the distilled preferences and facts that must outlive any single conversation and travel between the two agents. That's the piece neither the AI Kit nor the &lt;code&gt;BidiAgent&lt;/code&gt; gives me on its own, and it's what the rest of this article wires up.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔑 The one decision that matters: what is memory keyed to?
&lt;/h2&gt;

&lt;p&gt;Here is the insight that makes or breaks the whole thing.&lt;/p&gt;

&lt;p&gt;AgentCore Memory organizes records under an &lt;strong&gt;&lt;code&gt;actorId&lt;/code&gt;&lt;/strong&gt; and a &lt;strong&gt;&lt;code&gt;sessionId&lt;/code&gt;&lt;/strong&gt;. The natural temptation is to let each agent use its own runtime session as the identity. If you do that, the voice agent remembers voice sessions and the chat agent remembers chat sessions, and they never meet. You would have two separate memories that happen to use the same service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For omnichannel, the memory has to be keyed to the &lt;em&gt;user&lt;/em&gt;, not to the runtime session or the channel.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My app already has a stable per-user identifier: the &lt;code&gt;Amazon Cognito&lt;/code&gt; &lt;strong&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/strong&gt;. The same user signs into the chat and the voice agent, so if both agents use the Cognito &lt;code&gt;sub&lt;/code&gt; as the &lt;code&gt;actorId&lt;/code&gt;, they read and write the same records. A preference the voice agent stored under &lt;code&gt;sub=a2751...&lt;/code&gt; is exactly what the chat agent retrieves under &lt;code&gt;sub=a2751...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the design is one memory store, two agents, keyed by the Cognito &lt;code&gt;sub&lt;/code&gt;:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkm4pcgzmaasbemlkgjnc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkm4pcgzmaasbemlkgjnc.png" alt=" " width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🏗️ Step 1: create the memory in the Amplify backend
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;Amplify Gen 2&lt;/code&gt; is &lt;code&gt;CDK&lt;/code&gt; under the hood, the memory store is just another construct in &lt;code&gt;backend.ts&lt;/code&gt;, next to the data, auth, and the voice runtime from the previous article. I use the L1 &lt;code&gt;CfnMemory&lt;/code&gt;: for a service this new I want what I write to map one-to-one onto the &lt;code&gt;CloudFormation&lt;/code&gt; resource, with no abstraction deciding things for me.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;CfnMemory&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="s2"&gt;aws-cdk-lib/aws-bedrockagentcore&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;agentMemory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CfnMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ShoppingAgentMemory&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shoppingAgentMemory&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// Raw short-term events are kept for 30 days before expiring.&lt;/span&gt;
  &lt;span class="na"&gt;eventExpiryDuration&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="na"&gt;memoryExecutionRoleArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;memoryExecutionRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;memoryStrategies&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;userPreferenceMemoryStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PreferenceLearner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/preferences/{actorId}/&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;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;semanticMemoryStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FactExtractor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/facts/{actorId}/&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;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;memoryId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;agentMemory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attrMemoryId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two things worth calling out.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;namespaces&lt;/code&gt; use a &lt;code&gt;{actorId}&lt;/code&gt; template. AgentCore substitutes the real &lt;code&gt;actorId&lt;/code&gt; at write and read time, so &lt;code&gt;/preferences/{actorId}/&lt;/code&gt; becomes &lt;code&gt;/preferences/a2751.../&lt;/code&gt; for that user. This is what physically separates one user's memories from another's, using the same key both agents share.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;memoryExecutionRoleArn&lt;/code&gt; matters because long-term extraction runs &lt;code&gt;Amazon Bedrock&lt;/code&gt; models &lt;strong&gt;on your behalf&lt;/strong&gt;. The built-in strategies read your raw events and call a model to distill them, so the memory needs a role allowed to invoke Bedrock:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;memoryExecutionRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AgentMemoryRole&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;assumedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ServicePrincipal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock-agentcore.amazonaws.com&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;conditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws:SourceAccount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&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="nx"&gt;memoryExecutionRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock:InvokeModel&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;arn:aws:bedrock:*::foundation-model/*&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then both the voice runtime role and the chat handler role get read/write access to the memory (&lt;code&gt;CreateEvent&lt;/code&gt;, &lt;code&gt;RetrieveMemoryRecords&lt;/code&gt;, &lt;code&gt;ListMemoryRecords&lt;/code&gt;, and friends) on &lt;code&gt;agentMemory.attrMemoryArn&lt;/code&gt;, and both get &lt;code&gt;MEMORY_ID&lt;/code&gt; as an environment variable. Same store, same permissions, two consumers.&lt;/p&gt;
&lt;h2&gt;
  
  
  🎙️ Step 2: the voice agent
&lt;/h2&gt;

&lt;p&gt;The voice agent is a Strands &lt;code&gt;BidiAgent&lt;/code&gt;. The first job is to make sure it keys memory to the Cognito &lt;code&gt;sub&lt;/code&gt;, not to the runtime session.&lt;/p&gt;

&lt;p&gt;The frontend already authenticates the WebSocket to AgentCore with the user's Cognito token (that was the whole point of the JWT authorizer in the previous article). The token &lt;em&gt;is&lt;/em&gt; a JWT, and the &lt;code&gt;sub&lt;/code&gt; is right there inside it. So I resolve the &lt;code&gt;actorId&lt;/code&gt; from the connection:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve_actor_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The memory actorId is the Cognito `sub`, shared with the chat agent.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;
    &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&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;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;authorization&lt;/span&gt;&lt;span class="sh"&gt;"&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;auth&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="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&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;auth&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="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
        &lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_decode_jwt_sub&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="c1"&gt;# base64url-decode the JWT payload, read `sub`
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sub&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;sub&lt;/span&gt;
    &lt;span class="n"&gt;custom&lt;/span&gt; &lt;span class="o"&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;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;x-amzn-bedrock-agentcore-runtime-custom-actorid&lt;/span&gt;&lt;span class="sh"&gt;"&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;custom&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;custom&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anonymous&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now, a browser can't set arbitrary headers on a WebSocket handshake, and AgentCore only forwards headers to your container if they are on an &lt;strong&gt;allowlist&lt;/strong&gt;. So I let the frontend pass the &lt;code&gt;sub&lt;/code&gt; as a custom runtime header via a query parameter, and I allowlist it on the runtime:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// backend.ts — on the CfnRuntime&lt;/span&gt;
&lt;span class="nx"&gt;requestHeaderConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;requestHeaderAllowlist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Amzn-Bedrock-AgentCore-Runtime-Custom-actorId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// frontend — the Cognito sub, passed as a custom runtime header&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actorId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;idToken&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;sub&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;amp;X-Amzn-Bedrock-AgentCore-Runtime-Custom-actorId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actorId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Values sent as &lt;code&gt;X-Amzn-Bedrock-AgentCore-Runtime-Custom-*&lt;/code&gt; are delivered to the container as headers of the same name, and &lt;code&gt;resolve_actor_id&lt;/code&gt; reads it. Now the voice agent and the chat agent agree on who the user is.&lt;/p&gt;
&lt;h3&gt;
  
  
  Writing memory: the native session manager
&lt;/h3&gt;

&lt;p&gt;For persistence, Strands and &lt;code&gt;bedrock-agentcore&lt;/code&gt; offer a native integration: a session manager that transparently writes every turn to AgentCore Memory. I hand it the memory id, the session id, and, crucially, the shared &lt;code&gt;actorId&lt;/code&gt;:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bedrock_agentcore.memory.integrations.strands.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentCoreMemoryConfig&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bedrock_agentcore.memory.integrations.strands.session_manager&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;AgentCoreMemorySessionManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;memory_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentCoreMemoryConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;memory_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MEMORY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# unique per conversation
&lt;/span&gt;    &lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# the Cognito sub — shared across channels
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session_manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentCoreMemorySessionManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;agentcore_memory_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;memory_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MEMORY_REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;voice_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BidiAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sonic_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop_conversation&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;build_system_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# more on this in a second
&lt;/span&gt;    &lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_manager&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;With the session manager attached, every turn of the conversation gets written to the memory store, and the background strategies distill preferences and facts from those turns. Writing is fully handled for me.&lt;/p&gt;
&lt;h3&gt;
  
  
  Reading memory: do it yourself
&lt;/h3&gt;

&lt;p&gt;Reading back is where it gets interesting, and where the two agents end up looking different.&lt;/p&gt;

&lt;p&gt;The native session manager's automatic retrieval applies to the standard &lt;code&gt;Agent&lt;/code&gt;, not to the streaming &lt;code&gt;BidiAgent&lt;/code&gt; that Nova Sonic uses. For a real-time voice agent, retrieval is not wired into the loop for you. So I retrieve the long-term records myself, at the start of the session, and inject them into the system prompt:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve_memories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch this user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s long-term preferences and facts, keyed by Cognito sub.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;namespaces&lt;/span&gt; &lt;span class="o"&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;/preferences/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&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;/facts/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&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;context&lt;/span&gt; &lt;span class="o"&gt;=&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;namespace&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;namespaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve_memories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;memory_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MEMORY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;namespace_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user preferences, interests and facts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&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;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&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;content&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="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;text&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="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;if&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;context&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="n"&gt;text&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;context&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_system_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retrieve_memories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor_id&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;context&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;SYSTEM_PROMPT&lt;/span&gt;
    &lt;span class="n"&gt;remembered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&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;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&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;return &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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Here is what you remember about this customer from previous &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversations, across both voice and chat. Use it to personalize your &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;suggestions, and confirm before assuming it still applies:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&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;remembered&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So on the voice side: &lt;strong&gt;the session manager writes, and I read.&lt;/strong&gt; The write is native, the read is manual.&lt;/p&gt;
&lt;h2&gt;
  
  
  💬 Step 3: the text chat agent
&lt;/h2&gt;

&lt;p&gt;The chat agent runs on the Amplify AI Kit, through a custom conversation handler. There is no magic session manager here either, so the pattern is symmetric with the voice agent's read path: I do the retrieve-and-inject myself, plus I persist the turn.&lt;/p&gt;

&lt;p&gt;The AI Kit passes the user's Cognito token on the conversation event headers, so I get the same &lt;code&gt;sub&lt;/code&gt; the voice agent uses:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveActorId&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;ConversationTurnEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;auth&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;"&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;decodeJwtSub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// same base64url-decode → `sub`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then the handler wraps the default AI Kit handler. Before the model runs, it retrieves the same namespaces and prepends what it finds to the system prompt. After, it writes the user's turn so the strategies can extract from it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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="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;ConversationTurnEvent&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;actorId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolveActorId&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;memoryClient&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;MEMORY_ID&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;actorId&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;userText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getLatestUserText&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;facts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nf"&gt;retrieveMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/preferences&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userText&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nf"&gt;retrieveMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/facts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userText&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;preamble&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildMemoryPreamble&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;facts&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;preamble&lt;/span&gt;&lt;span class="p"&gt;)&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;modelConfiguration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;systemPrompt&lt;/span&gt; &lt;span class="o"&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;preamble&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n&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;modelConfiguration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;systemPrompt&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userText&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;persistUserTurn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actorId&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;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userText&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;return&lt;/span&gt; &lt;span class="nf"&gt;handleConversationTurnEvent&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same store, same &lt;code&gt;actorId&lt;/code&gt;, same namespaces. The only difference from the voice agent is that here I also write manually (&lt;code&gt;persistUserTurn&lt;/code&gt; calls &lt;code&gt;CreateEvent&lt;/code&gt;), because there is no session manager doing it for me.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔀 Two integration styles, one memory
&lt;/h2&gt;

&lt;p&gt;This is the part I find genuinely interesting. The two agents talk to the &lt;em&gt;same&lt;/em&gt; memory but integrate with it differently, and that is not a mistake, it's the reality of working across two runtimes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Voice agent (Strands BidiAgent)&lt;/th&gt;
&lt;th&gt;Chat agent (Amplify AI Kit)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Write&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native session manager&lt;/td&gt;
&lt;td&gt;Manual &lt;code&gt;CreateEvent&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Read&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual retrieve + inject into system prompt&lt;/td&gt;
&lt;td&gt;Manual retrieve + inject into system prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Identity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cognito &lt;code&gt;sub&lt;/code&gt; from JWT / custom header&lt;/td&gt;
&lt;td&gt;Cognito &lt;code&gt;sub&lt;/code&gt; from JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The takeaway: &lt;strong&gt;omnichannel memory is not about a single SDK that does everything for you. It's about agreeing on the key (the user identity) and the namespaces.&lt;/strong&gt; Once both agents agree that memory is keyed to the Cognito &lt;code&gt;sub&lt;/code&gt; and lives under &lt;code&gt;/preferences/{actorId}/&lt;/code&gt; and &lt;code&gt;/facts/{actorId}/&lt;/code&gt;, the plumbing on each side can differ. The memory is the contract; the integration is per-runtime.&lt;/p&gt;
&lt;h2&gt;
  
  
  🛣️ A road I deliberately didn't take
&lt;/h2&gt;

&lt;p&gt;There was another perfectly valid way to do this, and it's worth naming.&lt;/p&gt;

&lt;p&gt;Instead of wiring each agent to AgentCore Memory through its own runtime integration, I could have built a &lt;strong&gt;single "memory" tool&lt;/strong&gt;, a small function that reads and writes AgentCore Memory, and handed that same tool to every agent, exactly like &lt;code&gt;searchProducts&lt;/code&gt; is shared today. Every agent would then remember and recall by calling the tool, the integration would be identical everywhere, and a third or fourth channel would just get the same tool. That approach is clean, uniform, and it's probably what I'd reach for if I had five channels instead of two.&lt;/p&gt;

&lt;p&gt;I chose the other path on purpose: I wanted to explore the &lt;strong&gt;native integration options&lt;/strong&gt; each runtime offers, the Strands session manager on the voice side, and the Amplify AI Kit conversation handler on the chat side, and see how memory fits into each one's grain rather than bolting a uniform tool on top. That's also what surfaced the interesting asymmetry above (native write, manual read for &lt;code&gt;BidiAgent&lt;/code&gt;), which the shared-tool approach would have hidden.&lt;/p&gt;

&lt;p&gt;But the difference between the two isn't just uniformity, it's &lt;strong&gt;who decides when memory is used&lt;/strong&gt;, and that's the part I find most important.&lt;/p&gt;

&lt;p&gt;With a memory &lt;strong&gt;tool&lt;/strong&gt;, recall is &lt;em&gt;agentic&lt;/em&gt;: the memory is one more tool in the agent's belt, and the LLM decides, turn by turn, whether to call it. That's flexible (the agent can choose to look something up only when it seems relevant) but it's also non-deterministic. The model might not call the tool when you'd want it to, so the user says "give me options" and the agent, having decided it didn't need memory this turn, answers as if it knows nothing about them. You're trusting the model's judgment about when to remember.&lt;/p&gt;

&lt;p&gt;With the &lt;strong&gt;native&lt;/strong&gt; integration I used here, recall is &lt;em&gt;deterministic&lt;/em&gt;. I retrieve the user's preferences and inject them into the system prompt at the start of every conversation, unconditionally. The model doesn't get a vote on whether to be aware of them; the context is simply always there. For a shopping assistant that should feel like it &lt;em&gt;knows&lt;/em&gt; the returning customer, "always aware" is the behavior I want, not "aware if the model felt like calling a tool".&lt;/p&gt;

&lt;p&gt;So the trade-off is: a memory tool gives the LLM control and flexibility over recall; native injection gives &lt;em&gt;you&lt;/em&gt; control and guarantees the context is present. Neither is universally right. Agentic recall shines when memory is large and lookups should be selective; deterministic injection shines when a small, high-value profile should shape every single response.&lt;/p&gt;

&lt;p&gt;So read this article as one of two good options. If you want maximum uniformity across many agents and you're comfortable letting the model decide when to recall, a shared memory tool is a great choice. If you want the context guaranteed on every turn and you want to understand how memory plugs into Strands and Amplify Gen 2 natively, this is that exploration. Either way, the design principle that matters, keying memory to the user, is the same.&lt;/p&gt;
&lt;h2&gt;
  
  
  ✅ Does it actually cross channels?
&lt;/h2&gt;

&lt;p&gt;The test that matters is the bidirectional one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chat, then voice.&lt;/strong&gt; In the text chat I say I'm shopping for camping and I pick a DayHike 25L Pack. A minute later (long-term extraction is asynchronous, it takes a moment), I open the voice agent and ask, in Italian, what it recommends for me. It brings up camping and the pack, without me repeating anything. It read what the chat agent wrote.&lt;/p&gt;

&lt;p&gt;Here is me asking via chat articles for un upcoming hiking in October in Iceland: the agent suggested me some useful ones.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmv94ae25tq2rcy4oet6m.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmv94ae25tq2rcy4oet6m.png" alt=" " width="800" height="673"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that I've made a call to the voice agent, asking more information about those article. I've never mentioned Iceland again, thus confirming it got this information from the memory (also I have logs!).&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fljugni4pa5vi8jd4nfz1.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fljugni4pa5vi8jd4nfz1.png" alt=" " width="800" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice, then chat.&lt;/strong&gt; The reverse works the same way. A preference spoken to the voice agent surfaces in the next chat turn.&lt;/p&gt;

&lt;p&gt;One thing to keep in mind when you try this: long-term memory is extracted &lt;strong&gt;asynchronously&lt;/strong&gt;. Right after a turn, the raw event exists but the distilled preference might not yet, so a retrieve one second later can come back empty. Give the extraction a moment. That is the nature of long-term memory: it's the slow, considered kind, not the immediate transcript.&lt;/p&gt;
&lt;h2&gt;
  
  
  💸 A note on cost
&lt;/h2&gt;

&lt;p&gt;AgentCore Memory is &lt;strong&gt;serverless and consumption-based&lt;/strong&gt;, there is no fixed monthly fee just for having a memory store. You pay on three axes: short-term events written, long-term records stored, and retrieval calls. For a demo like this it rounds to cents.&lt;/p&gt;

&lt;p&gt;The nice part is that the rest of the stack is the same kind of thing. The AgentCore &lt;strong&gt;Runtime&lt;/strong&gt; (in the serverless microVM mode we use) bills CPU and memory only while a session is running, I/O wait is free, so with no one talking to it there is effectively no idle compute charge. The &lt;strong&gt;ECR&lt;/strong&gt; image is just storage, a few cents per month. Left alone, this whole stack costs almost nothing; you pay when someone actually uses it.&lt;/p&gt;
&lt;h2&gt;
  
  
  📱 Next step: add a WhatsApp channel with AWS End User Messaging
&lt;/h2&gt;

&lt;p&gt;Here is where the design pays off. Once memory is keyed to the user and not to the channel, &lt;strong&gt;adding a third channel is mostly plumbing.&lt;/strong&gt; The memory doesn't change at all.&lt;/p&gt;

&lt;p&gt;Imagine a WhatsApp channel using &lt;strong&gt;AWS End User Messaging&lt;/strong&gt; (Social). The shape would be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A user messages your WhatsApp business number. &lt;strong&gt;AWS End User Messaging&lt;/strong&gt; receives the inbound message and publishes it (via &lt;code&gt;Amazon SNS&lt;/code&gt;) to a Lambda.&lt;/li&gt;
&lt;li&gt;The Lambda is the third agent. It runs the same shopping logic, calling the same &lt;code&gt;searchProducts&lt;/code&gt; tool the other two channels use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The identity question is the whole game again.&lt;/strong&gt; WhatsApp identifies the user by phone number, not by a Cognito &lt;code&gt;sub&lt;/code&gt;. So you need a mapping from phone number to your app's user identity, for example a small &lt;code&gt;DynamoDB&lt;/code&gt; table populated during an opt-in or account-linking step. Once you resolve the phone number to the Cognito &lt;code&gt;sub&lt;/code&gt;, you set that as the &lt;code&gt;actorId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;From there it's the pattern you've already seen twice: retrieve &lt;code&gt;/preferences/{actorId}/&lt;/code&gt; and &lt;code&gt;/facts/{actorId}/&lt;/code&gt;, inject them into the prompt, generate a reply, send it back through &lt;strong&gt;AWS End User Messaging&lt;/strong&gt;, and write the turn to memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The user starts on WhatsApp on the train, continues by voice at home, finishes in the web chat, and the assistant remembers throughout. No channel owns the memory. The &lt;strong&gt;user&lt;/strong&gt; owns the memory, and every channel is just a different door into the same context.&lt;/p&gt;

&lt;p&gt;That mapping step (phone number to user identity) is the only real new work. Everything else, the memory store, the namespaces, the retrieve-and-inject pattern, is already built. That is the point of keying memory to the user: &lt;strong&gt;new channels are additive, not a rewrite.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🧠 What I take away from this project
&lt;/h2&gt;

&lt;p&gt;Three things stood out building this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity is the design.&lt;/strong&gt; The single most important decision wasn't which memory strategy to use or how to call the SDK. It was keying memory to the Cognito &lt;code&gt;sub&lt;/code&gt; instead of the runtime session. Get that right and omnichannel falls out almost for free. Get it wrong and you have two agents with amnesia and no amount of SDK cleverness fixes it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One memory, many integrations.&lt;/strong&gt; The voice agent and the chat agent integrate with AgentCore Memory differently, one uses a native session manager to write, the other writes manually, and both retrieve and inject by hand. That asymmetry is fine. The memory is the shared contract; how each runtime reads and writes it is a local detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's all one backend, still.&lt;/strong&gt; The memory store, its IAM, its wiring into both the voice runtime and the chat handler, are all just &lt;code&gt;CDK&lt;/code&gt; constructs sitting next to the data and auth. Adding cross-channel memory didn't mean a new system to operate. It meant a few more constructs in the same &lt;code&gt;npx ampx sandbox&lt;/code&gt; deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your &lt;code&gt;Amazon DynamoDB&lt;/code&gt; database was an AI tool. The agent that talks to it became serverless. And now, whichever channel you reach for, it's the same assistant, and it remembers you.&lt;/strong&gt;&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;
        dynamodbvector
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An example application using Dynamo Db Vector Search as an AI tool
    &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;Semantic Search for Text, Voice &amp;amp; Omnichannel Agents on AWS: Amazon DynamoDB Vector Search, Amplify Gen 2, Amazon AgentCore &amp;amp; Amazon Nova Sonic&lt;/h1&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Companion posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/your-database-is-an-ai-tool-semantic-search-with-amazon-dynamodb-vector-search-46ff" rel="nofollow"&gt;Your database is an AI tool: semantic search with Amazon DynamoDB Vector Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/deploying-a-real-time-voice-agent-with-agentcore-runtime-and-amplify-gen-2-45bl" rel="nofollow"&gt;Deploying a real-time voice agent with AgentCore Runtime and Amplify Gen 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Omnichannel agents: sharing memory across a voice and a text agent with Amazon Bedrock AgentCore Memory (see &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector/./blog/blog-3.md" rel="noopener noreferrer"&gt;&lt;code&gt;blog/blog-3.md&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;A sample application that shows how to use &lt;strong&gt;Amazon DynamoDB native vector search&lt;/strong&gt; to build semantic search over application data, how to expose that capability to AI agents as a tool, how to deploy a real-time voice agent for it on &lt;strong&gt;Amazon Bedrock AgentCore Runtime&lt;/strong&gt;, and how to give a voice agent and a text agent a &lt;strong&gt;shared memory&lt;/strong&gt; so they behave as one omnichannel assistant — all inside a single AWS Amplify Gen 2 backend.&lt;/p&gt;

&lt;p&gt;It demonstrates the same…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  🙋 Who am I
&lt;/h2&gt;

&lt;p&gt;I'm &lt;a href="https://www.linkedin.com/in/desiodavide" rel="noopener noreferrer"&gt;D. De Sio&lt;/a&gt; and I work as a Head of Software Engineering in &lt;a href="https://eleva.it/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;br&gt;
As of September 2026, I’m an &lt;a href="https://www.credly.com/badges/9929fdf2-7a3d-4013-9de6-57c80e4920b9/public_url" rel="noopener noreferrer"&gt;AWS Certified Solution Architect Professional&lt;/a&gt; and &lt;a href="https://www.credly.com/badges/8c5a1487-191b-429e-8c2d-7cee43bf316b/public_url" rel="noopener noreferrer"&gt;AWS Certified DevOps Engineer Professional&lt;/a&gt;, but also a &lt;a href="https://www.linkedin.com/company/aws-user-group-pavia/" rel="noopener noreferrer"&gt;User Group Leader (in Pavia)&lt;/a&gt;, an &lt;strong&gt;AWS Community Builder&lt;/strong&gt; and, last but not least, a #serverless enthusiast.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fang7p9mz81sxnhjazutq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fang7p9mz81sxnhjazutq.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I just shared long-term memory with my AI agents. I'm sure they'll remember I'm the good guy when Skynet goes live.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎉 AWS Community Day Italy
&lt;/h2&gt;

&lt;p&gt;The full agenda for &lt;a href="https://www.awscommunityday.it/" rel="noopener noreferrer"&gt;AWS Community Day Italy&lt;/a&gt; is out!&lt;/p&gt;

&lt;p&gt;If you'd love to hear what the community has been working on, what they've learned, and what they want to share, come join us in Rome on October 2nd. &lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>agentcore</category>
      <category>memory</category>
    </item>
    <item>
      <title>Deploying a real-time voice agent with AgentCore Runtime and Amplify Gen 2</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Wed, 16 Sep 2026 20:58:25 +0000</pubDate>
      <link>https://dev.to/aws-builders/deploying-a-real-time-voice-agent-with-agentcore-runtime-and-amplify-gen-2-45bl</link>
      <guid>https://dev.to/aws-builders/deploying-a-real-time-voice-agent-with-agentcore-runtime-and-amplify-gen-2-45bl</guid>
      <description>&lt;h1&gt;
  
  
  🏃 TL;DR
&lt;/h1&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/aws-builders/your-database-is-an-ai-tool-semantic-search-with-amazon-dynamodb-vector-search-46ff"&gt;previous article&lt;/a&gt; I built a semantic product search on top of &lt;code&gt;Amazon DynamoDB&lt;/code&gt; Vector Search, and then gave that capability to an AI agent as a tool. One of the things I explored at the end was a voice agent: a Strands &lt;code&gt;BidiAgent&lt;/code&gt; powered by &lt;code&gt;Amazon Nova Sonic&lt;/code&gt; that could search the catalog by voice.&lt;/p&gt;

&lt;p&gt;That voice agent ran locally. A Python server on my laptop, a WebSocket, my microphone. Great for a demo, but it lives on my machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So the question for this article is: how do I actually deploy it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I want the voice agent to run on AWS, I want it authenticated with the same users my app already has, and I want it to be part of the same &lt;code&gt;Amplify Gen 2&lt;/code&gt; backend as everything else. No separate project, no separate auth, no separate deploy command.&lt;/p&gt;

&lt;p&gt;It turns out this fits together really nicely with &lt;strong&gt;Amazon Bedrock AgentCore Runtime&lt;/strong&gt;. Let me walk through it.&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;
        dynamodbvector
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An example application using Dynamo Db Vector Search as an AI tool
    &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;Semantic Search for Text, Voice &amp;amp; Omnichannel Agents on AWS: Amazon DynamoDB Vector Search, Amplify Gen 2, Amazon AgentCore &amp;amp; Amazon Nova Sonic&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Companion posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/your-database-is-an-ai-tool-semantic-search-with-amazon-dynamodb-vector-search-46ff" rel="nofollow"&gt;Your database is an AI tool: semantic search with Amazon DynamoDB Vector Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/deploying-a-real-time-voice-agent-with-agentcore-runtime-and-amplify-gen-2-45bl" rel="nofollow"&gt;Deploying a real-time voice agent with AgentCore Runtime and Amplify Gen 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Omnichannel agents: sharing memory across a voice and a text agent with Amazon Bedrock AgentCore Memory (see &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector/./blog/blog-3.md" rel="noopener noreferrer"&gt;&lt;code&gt;blog/blog-3.md&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;A sample application that shows how to use &lt;strong&gt;Amazon DynamoDB native vector search&lt;/strong&gt; to build semantic search over application data, how to expose that capability to AI agents as a tool, how to deploy a real-time voice agent for it on &lt;strong&gt;Amazon Bedrock AgentCore Runtime&lt;/strong&gt;, and how to give a voice agent and a text agent a &lt;strong&gt;shared memory&lt;/strong&gt; so they behave as one omnichannel assistant — all inside a single AWS Amplify Gen 2 backend.&lt;/p&gt;
&lt;p&gt;It demonstrates the same…&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/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  🎙️ Where we left off
&lt;/h2&gt;

&lt;p&gt;Quick recap of the voice agent: it's a Python app: a FastAPI server that exposes a WebSocket on &lt;code&gt;/ws&lt;/code&gt;, and a Strands &lt;code&gt;BidiAgent&lt;/code&gt; wired to &lt;code&gt;Amazon Nova Sonic&lt;/code&gt; with our &lt;code&gt;search_products&lt;/code&gt; tool.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.experimental.bidi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BidiAgent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.experimental.bidi.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BidiNovaSonicModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Search the product catalog using natural language.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Nova Micro parses the query, Titan embeds it,
&lt;/span&gt;    &lt;span class="c1"&gt;# DynamoDB SearchVectors finds the matches, we filter by price.
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;sonic_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BidiNovaSonicModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon.nova-2-sonic-v1:0&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="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.websocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/ws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;voice_chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BidiAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sonic_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_products&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="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&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="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receive_json&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_json&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;Locally I ran this with &lt;code&gt;uvicorn&lt;/code&gt;, the browser connected to &lt;code&gt;ws://127.0.0.1:8080/ws&lt;/code&gt;, and everything worked. &lt;br&gt;
&lt;strong&gt;Now I want the exact same code running on AWS.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🧩 What is Amazon Bedrock AgentCore Runtime, and why it fits
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Amazon Bedrock AgentCore Runtime&lt;/code&gt; is a serverless runtime purpose-built for hosting AI agents. It's framework-agnostic (Strands, LangGraph, CrewAI, whatever) and, importantly for us, it supports &lt;strong&gt;bidirectional streaming over WebSocket&lt;/strong&gt;, which is exactly what a real-time voice agent needs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Amazon Bedrock AgentCore Runtime&lt;/code&gt; contract is simple, you give it a container that listens on &lt;strong&gt;port 8080&lt;/strong&gt; and exposes a WebSocket at &lt;strong&gt;&lt;code&gt;/ws&lt;/code&gt;&lt;/strong&gt;, plus a &lt;code&gt;/ping&lt;/code&gt; health check. That's already how our agent is written. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Amazon Bedrock AgentCore&lt;/code&gt;  handles the rest: session isolation, scaling, authentication, and the public WebSocket endpoint.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the plan is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Package the voice agent as a docker container.&lt;/li&gt;
&lt;li&gt;Deploy it to AgentCore Runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticate it with the Cognito user pool Amplify already created&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Connect the browser to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And because &lt;strong&gt;&lt;code&gt;Amplify Gen 2&lt;/code&gt; is CDK under the hood&lt;/strong&gt;, I can do all of this inside the same &lt;code&gt;amplify/backend.ts&lt;/code&gt; I already have, without a second project or a specific &lt;code&gt;CDK&lt;/code&gt;/&lt;code&gt;CloudFormation&lt;/code&gt;/Terraform project.&lt;/p&gt;
&lt;h2&gt;
  
  
  📦 Step 1: containerize the agent
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Amazon Bedrock AgentCore Runtime&lt;/code&gt; runs &lt;strong&gt;ARM64&lt;/strong&gt; containers. &lt;br&gt;
The Dockerfile is minimal:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; --platform=linux/arm64 python:3.12-slim&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; agent.py .&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; CONTAINER_ENV=true&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "agent.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The only change to the agent itself is binding to &lt;code&gt;0.0.0.0&lt;/code&gt; when running in the container (locally it stayed on &lt;code&gt;127.0.0.1&lt;/code&gt;):&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&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;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CONTAINER_ENV&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;uvicorn&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="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  🏗️ Step 2: deploy it from the Amplify backend
&lt;/h2&gt;

&lt;p&gt;Here's the part I like: again, since &lt;code&gt;Amplify Gen 2&lt;/code&gt; backends are &lt;code&gt;CDK&lt;/code&gt; constructs, I can build the image and create the runtime right in my &lt;code&gt;backend.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, we should build the ARM64 image and push it to &lt;code&gt;Amazon ECR&lt;/code&gt;. The &lt;code&gt;CDK&lt;/code&gt; &lt;code&gt;DockerImageAsset&lt;/code&gt; does all of that during deployment, no manual &lt;code&gt;docker build&lt;/code&gt; or &lt;code&gt;docker push&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ecrAssets&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-ecr-assets&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;voiceImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ecrAssets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DockerImageAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VoiceAgentImage&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;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;voice-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ecrAssets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LINUX_ARM64&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;Then we need an execution role for the runtime: it needs to pull the image, call &lt;code&gt;Amazon Bedrock&lt;/code&gt; (&lt;code&gt;Amazon Nova Sonic 2&lt;/code&gt; for voice, plus &lt;code&gt;Amazon Nova Micro&lt;/code&gt; and &lt;code&gt;Amazon Titan Embeddings&lt;/code&gt; for the search tool), and run &lt;code&gt;SearchVectors&lt;/code&gt; on the DynamoDB table:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;voiceRuntimeRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VoiceAgentRuntimeRole&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;assumedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ServicePrincipal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock-agentcore.amazonaws.com&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;conditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws:SourceAccount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&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="nx"&gt;voiceImage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantPull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceRuntimeRole&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;voiceRuntimeRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ecr:GetAuthorizationToken&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&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="nx"&gt;voiceRuntimeRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock:InvokeModel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock:InvokeModelWithBidirectionalStream&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;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;arn:aws:bedrock:*::foundation-model/amazon.nova-2-sonic-v1:0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;arn:aws:bedrock:*::foundation-model/amazon.titan-embed-text-v2:0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;arn:aws:bedrock:*::foundation-model/amazon.nova-micro-v1:0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`arn:aws:bedrock:*:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:inference-profile/eu.amazon.nova-micro-v1:0`&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="nx"&gt;voiceRuntimeRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dynamodb:SearchVectors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dynamodb:GetItem&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;productsTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableArn&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;productsTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableArn&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/index/*`&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;And finally we need the runtime itself. Here I deliberately reach for the L1 &lt;code&gt;CfnRuntime&lt;/code&gt; construct. Level 1 (L1) constructs map directly one-to-one to raw CloudFormation resources, while Level 2 (L2) constructs provide higher-level, object-oriented abstractions with built-in security best practices and helper methods. For a service this new I prefer the L1: it maps straight onto the &lt;code&gt;CloudFormation&lt;/code&gt; resource, so what I write is exactly what gets deployed, with no abstraction deciding things for me.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;CfnRuntime&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="s2"&gt;aws-cdk-lib/aws-bedrockagentcore&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;voiceRuntime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CfnRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VoiceAgentRuntime&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;agentRuntimeName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;voiceShoppingAgent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;agentRuntimeArtifact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;containerConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;containerUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;voiceImage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageUri&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;networkConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;networkMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUBLIC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;protocolConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HTTP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;roleArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;voiceRuntimeRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;environmentVariables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;CONTAINER_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;productsTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;BEDROCK_REGION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eu-north-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...model ids&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;authorizerConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;customJwtAuthorizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;discoveryUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;allowedClients&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userPoolClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPoolClientId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That &lt;code&gt;protocolConfiguration: "HTTP"&lt;/code&gt; is worth a note. The valid protocol values are &lt;code&gt;HTTP&lt;/code&gt;, &lt;code&gt;A2A&lt;/code&gt;, &lt;code&gt;AGUI&lt;/code&gt; and &lt;code&gt;MCP&lt;/code&gt;, there is no &lt;code&gt;WEBSOCKET&lt;/code&gt; value. Bidirectional streaming over WebSocket runs &lt;strong&gt;on top of the &lt;code&gt;HTTP&lt;/code&gt; server protocol&lt;/strong&gt;: the container exposes &lt;code&gt;/ws&lt;/code&gt;, AgentCore speaks WebSocket to the client, but as far as the runtime configuration is concerned, the protocol is &lt;code&gt;HTTP&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One detail on the execution role: alongside &lt;code&gt;grantPull()&lt;/code&gt; (which covers &lt;code&gt;BatchGetImage&lt;/code&gt; and &lt;code&gt;GetDownloadUrlForLayer&lt;/code&gt; on the repository) the role also needs &lt;code&gt;ecr:GetAuthorizationToken&lt;/code&gt;, and that action requires a &lt;code&gt;"*"&lt;/code&gt; resource. Both are in the role above so the runtime can pull the image.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔐 Step 3: reuse the Amplify Cognito user pool
&lt;/h2&gt;

&lt;p&gt;This is where the "same backend" idea pays off as ny app already has authentication: &lt;code&gt;Amplify&lt;/code&gt; created a &lt;code&gt;Amazon Cognito&lt;/code&gt; user pool and users sign in to use the chat and search. I don't want a second identity system for the voice agent.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AgentCore Runtime&lt;/code&gt; supports &lt;strong&gt;JWT inbound authorization&lt;/strong&gt;. You point it at an OIDC discovery URL and a list of allowed clients. An &lt;code&gt;Amazon Cognito&lt;/code&gt; user pool is also an OIDC provider, so I can wire the runtime straight to it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userPool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPool&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;userPoolClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPoolClient&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;discoveryUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="s2"&gt;`https://cognito-idp.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.amazonaws.com/`&lt;/span&gt; &lt;span class="o"&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;userPool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPoolId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.well-known/openid-configuration`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ...passed into the runtime's authorizerConfiguration.customJwtAuthorizer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now the same user who is signed into the app can authenticate to the voice agent, with no extra setup. The token they already have is the token the runtime accepts.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔄 Granting the user permission to invoke the runtime
&lt;/h2&gt;

&lt;p&gt;The signed-in user connects to the runtime, so the &lt;code&gt;Amazon Cognito&lt;/code&gt; &lt;strong&gt;authenticated role&lt;/strong&gt; needs permission to invoke it. There's a nice detail in how you wire this up: the voice stack already depends on the auth stack (it reads the user pool), so you want the dependency to stay one-directional. The clean way is to define the policy &lt;strong&gt;inside the voice stack&lt;/strong&gt; and attach it to the existing auth role by reference:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VoiceAgentInvokePolicy&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;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticatedUserIamRole&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;statements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock-agentcore:InvokeAgentRuntime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock-agentcore:InvokeAgentRuntimeWithWebSocketStream&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;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nx"&gt;voiceRuntime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attrAgentRuntimeArn&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;voiceRuntime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attrAgentRuntimeArn&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="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 policy is created in the voice stack, which is allowed to reference the auth role, while the auth stack never needs to know about the voice stack. The dependency flows in a single direction.&lt;/p&gt;
&lt;h2&gt;
  
  
  🌐 Step 4: connect the browser
&lt;/h2&gt;

&lt;p&gt;The last piece is the frontend. Locally the browser connected to &lt;code&gt;ws://127.0.0.1:8080/ws&lt;/code&gt;. Deployed, it connects to the &lt;code&gt;AgentCore&lt;/code&gt; endpoint:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wss://bedrock-agentcore.&amp;lt;region&amp;gt;.amazonaws.com/runtimes/&amp;lt;runtimeArn&amp;gt;/ws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The interesting question is authentication. &lt;code&gt;AgentCore&lt;/code&gt; accepts &lt;code&gt;SigV4&lt;/code&gt; (signed headers or a presigned URL) or an OAuth bearer token. From a &lt;strong&gt;browser&lt;/strong&gt;, &lt;code&gt;SigV4&lt;/code&gt; on a WebSocket is awkward, because the browser's WebSocket API doesn't let you set custom headers on the handshake.&lt;/p&gt;

&lt;p&gt;AWS documents a clean workaround for exactly this case: pass the bearer token through the &lt;code&gt;Sec-WebSocket-Protocol&lt;/code&gt; header. The token is base64url-encoded and sent as a subprotocol, alongside a sentinel subprotocol:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;fetchAuthSession&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="s2"&gt;aws-amplify/auth&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchAuthSession&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toString&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;base64url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/=/g&lt;/span&gt;&lt;span class="p"&gt;,&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="s2"&gt;`wss://bedrock-agentcore.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.amazonaws.com`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`/runtimes/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runtimeArn&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/ws`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`?qualifier=DEFAULT&amp;amp;X-Amzn-Bedrock-AgentCore-Runtime-Session-Id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;protocols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s2"&gt;`base64UrlBearerAuthorization.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;base64url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64UrlBearerAuthorization&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&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;protocols&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That &lt;code&gt;Amazon Cognito&lt;/code&gt; access token is exactly what the runtime's JWT authorizer validates. The user's &lt;code&gt;client_id&lt;/code&gt; claim has to match the &lt;code&gt;allowedClients&lt;/code&gt; we configured, which it does, because it's the same user pool client &lt;code&gt;Amplify&lt;/code&gt; gave us.&lt;/p&gt;

&lt;p&gt;From here on, the rest of the frontend doesn't change at all. The same code that captured microphone audio, streamed PCM frames, and played back the agent's voice against the local server now works against AgentCore. Only the URL and the auth changed.&lt;/p&gt;
&lt;h2&gt;
  
  
  🗺️ The whole picture
&lt;/h2&gt;

&lt;p&gt;Everything lives in one &lt;code&gt;Amplify Gen 2&lt;/code&gt; backend and deploys with a single &lt;code&gt;npx ampx sandbox&lt;/code&gt;:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6yds0k2o4kxzezdiqhj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6yds0k2o4kxzezdiqhj.png" alt=" " width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser signs in once with &lt;code&gt;Amazon Cognito&lt;/code&gt;. That identity gets it into the app, into the search API, into the chat, and now into the voice agent too.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧠 What I take away from this project
&lt;/h2&gt;

&lt;p&gt;A couple of things stood out while building this.&lt;/p&gt;

&lt;p&gt;The first is how little the agent code changed between local and deployed. The same FastAPI + Strands &lt;code&gt;BidiAgent&lt;/code&gt; server ran on my laptop and, unchanged, inside &lt;code&gt;Amazon Bedrock AgentCore&lt;/code&gt;. The container contract (port 8080, &lt;code&gt;/ws&lt;/code&gt;, &lt;code&gt;/ping&lt;/code&gt;) is simple enough that "make it a container" was the only real step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second is the value of keeping it all in one backend. Because &lt;code&gt;Amplify Gen 2&lt;/code&gt; is &lt;code&gt;CDK&lt;/code&gt;, the runtime, its image, its &lt;code&gt;IAM&lt;/code&gt;, and its wiring to &lt;code&gt;Amazon Cognito&lt;/code&gt; are all just constructs next to my data and auth definitions. The voice agent isn't a separate system I have to operate, it's another resource in the same deploy, sharing the same users.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The voice agent that used to live on my laptop now runs on AWS, authenticated with the users my app already had, deployed with the same command as everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your &lt;code&gt;Amplify Gen 2&lt;/code&gt; deployed &lt;code&gt;Amazon DynamoDb&lt;/code&gt; database was already an AI tool.&lt;br&gt;
Now the agent that talks to it is serverless and, thanks to &lt;code&gt;CDK&lt;/code&gt;, it's wired to &lt;code&gt;Amplify Gen 2&lt;/code&gt; deployments too.&lt;/strong&gt;&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;
        dynamodbvector
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An example application using Dynamo Db Vector Search as an AI tool
    &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;Semantic Search for Text, Voice &amp;amp; Omnichannel Agents on AWS: Amazon DynamoDB Vector Search, Amplify Gen 2, Amazon AgentCore &amp;amp; Amazon Nova Sonic&lt;/h1&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Companion posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/your-database-is-an-ai-tool-semantic-search-with-amazon-dynamodb-vector-search-46ff" rel="nofollow"&gt;Your database is an AI tool: semantic search with Amazon DynamoDB Vector Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/deploying-a-real-time-voice-agent-with-agentcore-runtime-and-amplify-gen-2-45bl" rel="nofollow"&gt;Deploying a real-time voice agent with AgentCore Runtime and Amplify Gen 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Omnichannel agents: sharing memory across a voice and a text agent with Amazon Bedrock AgentCore Memory (see &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector/./blog/blog-3.md" rel="noopener noreferrer"&gt;&lt;code&gt;blog/blog-3.md&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;A sample application that shows how to use &lt;strong&gt;Amazon DynamoDB native vector search&lt;/strong&gt; to build semantic search over application data, how to expose that capability to AI agents as a tool, how to deploy a real-time voice agent for it on &lt;strong&gt;Amazon Bedrock AgentCore Runtime&lt;/strong&gt;, and how to give a voice agent and a text agent a &lt;strong&gt;shared memory&lt;/strong&gt; so they behave as one omnichannel assistant — all inside a single AWS Amplify Gen 2 backend.&lt;/p&gt;

&lt;p&gt;It demonstrates the same…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  🙋 Who am I
&lt;/h2&gt;

&lt;p&gt;I'm &lt;a href="https://www.linkedin.com/in/desiodavide" rel="noopener noreferrer"&gt;D. De Sio&lt;/a&gt; and I work as a Head of Software Engineering in &lt;a href="https://eleva.it/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;br&gt;
As of September 2026, I’m an &lt;a href="https://www.credly.com/badges/9929fdf2-7a3d-4013-9de6-57c80e4920b9/public_url" rel="noopener noreferrer"&gt;AWS Certified Solution Architect Professional&lt;/a&gt; and &lt;a href="https://www.credly.com/badges/8c5a1487-191b-429e-8c2d-7cee43bf316b/public_url" rel="noopener noreferrer"&gt;AWS Certified DevOps Engineer Professional&lt;/a&gt;, but also a &lt;a href="https://www.linkedin.com/company/aws-user-group-pavia/" rel="noopener noreferrer"&gt;User Group Leader (in Pavia)&lt;/a&gt;, an &lt;strong&gt;AWS Community Builder&lt;/strong&gt; and, last but not least, a #serverless enthusiast.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flm4rzz6fbj5a7lhyk6qa.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flm4rzz6fbj5a7lhyk6qa.png" alt=" " width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎉 AWS Community Day Italy
&lt;/h2&gt;

&lt;p&gt;The full agenda for &lt;a href="https://www.awscommunityday.it/" rel="noopener noreferrer"&gt;AWS Community Day Italy&lt;/a&gt; is out!&lt;/p&gt;

&lt;p&gt;If you'd love to hear what the community has been working on, what they've learned, and what they want to share, come join us in Rome on October 2nd.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>agentcore</category>
      <category>amplify</category>
    </item>
    <item>
      <title>Your database is an AI Tool: semantic search with Amazon DynamoDB Vector Search</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Tue, 01 Sep 2026 13:29:32 +0000</pubDate>
      <link>https://dev.to/aws-builders/your-database-is-an-ai-tool-semantic-search-with-amazon-dynamodb-vector-search-46ff</link>
      <guid>https://dev.to/aws-builders/your-database-is-an-ai-tool-semantic-search-with-amazon-dynamodb-vector-search-46ff</guid>
      <description>&lt;h1&gt;
  
  
  🏃 TL;DR
&lt;/h1&gt;

&lt;p&gt;It's been a while since I wrote an article that wasn't about &lt;a href="https://github.com/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;KiroGraph&lt;/a&gt;! &lt;/p&gt;

&lt;p&gt;Lately, I've been playing with AI agents quite a lot, and one question in particular has caught my attention: how do we make agents useful when the information they need isn't sitting in a knowledge base, but directly inside an application?&lt;/p&gt;

&lt;p&gt;Let's take something very simple: imagine an e-commerce application with products, prices, categories, inventory and descriptions, nothing particularly special. The application is already running, and DynamoDB is already storing all this information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now we add an AI shopping assistant.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A user doesn't necessarily want to search using product names or exact keywords. They might say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm going hiking in Iceland in October. I need something lightweight, waterproof and I'd like to stay under €200."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where things get interesting: the user isn't really telling us what words to search for, but what they're looking for. And that's exactly the kind of problem where semantic search can make a big difference.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Amazon DynamoDB&lt;/code&gt; now has &lt;strong&gt;native vector search&lt;/strong&gt;, which means we can store embeddings alongside our application data and use it to retrieve items based on their semantic similarity.&lt;/p&gt;

&lt;p&gt;So instead of adding a separate vector database just because our AI application needs semantic retrieval, we can ask a different question: &lt;strong&gt;What if the database that already stores our application data could also be the vector database?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's what I want to explore in this article.&lt;/p&gt;

&lt;p&gt;And then, once we have that capability, we'll make it available to an AI agent as a &lt;strong&gt;tool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;See this GitHub repo for a practical example of the application shown in this article.&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;
        dynamodbvector
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An example application using Dynamo Db Vector Search as an AI tool
    &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;Semantic Search for Text, Voice &amp;amp; Omnichannel Agents on AWS: Amazon DynamoDB Vector Search, Amplify Gen 2, Amazon AgentCore &amp;amp; Amazon Nova Sonic&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Companion posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/your-database-is-an-ai-tool-semantic-search-with-amazon-dynamodb-vector-search-46ff" rel="nofollow"&gt;Your database is an AI tool: semantic search with Amazon DynamoDB Vector Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/deploying-a-real-time-voice-agent-with-agentcore-runtime-and-amplify-gen-2-45bl" rel="nofollow"&gt;Deploying a real-time voice agent with AgentCore Runtime and Amplify Gen 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Omnichannel agents: sharing memory across a voice and a text agent with Amazon Bedrock AgentCore Memory (see &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector/./blog/blog-3.md" rel="noopener noreferrer"&gt;&lt;code&gt;blog/blog-3.md&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;A sample application that shows how to use &lt;strong&gt;Amazon DynamoDB native vector search&lt;/strong&gt; to build semantic search over application data, how to expose that capability to AI agents as a tool, how to deploy a real-time voice agent for it on &lt;strong&gt;Amazon Bedrock AgentCore Runtime&lt;/strong&gt;, and how to give a voice agent and a text agent a &lt;strong&gt;shared memory&lt;/strong&gt; so they behave as one omnichannel assistant — all inside a single AWS Amplify Gen 2 backend.&lt;/p&gt;
&lt;p&gt;It demonstrates the same…&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/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  🔍 Why semantic search for application data?
&lt;/h2&gt;

&lt;p&gt;Let's start with the problem rather than the technology.&lt;/p&gt;

&lt;p&gt;Suppose our &lt;code&gt;DynamoDB&lt;/code&gt; table contains products like this:&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;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JACKET-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"StormShield Alpine Jacket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"outdoor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lightweight waterproof shell designed for hiking in cold and rainy conditions."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;189&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"available"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;A user asking for:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Something lightweight for hiking in Iceland in October"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;might never mention the words "shell" or "rainy conditions". But we know what they mean, and that gap is exactly what separates keyword search from semantic search.&lt;/p&gt;

&lt;p&gt;With keyword search, we're mostly asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Does this item contain the words I'm looking for?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While using vector search, we're asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How close is this item to what the user is actually asking for?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;To do that, we need to represent the product as a vector.&lt;/strong&gt;&lt;br&gt;
We can generate an embedding from its name, description and other relevant textual attributes and store that embedding together with the product in &lt;code&gt;DynamoDB&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The item now contains both the information the application needs and the information the semantic search needs.&lt;/p&gt;

&lt;p&gt;Conceptually our &lt;code&gt;DynamoDB&lt;/code&gt; record would be:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product
 ├── name
 ├── description
 ├── category
 ├── price
 ├── availability
 └── embedding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the first thing that makes &lt;code&gt;DynamoDB Vector Search&lt;/code&gt; interesting to me: &lt;strong&gt;the vector isn't some external representation of a document, rather it's another representation of an entity that already belongs to the application.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🔄 What DynamoDB Vector Search changes
&lt;/h2&gt;

&lt;p&gt;If you've built RAG systems before, this architecture might look very familiar. We usually end up with something like:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78ccfx1kwj3a46r5i6a3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78ccfx1kwj3a46r5i6a3.png" alt=" " width="800" height="1071"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is nothing inherently wrong with this: a dedicated vector or search database can be exactly the right choice depending on the workload. But there is a fundamental difference when our vectors represent application entities.&lt;/p&gt;

&lt;p&gt;Take our product: it has description, price, category, inventory state, availability. With &lt;code&gt;DynamoDB Vector Search&lt;/code&gt;, we can keep the vector representation next to those attributes and use them together when searching.&lt;/p&gt;

&lt;p&gt;So our search isn't just:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Find products similar to this text."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It can become:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Find products semantically similar to this request, where the price is below €200 and the product is currently available."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a much more useful query for an application, and it's where &lt;code&gt;DynamoDB Vector Search&lt;/code&gt; becomes more than just a way to store embeddings: it lets us combine &lt;strong&gt;vector similarity with the operational attributes that already drive the application&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  🏗️ Setting up the table and vector index
&lt;/h2&gt;

&lt;p&gt;Let's look at what it takes to set this up in practice: a DynamoDB table in on-demand capacity mode (required for vector indexes), and a vector index on top of it.&lt;/p&gt;

&lt;p&gt;At the time of writing, CloudFormation doesn't support the &lt;code&gt;VectorIndexes&lt;/code&gt; property yet, so we create the table first and then add the vector index via the &lt;code&gt;UpdateTable&lt;/code&gt; API. In our sample application we use AWS CDK with an &lt;code&gt;AwsCustomResource&lt;/code&gt; to make this SDK call as part of the deployment:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;productsTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ProductsTable&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;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ProductId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;billingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BillingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwsCustomResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CreateVectorIndex&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;onCreate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DynamoDB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;updateTable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;productsTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;VectorIndexUpdates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="na"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;IndexName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ProductEmbeddingIndex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;VectorAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Embedding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;Projection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ProjectionType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ALL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;Dimensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;DistanceFunction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COSINE&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;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;physicalResourceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PhysicalResourceId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ProductEmbeddingIndex&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="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A few things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dimensions: 1024&lt;/strong&gt; matches the output of Amazon Titan Text Embeddings V2, which is the model we use to generate embeddings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DistanceFunction: COSINE&lt;/strong&gt; means lower scores indicate greater similarity (0 = identical, 2 = opposite direction).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Projection: ALL&lt;/strong&gt; makes every attribute available in search results, so we can return product details directly from the vector search without a second read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the index is &lt;code&gt;ACTIVE&lt;/code&gt;, we can store vectors alongside our products and search them.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F218eocgr9iz4p1eq8y1d.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F218eocgr9iz4p1eq8y1d.png" alt=" " width="799" height="305"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  📦 Storing products with embeddings
&lt;/h2&gt;

&lt;p&gt;Writing a product with its vector embedding is a normal DynamoDB &lt;code&gt;PutItem&lt;/code&gt; call. The embedding is stored as a list of numbers (&lt;code&gt;L&lt;/code&gt; type containing &lt;code&gt;N&lt;/code&gt; elements):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;BedrockRuntimeClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;InvokeModelCommand&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="s2"&gt;@aws-sdk/client-bedrock-runtime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PutItemCommand&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="s2"&gt;@aws-sdk/client-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;marshall&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="s2"&gt;@aws-sdk/util-dynamodb&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;bedrock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockRuntimeClient&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;dynamodb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DynamoDBClient&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;

&lt;span class="c1"&gt;// Generate an embedding from the product text&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvokeModelCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amazon.titan-embed-text-v2:0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;StormShield Alpine Jacket. Lightweight waterproof shell designed for hiking in cold and rainy conditions.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;normalize&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="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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;embedding&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&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="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Store the product with its embedding&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;marshall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JACKET-123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;StormShield Alpine Jacket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outdoor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Lightweight waterproof shell designed for hiking in cold and rainy conditions.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;189&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Available&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Add the embedding as L(ist) of N(umber) values&lt;/span&gt;
&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;L&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="na"&gt;N&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&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;await&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutItemCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The embedding model produces a 1024-dimensional vector from the product's name and description. That vector now lives alongside the operational attributes in the same item.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4a9term5etasfa7xjnh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4a9term5etasfa7xjnh.png" alt=" " width="799" height="560"&gt;&lt;/a&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl0snz157nylc9xg4cncv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl0snz157nylc9xg4cncv.png" alt=" " width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🎯 Semantic search and structured filters
&lt;/h2&gt;

&lt;p&gt;Let's go back to the original request:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm going hiking in Iceland in October. I need something lightweight, waterproof and I'd like to stay under €200."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are really two different things happening here.&lt;/p&gt;

&lt;p&gt;The first part is semantic:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lightweight
waterproof
hiking
cold weather
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The second part is a normal application constraint:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;price &amp;lt;= 200
available = true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;DynamoDB Vector Search&lt;/code&gt; allows us to bring these two worlds together: the query embedding tells us which products are semantically relevant while the DynamoDB attributes tell us whether those products actually satisfy the application constraints.&lt;/p&gt;

&lt;p&gt;This is an important distinction, as we're not replacing our normal database queries with vector search but we're adding another dimension to them.&lt;/p&gt;

&lt;p&gt;And that's probably the way I'd think about DynamoDB Vector Search in an application.&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Let's put a vector database next to our application."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Let's make some of our existing application entities searchable by meaning."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  🧠 Using an LLM to separate meaning from filters
&lt;/h2&gt;

&lt;p&gt;This is a practical problem that comes up as soon as you try to build this: the user writes one sentence, but we need to split it into two different things, with the semantic part going to the embedding model and the structured part becoming a filter.&lt;/p&gt;

&lt;p&gt;A user writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm looking for a warm jacket for winter hiking, under 200 euros"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We need to turn that into:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Where it goes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;warm jacket for winter hiking&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;→ embedding model → vector search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;price &amp;lt;= 200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;→ post-search filter on DynamoDB attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;But the user doesn't make that distinction. They write one sentence that mixes intent and constraints, so we need something that understands natural language well enough to tell the two apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is where a small LLM comes in.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we even touch the vector index, we can send the user's query to a fast, inexpensive model like &lt;code&gt;Amazon Nova Micro&lt;/code&gt; and ask it to extract two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;semantic description&lt;/strong&gt;, what the user is looking for by meaning&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;structured filters&lt;/strong&gt;, price range, availability, or any other attribute constraint
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseUserQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ParsedQuery&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;systemPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You are a query parser for a product search engine.
Given a user's natural language query, extract:
1. The semantic/descriptive part (what the user is looking for by meaning)
2. Any structured filters (price constraints, availability)

Respond ONLY with a JSON object:
{
  "semanticQuery": "the descriptive part for semantic search",
  "maxPrice": number or null,
  "minPrice": number or null,
  "availableOnly": true or null
}`&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;response&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;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvokeModelCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eu.amazon.nova-micro-v1:0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messages-v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;system&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="nx"&gt;systemPrompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&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="nx"&gt;userQuery&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="na"&gt;inferenceConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;temperature&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="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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&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="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&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="nx"&gt;text&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;parsed&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;For the query &lt;em&gt;"warm jacket for winter hiking, under 200 euros"&lt;/em&gt;, Nova Micro returns:&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;"semanticQuery"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warm jacket for winter hiking"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"availableOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;Now we have a clean separation: the semantic query goes to the embedding model and then to &lt;code&gt;SearchVectors&lt;/code&gt;, while the price constraint is applied as a post-search filter on the results.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7scc3qghln98ucr4518d.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7scc3qghln98ucr4518d.png" alt=" " width="800" height="743"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🔎 Calling SearchVectors
&lt;/h2&gt;

&lt;p&gt;With the semantic query extracted, we generate an embedding and call &lt;code&gt;SearchVectors&lt;/code&gt;. This is the core of the vector search:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generate embedding from the semantic part only&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryEmbedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;semanticQuery&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// SearchVectors expects a plain array of {N: "value"} objects (not wrapped in L type)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchVector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queryEmbedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&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="na"&gt;N&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&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;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SearchVectorsCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;IndexName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ProductEmbeddingIndex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;SearchVector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchVector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;TopK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// fetch extra to account for post-filtering&lt;/span&gt;
  &lt;span class="na"&gt;ProjectionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ProductId, #n, Category, Description, Price, Available&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ExpressionAttributeNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Name&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A few things worth noting about the &lt;code&gt;SearchVectors&lt;/code&gt; API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SearchVector&lt;/strong&gt; is a plain array of &lt;code&gt;{N: "value"}&lt;/code&gt; objects, &lt;strong&gt;not&lt;/strong&gt; wrapped in the DynamoDB &lt;code&gt;L&lt;/code&gt; type. This is different from how you store the vector in a &lt;code&gt;PutItem&lt;/code&gt;, where you use &lt;code&gt;{L: [{N: "0.123"}, ...]}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TopK&lt;/strong&gt; controls how many results come back (max 100). We ask for more than we need because we're going to filter some out in the next step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ProjectionExpression&lt;/strong&gt; works just like in &lt;code&gt;Query&lt;/code&gt; or &lt;code&gt;Scan&lt;/code&gt;. You can only return attributes that are projected into the vector index.&lt;/li&gt;
&lt;li&gt;The results come back sorted by similarity, with the most similar item first. Each result includes the item attributes and a &lt;strong&gt;Score&lt;/strong&gt;. For COSINE, lower is better.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  🔧 Applying structured filters after the search
&lt;/h2&gt;

&lt;p&gt;Now we combine the vector search results with the structured filters that Nova Micro extracted. Since &lt;code&gt;SearchConditionExpression&lt;/code&gt; currently only supports equality operators, we apply range filters in our application code:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Map DynamoDB items to our application types&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&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;SearchResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&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;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unmarshall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&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;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&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="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Available&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Score&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="c1"&gt;// Apply the structured filters extracted by Nova Micro&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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxPrice&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxPrice&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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minPrice&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minPrice&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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availableOnly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Return the top results&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is why we asked for &lt;code&gt;TopK: 20&lt;/code&gt; instead of 5. After filtering out products that don't meet the price constraint, we still want enough results left to return.&lt;/p&gt;

&lt;p&gt;The complete flow through the Lambda looks like this:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxiofh5epme9s86fmyt8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxiofh5epme9s86fmyt8.png" alt=" " width="800" height="1002"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each step has a clear responsibility: Nova Micro understands the user's language, Titan Embeddings turns meaning into a vector, DynamoDB finds the closest matches, and the application logic applies the business constraints.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tx0w90zkmwptfss1ylz.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tx0w90zkmwptfss1ylz.png" alt=" " width="800" height="721"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🤖 Now let's give this capability to an AI agent
&lt;/h2&gt;

&lt;p&gt;Once &lt;code&gt;DynamoDB&lt;/code&gt; can answer semantic questions about our application data, the next step is giving that capability to an AI agent as a &lt;strong&gt;tool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We could create a generic tool like &lt;code&gt;search_vectors&lt;/code&gt;, but I don't think that's particularly useful.&lt;/p&gt;

&lt;p&gt;The agent doesn't need to know that we're using &lt;code&gt;DynamoDB&lt;/code&gt;, it doesn't need to know about vector indexes, and it doesn't need to understand embeddings. &lt;strong&gt;Those are implementation details&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What the agent needs is a capability that makes sense in the context of the application. For our e-commerce application, that could simply be &lt;code&gt;search_products&lt;/code&gt;: the agent calls it with a natural language description of what the user wants, and the tool handles everything behind the scenes, including LLM parsing, embedding generation, vector search and structured filtering.&lt;/p&gt;

&lt;p&gt;Whether we expose this tool through &lt;strong&gt;MCP&lt;/strong&gt; (Model Context Protocol), through Bedrock's native &lt;strong&gt;tool use&lt;/strong&gt; in the Converse API, or through any other tool-calling mechanism doesn't really change the pattern. The key idea is the same: &lt;strong&gt;the database capability becomes a tool that the agent can use&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In our sample application, we use the &lt;strong&gt;Amplify AI Kit&lt;/strong&gt; which provides a conversation route backed by Amazon Bedrock. We define &lt;code&gt;searchProducts&lt;/code&gt; as a tool that the conversation model can invoke:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;aiModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Amazon Nova Lite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You are a helpful AI shopping assistant for an outdoor gear store.
When a user describes what they're looking for, use the searchProducts tool 
to find relevant products. Present the results in a friendly, conversational way.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;searchProducts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Search the product catalog using natural language.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;searchProducts&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;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The architecture becomes:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvg4kjtm0c2i3hw1dq33j.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvg4kjtm0c2i3hw1dq33j.png" alt=" " width="765" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The agent decides &lt;strong&gt;when&lt;/strong&gt; to search and &lt;strong&gt;what&lt;/strong&gt; to search for, the tool handles the &lt;strong&gt;how&lt;/strong&gt;, and &lt;code&gt;DynamoDB Vector Search&lt;/code&gt; is what makes the search actually understand the user's intent.&lt;/p&gt;
&lt;h2&gt;
  
  
  💬 Building a conversational shopping experience
&lt;/h2&gt;

&lt;p&gt;Now let's put all of this together and see it in action.&lt;/p&gt;

&lt;p&gt;The user opens the chat and writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm going hiking in Iceland in October. I need something lightweight, waterproof and I'd like to stay under €200."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent reads the message, decides it needs product data, and calls the &lt;code&gt;searchProducts&lt;/code&gt; tool with the user's request. Behind the scenes, our Lambda does its four-step job: Nova Micro separates the semantic intent from the price constraint, Titan Embeddings generates a vector, DynamoDB &lt;code&gt;SearchVectors&lt;/code&gt; finds the closest products, and the application filters out anything over €200.&lt;/p&gt;

&lt;p&gt;The results flow back to the agent, which presents them conversationally:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag2l5lhqhfrxbqd7u3wc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag2l5lhqhfrxbqd7u3wc.png" alt=" " width="800" height="708"&gt;&lt;/a&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fllfuj0ydxi8lwxcjpfuu.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fllfuj0ydxi8lwxcjpfuu.png" alt=" " width="800" height="697"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But the conversation doesn't stop there.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The user might say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I don't really like the first one. Can you find something less technical looking?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent performs another search using the new context. It doesn't start from scratch, but understands the conversation history and refines its search.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8vy4dlal8ve0onr4chzl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8vy4dlal8ve0onr4chzl.png" alt=" " width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Something similar to the second one, but cheaper."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent handles the conversation, the tool handles the search, and DynamoDB handles the data, each piece doing what it's good at.&lt;/p&gt;
&lt;h2&gt;
  
  
  🎤 Taking it further: a voice shopping assistant
&lt;/h2&gt;

&lt;p&gt;Text chat is one way to interact with this system. But what if the user could just &lt;strong&gt;talk&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;Amazon Nova 2 Sonic&lt;/code&gt;, Amazon's speech-to-speech foundation model, we can build a voice agent that listens to the user, understands what they need, searches the product catalog using the same &lt;code&gt;search_products&lt;/code&gt; tool, and responds by voice. The user never types a word.&lt;/p&gt;

&lt;p&gt;The framework we use for this is &lt;code&gt;Strands Agents&lt;/code&gt;, an open source SDK for building AI agents. Its &lt;code&gt;BidiAgent&lt;/code&gt; class handles the bidirectional streaming lifecycle: audio in, audio out, tool calls in between.&lt;/p&gt;

&lt;p&gt;Here is the complete voice agent:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WebSocketDisconnect&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.experimental.bidi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BidiAgent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.experimental.bidi.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BidiNovaSonicModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;

&lt;span class="c1"&gt;# Our product search tool (same logic as the Lambda)
&lt;/span&gt;&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Search the product catalog using natural language.
    Returns matching products with names, prices and descriptions.

    Args:
        query: Natural language description of what the user wants.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Parse query with Nova Micro (semantic vs filters)
&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;parse_user_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# 2. Generate embedding with Titan
&lt;/span&gt;    &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semanticQuery&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="c1"&gt;# 3. SearchVectors in DynamoDB
&lt;/span&gt;    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search_vectors&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="c1"&gt;# 4. Apply price filters
&lt;/span&gt;    &lt;span class="n"&gt;filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsed&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;format_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Configure Nova 2 Sonic
&lt;/span&gt;&lt;span class="n"&gt;sonic_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BidiNovaSonicModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon.nova-2-sonic-v1:0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;provider_config&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;audio&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;voice&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;tiffany&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;input_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&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="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.websocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/ws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;voice_chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BidiAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sonic_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_products&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful voice assistant for an outdoor gear store.&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;await&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&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="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receive_json&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_json&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;That's the entire server. A few things worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;@tool&lt;/code&gt; decorator from Strands turns a regular Python function into a tool that the voice model can invoke. The same &lt;code&gt;search_products&lt;/code&gt; function we built before, with the same four-step pipeline, now works inside a voice conversation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BidiAgent&lt;/code&gt; manages the full bidirectional stream: it receives PCM audio from the browser, sends it to Nova 2 Sonic, handles tool calls when the model decides to search, and streams the voice response back.&lt;/li&gt;
&lt;li&gt;The WebSocket integration is remarkably simple. &lt;code&gt;websocket.receive_json&lt;/code&gt; and &lt;code&gt;websocket.send_json&lt;/code&gt; are passed directly as the agent's input and output channels. Strands handles the protocol.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user says &lt;em&gt;"I need something waterproof for hiking, under 200 euros"&lt;/em&gt; and the agent responds by voice with product recommendations. The entire flow, from speech recognition to product search to voice response, happens in a single streaming connection.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fntbtkji7ovnijk4c8026.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fntbtkji7ovnijk4c8026.png" alt=" " width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another interesting aspect is that, by simply using a multilingual model like &lt;code&gt;Amazon Nova Sonic 2&lt;/code&gt;, your assistant becomes multilingual out of the box. You don't need to change a single line of code.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0370av44rup8a0r85n8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0370av44rup8a0r85n8.png" alt=" " width="799" height="654"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For local development, the agent runs as a simple Python server on &lt;code&gt;localhost:8080&lt;/code&gt;. On the frontend, a React component captures microphone audio via an &lt;code&gt;AudioWorklet&lt;/code&gt;, streams it over WebSocket, and plays back the agent's audio response.&lt;/p&gt;

&lt;p&gt;For production, this same agent can be deployed on &lt;strong&gt;Amazon Bedrock AgentCore Runtime&lt;/strong&gt;, which provides managed WebSocket infrastructure with authentication, auto-scaling, and session management. The agent code stays the same, while AgentCore handles the operational concerns: IAM-based authentication via SigV4-signed WebSocket URLs, isolated microVM execution, and automatic scaling based on demand.&lt;/p&gt;
&lt;h2&gt;
  
  
  🏪 What about a marketplace?
&lt;/h2&gt;

&lt;p&gt;Let's change the application without changing the architecture.&lt;/p&gt;

&lt;p&gt;Instead of products managed by a store, imagine a marketplace where users create listings.&lt;/p&gt;

&lt;p&gt;A listing could look like this:&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;"listingId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LISTING-9281"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Nike Air Max 90"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"White and grey sneakers, worn a few times, size 42."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sneakers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"IT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"embedding"&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="err"&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;A buyer would say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm looking for white sneakers for everyday use, size 42, under €100."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent calls a &lt;code&gt;search_listings&lt;/code&gt; tool and the semantic part of the query could be:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;white casual sneakers suitable for everyday use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;while &lt;code&gt;DynamoDB&lt;/code&gt; filters can handle:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;size = 42
price &amp;lt;= 100
country = IT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The interesting part here is that we're searching user-generated content. One seller might write:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"White Nike sneakers, barely used."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another might write:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Clean white trainers, perfect for daily outfits."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another might simply write:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Nike Air Max 90, white and grey, size 42."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The wording is different, but the intent is the same, and that's exactly where semantic search makes the application feel more natural.&lt;/strong&gt; And again, the vector search is happening directly against the application data stored in &lt;code&gt;DynamoDB&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧩 The pattern is bigger than e-commerce
&lt;/h2&gt;

&lt;p&gt;At this point, I think the interesting part isn't really the store or the marketplace. They're just convenient examples for this digression. The actual pattern is much more general: it covers any app where we have an application entity that already lives in &lt;code&gt;DynamoDB&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can generate an embedding that represents the parts of that entity users may want to search semantically, and store it alongside the entity.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DynamoDB Vector Search&lt;/code&gt; lets us retrieve those entities based on semantic similarity while still working with their normal attributes.&lt;/p&gt;

&lt;p&gt;Then we expose that capability to an AI agent as a tool, whether through MCP, Bedrock tool use, or any other mechanism.&lt;/p&gt;

&lt;p&gt;That entity might be a product, a marketplace listing, a hotel, a job, an event, a course or anything else that users need to discover based on meaning. The important thing is that the vector belongs to the application entity.&lt;/p&gt;
&lt;h2&gt;
  
  
  ✨ What I like about this architecture
&lt;/h2&gt;

&lt;p&gt;There is a subtle but important difference between this and the way we usually approach RAG.&lt;/p&gt;

&lt;p&gt;With a typical RAG architecture, we're taking documents, splitting them into chunks, generating embeddings and putting those chunks into a retrieval system. The thing we're retrieving is knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here, we're retrieving application state&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  🤔 So when would I use DynamoDB Vector Search?
&lt;/h2&gt;

&lt;p&gt;I don't think the answer should be "whenever I need vectors." There are plenty of workloads where a dedicated search or vector database is a better fit.&lt;/p&gt;

&lt;p&gt;If I'm building a huge document retrieval platform, for example, I'm probably going to evaluate solutions specifically designed around that problem.&lt;/p&gt;

&lt;p&gt;The use case I find particularly compelling is when the data already lives in &lt;code&gt;DynamoDB&lt;/code&gt; and the application needs to find those entities based on their semantic meaning. In these cases, the vector isn't really a separate piece of knowledge but rather another way of looking at the application data.&lt;/p&gt;
&lt;h3&gt;
  
  
  🎯 When to Use DynamoDB Vector Search vs. Dedicated Vector Databases
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario / Requirement&lt;/th&gt;
&lt;th&gt;Use DynamoDB Vector Search&lt;/th&gt;
&lt;th&gt;Use a Dedicated Vector DB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Single Source of Truth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your primary application data &lt;strong&gt;already lives in DynamoDB&lt;/strong&gt;, and vectors represent another semantic dimension of the same entity.&lt;/td&gt;
&lt;td&gt;Your source data consists of raw unstructured documents, PDFs, or logs stored primarily for search/RAG pipelines.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architecture &amp;amp; Sync&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You want to avoid complex ETL pipelines and maintain data sync seamlessly via &lt;strong&gt;DynamoDB Streams&lt;/strong&gt; without extra orchestration.&lt;/td&gt;
&lt;td&gt;You are willing to manage a separate ETL/sync pipeline between your primary DB and the Vector DB to leverage advanced vector features.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Filtering &amp;amp; Querying&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple vector searches or queries &lt;strong&gt;combined with DynamoDB Partition/Sort Keys&lt;/strong&gt; (e.g., &lt;em&gt;find similar products belonging to User X&lt;/em&gt;).&lt;/td&gt;
&lt;td&gt;Complex dynamic metadata filtering, multi-faceted aggregations, or heavy hybrid search algorithms (BM25 + Dense Vectors).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vector Scale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hundreds of thousands to tens of millions of vectors tied directly to application domain entities.&lt;/td&gt;
&lt;td&gt;Billions of vectors at massive global scale for dedicated enterprise-wide document retrieval platforms.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency Requirements&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Acceptable query latency in the &lt;strong&gt;tens of milliseconds&lt;/strong&gt; range&lt;/td&gt;
&lt;td&gt;Strict &lt;strong&gt;sub-10ms&lt;/strong&gt; latency requirements for high-frequency, real-time nearest-neighbor (HNSW) search.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost &amp;amp; Operational Overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Great for serverless-first architectures with variable or moderate traffic, leveraging pay-per-use components.&lt;/td&gt;
&lt;td&gt;More cost-effective at sustained massive scale with dedicated instance-based indexing and fine-tuned memory usage.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  🛠️ Your database is an AI tool
&lt;/h2&gt;

&lt;p&gt;This is what I find most interesting about the whole thing.&lt;/p&gt;

&lt;p&gt;We usually think of a database as something the application talks to; with AI agents, we should start to see it differently.&lt;/p&gt;

&lt;p&gt;We can take the database that already contains our application entities, make those entities searchable by meaning, and expose that capability as a tool that an agent can use, whether through MCP, Bedrock tool use, or any tool-calling mechanism that fits the application.&lt;/p&gt;

&lt;p&gt;The result isn't really a new "AI database" because it's still our application database: &lt;strong&gt;The vector isn't the destination. It's a new way for an AI agent to discover the data your application already owns.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your database is an AI tool. See this GitHub repo for a practical example of the application shown in this article.&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;
        dynamodbvector
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An example application using Dynamo Db Vector Search as an AI tool
    &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;Semantic Search for Text, Voice &amp;amp; Omnichannel Agents on AWS: Amazon DynamoDB Vector Search, Amplify Gen 2, Amazon AgentCore &amp;amp; Amazon Nova Sonic&lt;/h1&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Companion posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/your-database-is-an-ai-tool-semantic-search-with-amazon-dynamodb-vector-search-46ff" rel="nofollow"&gt;Your database is an AI tool: semantic search with Amazon DynamoDB Vector Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/deploying-a-real-time-voice-agent-with-agentcore-runtime-and-amplify-gen-2-45bl" rel="nofollow"&gt;Deploying a real-time voice agent with AgentCore Runtime and Amplify Gen 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Omnichannel agents: sharing memory across a voice and a text agent with Amazon Bedrock AgentCore Memory (see &lt;a href="https://github.com/davide-desio-eleva/dynamodbvector/./blog/blog-3.md" rel="noopener noreferrer"&gt;&lt;code&gt;blog/blog-3.md&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;A sample application that shows how to use &lt;strong&gt;Amazon DynamoDB native vector search&lt;/strong&gt; to build semantic search over application data, how to expose that capability to AI agents as a tool, how to deploy a real-time voice agent for it on &lt;strong&gt;Amazon Bedrock AgentCore Runtime&lt;/strong&gt;, and how to give a voice agent and a text agent a &lt;strong&gt;shared memory&lt;/strong&gt; so they behave as one omnichannel assistant — all inside a single AWS Amplify Gen 2 backend.&lt;/p&gt;

&lt;p&gt;It demonstrates the same…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/davide-desio-eleva/dynamodbvector" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  🙋 Who am I
&lt;/h2&gt;

&lt;p&gt;I'm &lt;a href="https://www.linkedin.com/in/desiodavide" rel="noopener noreferrer"&gt;D. De Sio&lt;/a&gt; and I work as a Head of Software Engineering in &lt;a href="https://eleva.it/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;br&gt;
As of September 2026, I’m an &lt;a href="https://www.credly.com/badges/9929fdf2-7a3d-4013-9de6-57c80e4920b9/public_url" rel="noopener noreferrer"&gt;AWS Certified Solution Architect Professional&lt;/a&gt; and &lt;a href="https://www.credly.com/badges/8c5a1487-191b-429e-8c2d-7cee43bf316b/public_url" rel="noopener noreferrer"&gt;AWS Certified DevOps Engineer Professional&lt;/a&gt;, but also a &lt;a href="https://www.linkedin.com/company/aws-user-group-pavia/" rel="noopener noreferrer"&gt;User Group Leader (in Pavia)&lt;/a&gt;, an &lt;strong&gt;AWS Community Builder&lt;/strong&gt; and, last but not least, a #serverless enthusiast.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fblk6ad3topn7icpwchum.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fblk6ad3topn7icpwchum.png" alt=" " width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Community Day Italy
&lt;/h2&gt;

&lt;p&gt;Did you know that the Call for Papers for &lt;a href="https://www.awscommunityday.it/" rel="noopener noreferrer"&gt;AWS Community Day Italy&lt;/a&gt; is still open?&lt;/p&gt;

&lt;p&gt;We’d love to hear what you’ve been working on, what you’ve learned, and what you’d like to share with the AWS community.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have a talk in mind? This is your chance!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;👉 Check out the Call for Papers and submit your proposal &lt;a href="https://conference-hall.io/aws-community-day-italy-2026" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>dynamodb</category>
      <category>vectordatabase</category>
    </item>
    <item>
      <title>Kiro V1.0.0 is out: migrate your hooks!</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Thu, 18 Jun 2026 09:44:45 +0000</pubDate>
      <link>https://dev.to/aws-builders/kiro-v100-is-out-migrate-your-hooks-59oc</link>
      <guid>https://dev.to/aws-builders/kiro-v100-is-out-migrate-your-hooks-59oc</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwps55uc1qnn5q0frr4uu.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwps55uc1qnn5q0frr4uu.png" alt=" " width="800" height="783"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kiro 1.0 is now publicly available and ready to use!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fortunately, KiroGraph, my code intelligence graph for Kiro, relies heavily on Kiro's lifecycle components, making it a great real-world test case to verify whether all the features we loved during the beta are still working as expected.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1znqmlare6zw9wtu0fj9.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1znqmlare6zw9wtu0fj9.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;
        kirograph
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Semantic code knowledge graph for Kiro: fewer tool calls, instant symbol lookups, 100% local.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Flogo.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;KiroGraph&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/terminal.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Fterminal.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Semantic code knowledge graph for &lt;a href="https://kiro.dev" rel="nofollow noopener noreferrer"&gt;Kiro&lt;/a&gt;: fewer tool calls, instant symbol lookups, 100% local.&lt;/p&gt;
&lt;p&gt;Inspired by &lt;a href="https://github.com/colbymchenry/codegraph" rel="noopener noreferrer"&gt;CodeGraph&lt;/a&gt; by &lt;a href="https://github.com/colbymchenry" rel="noopener noreferrer"&gt;colbymchenry&lt;/a&gt; for Claude Code, rebuilt natively for Kiro's MCP and hooks system.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full support is for Kiro only.&lt;/strong&gt; Experimental integrations for 34 other MCP-capable tools (Cursor, Copilot, Claude Code, Windsurf, Cline, and more) are available with auto-detection. See &lt;a href="https://github.com/davide-desio-eleva/kirograph/docs/guide/integrations.md" rel="noopener noreferrer"&gt;Integrations&lt;/a&gt; for the full list.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why KiroGraph?&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;When you ask Kiro to work on a complex task, it explores your codebase using file reads, grep, and glob searches. Every one of those is a tool call, and tool calls consume context and slow things down.&lt;/p&gt;
&lt;p&gt;KiroGraph gives Kiro a semantic knowledge graph that's pre-indexed and always up to date. Instead of scanning files to understand your code, Kiro queries the graph instantly: symbol relationships, call graphs, type hierarchies, impact radius, all in a single MCP tool call.&lt;/p&gt;
&lt;p&gt;The result is fewer tool…&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/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Good News
&lt;/h2&gt;

&lt;p&gt;Let's start with what didn't change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Steering and Skills&lt;/strong&gt; (using the SKILL.md format) continue to work exactly as before, with the same structure and frontmatter.&lt;/li&gt;
&lt;li&gt;All &lt;strong&gt;MCP&lt;/strong&gt; servers, both project-level and global, are automatically connected and immediately available after the upgrade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;So far, so good.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's changed
&lt;/h2&gt;

&lt;p&gt;Now for the interesting part.&lt;/p&gt;

&lt;p&gt;Many of us have learned to love &lt;strong&gt;Kiro Hooks&lt;/strong&gt; because they allow us to customize development workflows and lifecycle events.&lt;/p&gt;

&lt;p&gt;For example, &lt;strong&gt;KiroGraph uses a hook to automatically synchronize the code graph at agent stop whenever files are modified&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you open an existing project that still uses legacy hooks, &lt;strong&gt;you'll immediately notice that they're marked as V1 and inactive.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At first glance, this looks alarming.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjtz94y5s57ct8cpc4m9f.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjtz94y5s57ct8cpc4m9f.png" alt=" " width="768" height="1408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Kiro migration pop up
&lt;/h2&gt;

&lt;p&gt;The good news is that &lt;strong&gt;Kiro can actually help you migrate your hooks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you restart Kiro you'll probably see this migration popup which will assist you in a complete migration&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fez5vhnpbnjv1oumh4j12.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fez5vhnpbnjv1oumh4j12.png" alt=" " width="800" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kiro will confirm the migration.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffj2cj6w9oz12pa6t7yu5.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffj2cj6w9oz12pa6t7yu5.png" alt=" " width="799" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Be aware:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you'll still have older version, delete them&lt;/li&gt;
&lt;li&gt;new version won't be active, you should activate them one by one&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If the popup doesn't appear
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6g4os1rznxx0bws2n25j.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6g4os1rznxx0bws2n25j.png" alt=" " width="712" height="1544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This has happened to me in multiple projects, and I’ve also seen it affect other users working with KiroGraph or older hook formats: the expected popup doesn’t appear.&lt;/p&gt;

&lt;p&gt;An IDE restart might be sufficient, but in some cases a fresh installation can help as well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;However, if you’re using a tool like KiroGraph or a CLI that generates hooks in the older format, you won’t see this popup.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don’t worry: although the hook schema has changed and older hooks are no longer active, Kiro still understands both formats and can automatically generate the updated configuration for you.&lt;/p&gt;

&lt;p&gt;In my case, the entire migration was completed with a single prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Analyze the `.kiro/hooks` directory and identify all existing Kiro Hooks. These hooks use V1 format and are no longer active in the latest Kiro IDE V1.0.0.

Your tasks is:

1. Determine the differences between the legacy V1 hook format and the current hook format supported by Kiro IDE.
2. Migrate all existing V1 hooks to the new format.
3. Verify that the generated hooks are recognized and executed by the current Kiro IDE.

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Kiro's Response
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Now let me find where these hooks are. 
Now I have full understanding of the v1 format. 
Now I need to map the v1 triggers/actions to v2 format:

| v1 `when.type` | v2 `trigger` |
|---|---|
| `agentStop` | `Stop` |
| `preToolUse` | `PreToolUse` |

| v1 `then.type` | v2 `action.type` |
|---|---|
| `runCommand` | `command` |
| `askAgent` | `agent` |

| v1 `when.toolTypes: ["shell"]` | v2 `matcher` regex for shell tools |

Now write the new v2 hook files. 
All done. 
Here's what changed:

**`.kiro/hooks/`** — Replaced all 6 `.kiro.hook` files with their `.json` v2 equivalents. 

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

&lt;/div&gt;



&lt;p&gt;After analyzing the existing hooks, Kiro identified the required mappings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Legacy Format&lt;/th&gt;
&lt;th&gt;New Format&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agentStop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Stop&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;preToolUse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PreToolUse&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;runCommand&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;command&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;askAgent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agent&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrated all existing &lt;code&gt;.kiro.hook&lt;/code&gt; files to the new JSON format.&lt;/li&gt;
&lt;li&gt;Verified that all migrated hooks were correctly recognized by Kiro IDE.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before: old hooks format output
&lt;/h2&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KiroGraph Sync on Agent Stop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sync the KiroGraph index when the agent stops, picking up any file edits, creates, or deletes from the session."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"when"&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;"agentStop"&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;"then"&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;"runCommand"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kirograph sync --quiet 2&amp;gt;&amp;amp;1 &amp;gt; /dev/null"&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;h2&gt;
  
  
  After: new hooks format output
&lt;/h2&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;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KiroGraph Sync on Agent Stop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"trigger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kirograph sync --quiet 2&amp;gt;&amp;amp;1 &amp;gt; /dev/null"&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;h2&gt;
  
  
  KiroGraph
&lt;/h2&gt;

&lt;p&gt;Maintainers of tools like &lt;strong&gt;KiroGraph&lt;/strong&gt;, which automatically configure hooks for you, will likely update their installers and scripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KiroGraph&lt;/strong&gt; is ready for Kiro &lt;code&gt;v1.0.0&lt;/code&gt; and ensures backward compatibility for users still on the beta 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhaox0tzftpxcjeto9k1t.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhaox0tzftpxcjeto9k1t.png" alt=" " width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just update it to &lt;code&gt;v0.26.0&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you've been holding off on upgrading because you're worried about breaking your custom workflows, don't panic.&lt;/p&gt;

&lt;p&gt;Steering, Skills, and MCP integrations continue to work seamlessly, and even hook migrations can be largely automated by Kiro itself.&lt;/p&gt;

&lt;p&gt;Sometimes the best tool to fix a breaking change is the tool that introduced it.&lt;/p&gt;

&lt;h2&gt;
  
  
  🙋 Who am I
&lt;/h2&gt;

&lt;p&gt;I'm &lt;a href="https://www.linkedin.com/in/desiodavide" rel="noopener noreferrer"&gt;D. De Sio&lt;/a&gt; and I work as a Head of Software Engineering in &lt;a href="https://eleva.it/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;br&gt;
As of June 2026, I’m an &lt;a href="https://www.credly.com/badges/9929fdf2-7a3d-4013-9de6-57c80e4920b9/public_url" rel="noopener noreferrer"&gt;AWS Certified Solution Architect Professional&lt;/a&gt; and &lt;a href="https://www.credly.com/badges/8c5a1487-191b-429e-8c2d-7cee43bf316b/public_url" rel="noopener noreferrer"&gt;AWS Certified DevOps Engineer Professional&lt;/a&gt;, but also a &lt;a href="https://www.linkedin.com/company/aws-user-group-pavia/" rel="noopener noreferrer"&gt;User Group Leader (in Pavia)&lt;/a&gt;, an &lt;strong&gt;AWS Community Builder&lt;/strong&gt; and, last but not least, a #serverless enthusiast.&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%2Fllzr7x5kaub59zvn1od3.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%2Fllzr7x5kaub59zvn1od3.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kiro</category>
      <category>ai</category>
      <category>aws</category>
    </item>
    <item>
      <title>KiroGraph-Sec: From AWS Summit Milano Slides, Through Kiro Specs, to a Cybersecurity Feature</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Mon, 01 Jun 2026 08:16:46 +0000</pubDate>
      <link>https://dev.to/aws-builders/kirograph-sec-from-aws-summit-milano-slides-through-kiro-specs-to-a-cybersecurity-feature-12ch</link>
      <guid>https://dev.to/aws-builders/kirograph-sec-from-aws-summit-milano-slides-through-kiro-specs-to-a-cybersecurity-feature-12ch</guid>
      <description>&lt;p&gt;This is the third part of my "Build in Public with &lt;strong&gt;Kiro&lt;/strong&gt;" series. I'm an AWS Community Builder, and this is the story of what happens when you share something early, people actually care, and the project grows faster than you expected. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But also, the story of how a conference talk about VEX triage turned into a full SCA+ security module built spec-first with Kiro&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🏃 TL;DR
&lt;/h2&gt;

&lt;p&gt;Most SCA tools tell you "this dependency has a CVE." KiroGraph-Sec tells you whether that CVE is actually reachable from your HTTP routes, which entry points expose a hardcoded secret, and which public route reaches a vulnerable dep in two function calls. It does this by running a BFS over the same call graph KiroGraph already builds for code navigation.&lt;/p&gt;

&lt;p&gt;Built spec-first in Kiro after an AWS Summit Milano talk by &lt;a href="https://www.linkedin.com/in/argomauro/" rel="noopener noreferrer"&gt;Maurizio Argoneto (AWS Hero)&lt;/a&gt; on automating VEX triage with AI. Covers 14 ecosystems. Ships CycloneDX SBOM and VEX, SARIF for GitHub Security tab, an HTML security dashboard, and a full MCP tool suite so your AI agent can query the security index instead of scanning files.&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;
        kirograph
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Semantic code knowledge graph for Kiro: fewer tool calls, instant symbol lookups, 100% local.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Flogo.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;KiroGraph&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/terminal.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Fterminal.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Semantic code knowledge graph for &lt;a href="https://kiro.dev" rel="nofollow noopener noreferrer"&gt;Kiro&lt;/a&gt;: fewer tool calls, instant symbol lookups, 100% local.&lt;/p&gt;
&lt;p&gt;Inspired by &lt;a href="https://github.com/colbymchenry/codegraph" rel="noopener noreferrer"&gt;CodeGraph&lt;/a&gt; by &lt;a href="https://github.com/colbymchenry" rel="noopener noreferrer"&gt;colbymchenry&lt;/a&gt; for Claude Code, rebuilt natively for Kiro's MCP and hooks system.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full support is for Kiro only.&lt;/strong&gt; Experimental integrations for 34 other MCP-capable tools (Cursor, Copilot, Claude Code, Windsurf, Cline, and more) are available with auto-detection. See &lt;a href="https://github.com/davide-desio-eleva/kirograph/docs/guide/integrations.md" rel="noopener noreferrer"&gt;Integrations&lt;/a&gt; for the full list.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why KiroGraph?&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;When you ask Kiro to work on a complex task, it explores your codebase using file reads, grep, and glob searches. Every one of those is a tool call, and tool calls consume context and slow things down.&lt;/p&gt;
&lt;p&gt;KiroGraph gives Kiro a semantic knowledge graph that's pre-indexed and always up to date. Instead of scanning files to understand your code, Kiro queries the graph instantly: symbol relationships, call graphs, type hierarchies, impact radius, all in a single MCP tool call.&lt;/p&gt;
&lt;p&gt;The result is fewer tool…&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/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  🎤 It Started With a Slide Deck in Milano
&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%2Fidg8rnf0hqtadwzomceo.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%2Fidg8rnf0hqtadwzomceo.png" alt=" " width="800" height="935"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the AWS Summit Milano, Maurizio gave a talk titled &lt;strong&gt;"Chi ha messo questo nel mio codice? Dalla Confusione al Controllo: Automazione del Triage VEX con l'IA"&lt;/strong&gt; (roughly: &lt;em&gt;"Who put this in my code? From Confusion to Control: Automating VEX Triage with AI"&lt;/em&gt;).&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%2Fr5w8umts4cv4ybh4cvnc.jpeg" 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%2Fr5w8umts4cv4ybh4cvnc.jpeg" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The core argument was compelling and uncomfortable in equal measure: most developers today drop a question into an AI coding assistant and hope it figures out their security posture. &lt;strong&gt;That is not a strategy. That is delegation without accountability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Maurizio's thesis: AI is a great reasoning engine, but it needs &lt;em&gt;smart tooling underneath&lt;/em&gt; to do security work properly. When dealing with VEX documents (Vulnerability Exploitability eXchange), you do not want the AI to scan files blindly. You want it to query a semantic index that already understands your code structure. The AI should ask &lt;em&gt;"is this vulnerability reachable from my entry points?"&lt;/em&gt; against a pre-computed graph, not trawl through source files hoping to figure it out from context.&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%2F317i4z8nz9d1i8lxa3cs.jpeg" 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%2F317i4z8nz9d1i8lxa3cs.jpeg" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While listening to Maurizio, it became clear that KiroGraph already enabled this kind of navigation, and &lt;strong&gt;that was exactly how he was using it&lt;/strong&gt;. &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%2Fdubdyr2fwz390w36007p.jpeg" 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%2Fdubdyr2fwz390w36007p.jpeg" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The call graph, import edges, and architecture layers were already available. The dependency and vulnerability pieces were still missing, but the core foundation was already there.&lt;/p&gt;

&lt;p&gt;I took photos of the slides, walked home, and opened &lt;strong&gt;Kiro&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I would also like to sincerely thank Maurizio for giving KiroGraph the opportunity to be featured at such an important conference.&lt;/strong&gt;&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%2F35dh7n325tbmnvxx2qkm.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%2F35dh7n325tbmnvxx2qkm.png" alt=" " width="800" height="963"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📐 The Specs-First Approach
&lt;/h2&gt;

&lt;p&gt;I didn’t start by writing code first. I let Kiro write the &lt;strong&gt;specs&lt;/strong&gt; instead.&lt;/p&gt;

&lt;p&gt;I really want to test this approach: copy-pasting conference foto, giving a raw description of the idea discussed with Maurizio, and seeing what I get.&lt;/p&gt;

&lt;p&gt;Kiro in this modality generates three files, that effectively became the north star for the entire implementation: &lt;code&gt;requirements.md&lt;/code&gt;, &lt;code&gt;design.md&lt;/code&gt;, and &lt;code&gt;tasks.md&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Here is a fragment from the requirements doc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Introduction&lt;/span&gt;

KiroGraph-Sec is a security analysis module that extends KiroGraph's semantic
knowledge graph with dependency vulnerability detection and reachability-aware
impact analysis. Unlike traditional SCA tools that only report "vulnerable
dependency present," KiroGraph-Sec leverages the existing call graph and
architecture graph to determine whether vulnerable code paths are actually
reachable from the application's entry points.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single paragraph captures the fundamental insight from Maurizio's talk: &lt;strong&gt;the problem with most SCA tools is not that they miss vulnerabilities. It is that they find too many and tell you almost nothing about which ones actually matter.&lt;/strong&gt;&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%2Fol0a3qthrgbjo9d3h56w.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%2Fol0a3qthrgbjo9d3h56w.png" alt=" " width="800" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The design document defined the pipeline clearly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code extraction → reference resolution → architecture analysis → security analysis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was Kiro that figured out how the pipeline should run, and that the security analysis should depend on the previous steps, based on the KiroGraph code. Security runs &lt;em&gt;last&lt;/em&gt;, after the call graph exists, because it needs to traverse it. This is the architectural constraint that makes everything else possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Graph Model
&lt;/h3&gt;

&lt;p&gt;Two new node kinds, three new edge kinds. That is it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="n"&gt;dependency&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="n"&gt;has_vulnerability&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="n"&gt;vulnerability&lt;/span&gt;
&lt;span class="n"&gt;dependency&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="n"&gt;depends_on&lt;/span&gt;         &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="n"&gt;dependency&lt;/span&gt;
&lt;span class="n"&gt;dependency&lt;/span&gt;  &lt;span class="err"&gt;←&lt;/span&gt;  &lt;span class="n"&gt;declared_in&lt;/span&gt;        &lt;span class="err"&gt;←&lt;/span&gt;  &lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reachability verdict is the output of a BFS from all application entry points through call, import, and reference edges toward each &lt;code&gt;Dependency_Node&lt;/code&gt;. Three possible outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;affected&lt;/code&gt;&lt;/strong&gt;: a path exists. This CVE is reachable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;not_affected&lt;/code&gt;&lt;/strong&gt;: no path, no unresolved imports. You can document this with confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;under_investigation&lt;/code&gt;&lt;/strong&gt;: the traversal hit unresolved symbols (dynamic dispatch, reflection). Conservative by design: do not call it safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔄 The Iterative Build: From 5 Ecosystems to 14
&lt;/h2&gt;

&lt;p&gt;The first version of KiroGraph-Sec (0.19.0) shipped with the core pipeline: manifest parsing for npm, Maven, Go, pip, and Cargo; OSV integration; reachability analysis; and CycloneDX SBOM and VEX export.&lt;/p&gt;

&lt;p&gt;I needed a working implementation to validate Maurizio's thesis in practice. Once it was up and running, the results were immediately tangible.&lt;/p&gt;

&lt;p&gt;It was genuinely awesome: I found vulnerabilities in &lt;strong&gt;KiroGraph&lt;/strong&gt; itself and could then iterate directly with Kiro, asking it to generate fixes and close them. &lt;strong&gt;The whole loop, from discovery to remediation, worked far better than I had expected&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then I started closing gaps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reachability problem is what every other SCA tool avoids.&lt;/strong&gt; Snyk charges for it and limits it to JS/Python. Trivy does not do it. OWASP Dependency-Check does not do it. KiroGraph-Sec does it for free, across 14 ecosystems, using the same call graph that already powers &lt;code&gt;kirograph_context&lt;/code&gt; and &lt;code&gt;kirograph_impact&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The ecosystem list grew quickly: NuGet, Gradle, RubyGems, Composer, Swift PM, Dart/pub, Elixir/Hex, and crucially, &lt;strong&gt;&lt;code&gt;pyproject.toml&lt;/code&gt; support&lt;/strong&gt; for modern Python projects (Poetry, PDM, Hatch, PEP 621), because nobody writes &lt;code&gt;requirements.txt&lt;/code&gt; for new projects anymore. Plus &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; for the JavaScript monorepo crowd.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 What Makes It Different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Vulnerability That Does Not Actually Matter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kirograph vulns &lt;span class="nt"&gt;--verdict&lt;/span&gt; affected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Risk: 8.2]  CVE-2023-44270  postcss@8.4.21  [affected]
  CVSS 7.5  EPSS 0.12 / 54th%
  reaches via: POST /api/compile -&amp;gt; buildStylesheet -&amp;gt; postcss.process
  Fix: npm install postcss@8.4.31
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kirograph vulns &lt;span class="nt"&gt;--verdict&lt;/span&gt; not_affected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CVE-2024-1234  lodash@4.17.21  [not affected]
  CVSS 9.1  EPSS 0.03
  No reachable path from any entry point. No unresolved imports.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A CVSS 9.1 that does not affect you, and a CVSS 7.5 that does. Traditional SCA tools sort by CVSS and leave you to figure out the rest. KiroGraph-Sec sorts by &lt;strong&gt;risk score&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;risk_score = reachability_factor × (0.4 × CVSS_normalized + 0.6 × EPSS) × staleness_bonus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;reachability_factor&lt;/code&gt; is 1.0 for &lt;code&gt;affected&lt;/code&gt;, 0.5 for &lt;code&gt;under_investigation&lt;/code&gt;, and 0.1 for &lt;code&gt;not_affected&lt;/code&gt;. EPSS gets more weight than CVSS because it reflects actual exploitation probability in the wild, not theoretical severity.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Attack Surface Mapping
&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%2Fnh803tkrlgjiwuuu2fp0.jpeg" 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%2Fnh803tkrlgjiwuuu2fp0.jpeg" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the feature that Maurizio's talk most directly inspired. Not "which deps have CVEs?" but &lt;strong&gt;"which HTTP routes reach vulnerable dependencies?"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kirograph attack-surface &lt;span class="nt"&gt;--public-only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attack Surface  (47 routes total: 23 public, 24 authenticated)

Critical paths (routes reaching affected vulnerabilities):

[9.2] POST /api/compile      public      -&amp;gt; postcss@8.4.21 CVE-2023-44270 (2 hops)
[7.8] GET  /api/preview      public      -&amp;gt; lodash@4.17.20 CVE-2021-23337 (3 hops)
[4.1] POST /api/auth/login   public      -&amp;gt; bcrypt@5.0.0   CVE-2024-xxxx  (1 hop)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The BFS runs from every &lt;code&gt;route&lt;/code&gt; node through call/import/reference edges to &lt;code&gt;dependency&lt;/code&gt; nodes. The auth heuristic detects whether the path traverses functions named &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;authenticate&lt;/code&gt;, &lt;code&gt;guard&lt;/code&gt;, &lt;code&gt;middleware&lt;/code&gt;, etc. Public routes reaching vulnerable deps are your actual attack surface.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Secrets With Blast Radius
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kirograph security secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CRITICAL  AWS Access Key ID                    src/config/aws.ts:14
          AKIA****  in configureAWS()  reachable from 4 entry points
          Rotate this key immediately and use environment variables

HIGH      Database URL with credentials        src/db/connection.ts:8
          postgres://****  in createConnection()  reachable from 12 entry points
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every other secrets scanner tells you &lt;em&gt;file:line&lt;/em&gt;. This one tells you &lt;em&gt;file:line, function, and how many entry points expose that function&lt;/em&gt;. That is the call graph doing the work that used to require manual review.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤖 The MCP Integration: AI Does the Triage
&lt;/h2&gt;

&lt;p&gt;Here is where Maurizio's vision closes the loop. KiroGraph-Sec exposes its full analysis as MCP tools, which means the AI coding assistant can query it without reading files:&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="nf"&gt;kirograph_security&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2 affected vulnerabilities, 3 under investigation, 0 not affected.
   Top risk: CVE-2023-44270 (Risk: 8.2) in postcss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nf"&gt;kirograph_reachability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CVE-2023-44270&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Verdict: affected
   Reaching entry points: 1
   Path: POST /api/compile -&amp;gt; buildStylesheet -&amp;gt; postcss.process
   Affected layers: api, service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nf"&gt;kirograph_attack_surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publicOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3 public routes reach affected dependencies...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI is not scanning files. It is querying a pre-computed semantic index and getting structured, actionable answers. This is exactly what Maurizio was describing: &lt;strong&gt;use AI as a reasoning layer over smart tooling, not as a replacement for the tooling itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the agent is working on code that touches a vulnerable path, &lt;code&gt;kirograph_context&lt;/code&gt; will automatically surface a security warning inline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Security Warning&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; CVE-2023-44270 (CVSS 7.5, EPSS 0.12 / 54th%): postcss@8.4.21 (npm)
  reaches via: buildStylesheet
  Fix: npm install postcss@8.4.31
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moreover, KiroGraph-Sec introduces a set of security-focused MCP tools that allow the agent to answer the most common security, compliance, and software supply chain questions directly from the project graph. &lt;strong&gt;The goal is to provide actionable security insights through natural language queries, reducing the need to manually correlate information from multiple security tools and reports.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Example Question&lt;/th&gt;
&lt;th&gt;MCP Tool&lt;/th&gt;
&lt;th&gt;Why it was Developed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security Overview&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Give me a security overview of this project: total dependencies, CVE count, and verdict breakdown by reachability.&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_security()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To provide a quick assessment of the project's overall security posture.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reachability Analysis&lt;/td&gt;
&lt;td&gt;**&lt;em&gt;Is CVE-2023-44487 actually reachable from my entry points?&lt;/em&gt;  **&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_reachability()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To distinguish exploitable vulnerabilities from vulnerabilities that are merely present in dependencies.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SBOM Generation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Generate a CycloneDX 1.5 Software Bill of Materials for this project.&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_sbom()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To support compliance requirements and dependency inventory management.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VEX Export&lt;/td&gt;
&lt;td&gt;**&lt;em&gt;Export the Vulnerability Exploitability eXchange with reachability verdicts for every CVE.&lt;/em&gt;  **&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_vex()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To communicate exploitability and reachability information alongside vulnerability data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attack Surface Analysis&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Which HTTP routes expose vulnerable dependencies?&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_attack_surface()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To identify externally accessible attack paths associated with vulnerable components.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets Detection&lt;/td&gt;
&lt;td&gt;**&lt;em&gt;Are there any AWS keys, GitHub tokens, or database connection strings in the source code?&lt;/em&gt; **&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_secrets()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To detect exposed credentials and sensitive information in the codebase.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Flow Analysis&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Find SQL injection and path traversal patterns in the code.&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_security_flows()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To identify common insecure coding patterns and risky data flows.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supply Chain Health&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Are there any abandoned packages or newly added packages that could be a supply chain risk?&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_supply_chain()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To assess dependency health and detect potential supply-chain threats.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency Confusion&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Do we have internal packages whose names also exist in public registries?&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_dep_confusion()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To identify dependency confusion and typosquatting risks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remediation SLA Tracking&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Which CVEs have already breached their SLA threshold?&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_remediation()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To monitor vulnerability remediation timelines and policy compliance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License Compliance&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Are there any GPL or AGPL dependencies that violate our policy?&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_licenses()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To identify open-source license conflicts and compliance issues.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency Staleness&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Which dependencies are more than two major versions behind?&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_staleness()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To highlight outdated libraries that may introduce security or maintenance risks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerability Suppression&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Suppress CVE-2024-1234 until December 31, 2026.&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_vuln_suppress()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To manage false positives and accepted-risk scenarios.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual CVE Registration&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Manually register CVE-2025-0001 against an internal package.&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_vuln_add()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To track vulnerabilities affecting proprietary or internal software components.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulnerability Filtering&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;List all confirmed-reachable critical CVEs.&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kirograph_vulns()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;To enable focused analysis and prioritization of security findings.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD Security Reporting&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;em&gt;Generate a SARIF report and fail the pipeline if critical vulnerabilities are found.&lt;/em&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kirograph_security()&lt;/code&gt; + CI integration&lt;/td&gt;
&lt;td&gt;To automate security validation and enforce policies during software delivery.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Together, these MCP tools enable KiroGraph-Sec to &lt;strong&gt;move beyond simple vulnerability enumeration by providing context-aware security analysis, reachability assessment, supply-chain visibility, and compliance support directly through the agent interface.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🧰 Beyond the Core: The Full KiroGraph-Sec Suite
&lt;/h2&gt;

&lt;p&gt;After the initial release, the module grew into something broader than a traditional SCA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supply chain health.&lt;/strong&gt; For each npm/PyPI/Cargo dependency, KiroGraph-Sec fetches the OpenSSF Scorecard score, maintainer count, and days since last activity. A Scorecard of 2/10 combined with a single maintainer on a critical transitive dependency is a risk that CVSS does not capture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency confusion detection.&lt;/strong&gt; This checks whether your internal package names also exist in public registries. It is how the &lt;a href="https://github.com/advisories/GHSA-pjwm-rvh2-c87w" rel="noopener noreferrer"&gt;ua-parser-js attack&lt;/a&gt; worked. A one-line check in principle, but nobody does it automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SAST-lite via call graph.&lt;/strong&gt; Instead of AST-pattern matching (which generates enormous false positive rates), this uses the call graph to find dangerous data flows: SQL queries built from controller-like functions, &lt;code&gt;eval()&lt;/code&gt; calls reached from request handlers, &lt;code&gt;readFile&lt;/code&gt; calls in download-handling functions. Each finding is tagged with its OWASP Top 10 category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CI/CD integration.&lt;/strong&gt; &lt;code&gt;kirograph security ci-report --format sarif&lt;/code&gt; produces a valid SARIF 2.1.0 file that uploads directly to GitHub's Security tab as code scanning results. &lt;code&gt;--fail-on affected&lt;/code&gt; gives you a security gate with a clean exit code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remediation SLA tracking.&lt;/strong&gt; Knowing about a CVE is not the same as fixing it. KiroGraph-Sec records when each vulnerability was first detected, when a fix became available, and surfaces overdue items (critical: 7 days, high: 30 days, medium: 90 days).&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 The Dashboard
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kirograph security &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a self-contained HTML file, no server or external dependencies required, with 10 tabs: Overview, Vulnerabilities, Attack Surface, Secrets, SAST Flows, SBOM, VEX, Licenses, Staleness, Remediation. The Vulnerabilities tab defaults to sorting by risk score, shows EPSS badges, and lets you expand each CVE to see the call paths. The SBOM and VEX tabs include one-click CycloneDX JSON download.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 What the Kiro Specs Workflow Taught Me
&lt;/h2&gt;

&lt;p&gt;Writing specs before code, particularly with Kiro, forced decisions that would otherwise have been deferred to implementation time. The requirements document defined what a &lt;code&gt;Reachability_Verdict&lt;/code&gt; meant before a single line of traversal code existed. The design document resolved the pipeline ordering question (&lt;code&gt;security runs after architecture&lt;/code&gt;) before the two modules had any coupling.&lt;/p&gt;

&lt;p&gt;When Kiro generated tasks from those specs, it produced a dependency graph that was almost exactly right. The implementation followed the spec, not the other way around.&lt;/p&gt;

&lt;p&gt;The most useful line in the entire spec turned out to be this one, from the design doc:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The key differentiator from traditional SCA tools is the reachability analysis: by leveraging KiroGraph's existing call graph, import edges, and architecture layers, KiroGraph-Sec can classify vulnerabilities as &lt;code&gt;affected&lt;/code&gt;, &lt;code&gt;not_affected&lt;/code&gt;, or &lt;code&gt;under_investigation&lt;/code&gt; based on whether vulnerable code is actually reachable from the application's entry points.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&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;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; kirograph
kirograph init &lt;span class="nt"&gt;--index&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then enable the security module in &lt;code&gt;.kirograph/config.json&lt;/code&gt;:&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;"enableArchitecture"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enableSecurity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"securityDatabases"&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="s2"&gt;"OSV"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"securityAutoEnrich"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;Run &lt;code&gt;kirograph index&lt;/code&gt;, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kirograph security               &lt;span class="c"&gt;# overview&lt;/span&gt;
kirograph vulns                  &lt;span class="c"&gt;# sorted by risk score&lt;/span&gt;
kirograph attack-surface         &lt;span class="c"&gt;# routes to vulnerable deps&lt;/span&gt;
kirograph security &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--open&lt;/span&gt; &lt;span class="c"&gt;# full dashboard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If KiroGraph-Sec is enabled, you will get a convenient steering file with examples on how to run a security audit workflow. In Kiro IDE, type &lt;code&gt;/kirograph-security&lt;/code&gt; to activate the step-by-step security audit workflow.&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;inclusion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="c1"&gt;# KiroGraph: Security Audit Workflow&lt;/span&gt;

&lt;span class="s"&gt;Follow these steps for a structured security audit using the knowledge graph.&lt;/span&gt;
&lt;span class="s"&gt;Activate this workflow before a release, after adding dependencies, or when asked to review security posture.&lt;/span&gt;

&lt;span class="c1"&gt;## Steps&lt;/span&gt;

&lt;span class="c1"&gt;### 1. Overview&lt;/span&gt;
&lt;span class="s"&gt;kirograph_security()&lt;/span&gt;

&lt;span class="na"&gt;Note&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total dependencies, vulnerability count, verdict breakdown, stale warning count.&lt;/span&gt;

&lt;span class="c1"&gt;### 2. Triage reachable vulnerabilities&lt;/span&gt;

&lt;span class="na"&gt;kirograph_vulns(verdict&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;affected")&lt;/span&gt;

&lt;span class="s"&gt;Focus only on confirmed reachable CVEs. Sort output by EPSS score (exploitation probability) first, then CVSS severity.&lt;/span&gt;

&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*Act&lt;/span&gt; &lt;span class="s"&gt;immediately on:** EPSS &amp;gt;= 0.5 (actively exploited). Patch regardless of CVSS.&lt;/span&gt;
&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*Prioritize&lt;/span&gt;&lt;span class="s"&gt;:** EPSS 0.1–0.5 over low-EPSS high-CVSS entries.&lt;/span&gt;
&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="nv"&gt;*Low&lt;/span&gt; &lt;span class="s"&gt;urgency:** EPSS &amp;lt; 0.1 — use CVSS + reachability for triage.&lt;/span&gt;

&lt;span class="c1"&gt;### 3. Deep-dive reachability for critical CVEs&lt;/span&gt;
&lt;span class="na"&gt;For each high-priority CVE from step 2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;kirograph_reachability(target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;CVE-ID&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;package&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name&amp;gt;")&lt;/span&gt;

&lt;span class="na"&gt;This shows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exact call paths from entry points, affected architectural layers, distinct path count.&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;affected` verdict with known entry points → fix this dependency&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;not_affected` → no reachable path, document and move on&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;under_investigation` → unresolved symbols, treat conservatively&lt;/span&gt;

&lt;span class="c1"&gt;### 4. Check for under-investigation CVEs&lt;/span&gt;

&lt;span class="na"&gt;kirograph_vulns(verdict&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;under_investigation")&lt;/span&gt;

&lt;span class="na"&gt;For each&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run `kirograph_reachability` to see what symbols are unresolved. If you can determine&lt;/span&gt;
&lt;span class="s"&gt;the symbol is not called, you can downgrade to not_affected manually.&lt;/span&gt;

&lt;span class="c1"&gt;### 5. License compliance&lt;/span&gt;

&lt;span class="na"&gt;kirograph_licenses(policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;

&lt;span class="s"&gt;Review any DENY violations — these must be resolved before shipping.&lt;/span&gt;
&lt;span class="s"&gt;WARN violations should be documented and approved by the team.&lt;/span&gt;

&lt;span class="c1"&gt;### 6. Dependency staleness&lt;/span&gt;

&lt;span class="na"&gt;kirograph_staleness(threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.5)&lt;/span&gt;

&lt;span class="na"&gt;Score guide&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.3+ = worth reviewing, 0.7+ = significantly behind.&lt;/span&gt;
&lt;span class="na"&gt;Cross-reference with step 2 results&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stale + vulnerable = highest priority.&lt;/span&gt;

&lt;span class="c1"&gt;### 7. Refresh data if needed&lt;/span&gt;
&lt;span class="na"&gt;If vulnerability data looks stale (flagged in step 1) or dependencies changed recently&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;kirograph_vulns(refresh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;### 8. Export compliance artifacts&lt;/span&gt;

&lt;span class="s"&gt;kirograph_sbom()   // Software Bill of Materials&lt;/span&gt;
&lt;span class="s"&gt;kirograph_vex()    // Vulnerability Exploitability eXchange&lt;/span&gt;


&lt;span class="c1"&gt;## Interpretation Reference&lt;/span&gt;

&lt;span class="pi"&gt;|&lt;/span&gt; &lt;span class="err"&gt;Signal&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="err"&gt;Meaning&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="err"&gt;Action&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt;--------|---------|--------|&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; `affected` + EPSS &amp;gt;= 0.5 | Actively exploited, reachable | Patch immediately |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; `affected` + CVSS &amp;gt;= 9.0 | Critical, reachable | Patch this sprint |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; `affected` + CVSS 7.0–8.9 | High, reachable | Plan fix within 2 weeks |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; `not_affected` | No reachable path found | Document, no action needed |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; `under_investigation` | Reachability unclear | Manual review required |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; Stale &amp;gt;= 0.7 | Very outdated | Review for accumulated CVEs |&lt;/span&gt;
&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s"&gt; License DENY | Policy violation | Must resolve before release |&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  🏁 Conclusion
&lt;/h2&gt;

&lt;p&gt;Maurizio's talk framed the problem perfectly: AI without smart tooling is just pattern-matching over files. The value comes from giving it a structured index to query, one that understands your code's call graph, architecture, and dependencies simultaneously.&lt;/p&gt;

&lt;p&gt;KiroGraph-Sec is that index for security. It does not replace the AI agent. It gives it something worth querying.&lt;/p&gt;

&lt;p&gt;The full source is at &lt;a href="https://github.com/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;github.com/davide-desio-eleva/kirograph&lt;/a&gt;. The security module ships as part of version 0.19.0.&lt;/p&gt;

&lt;p&gt;As per other modules, KiroGraph-Sec has it's own inspired mascotte.&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%2Fejkyep6vel97o2tpv54m.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%2Fejkyep6vel97o2tpv54m.png" alt=" " width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🙋 Who am I
&lt;/h2&gt;

&lt;p&gt;I'm &lt;a href="https://www.linkedin.com/in/desiodavide" rel="noopener noreferrer"&gt;D. De Sio&lt;/a&gt; and I work as a Head of Software Engineering in &lt;a href="https://eleva.it/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;br&gt;
As of June 2026, I’m an &lt;a href="https://www.credly.com/badges/9929fdf2-7a3d-4013-9de6-57c80e4920b9/public_url" rel="noopener noreferrer"&gt;AWS Certified Solution Architect Professional&lt;/a&gt; and &lt;a href="https://www.credly.com/badges/8c5a1487-191b-429e-8c2d-7cee43bf316b/public_url" rel="noopener noreferrer"&gt;AWS Certified DevOps Engineer Professional&lt;/a&gt;, but also a &lt;a href="https://www.linkedin.com/company/aws-user-group-pavia/" rel="noopener noreferrer"&gt;User Group Leader (in Pavia)&lt;/a&gt;, an &lt;strong&gt;AWS Community Builder&lt;/strong&gt; and, last but not least, a #serverless enthusiast.&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%2Fllzr7x5kaub59zvn1od3.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%2Fllzr7x5kaub59zvn1od3.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>buildinpublic</category>
      <category>cybersecurity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>KiroGraph: from a personal side project to community-AI-driven tool</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Wed, 20 May 2026 07:37:28 +0000</pubDate>
      <link>https://dev.to/aws-builders/kirograph-from-a-personal-side-project-to-community-ai-driven-tool-395e</link>
      <guid>https://dev.to/aws-builders/kirograph-from-a-personal-side-project-to-community-ai-driven-tool-395e</guid>
      <description>&lt;p&gt;This is the second part of my "Build in Public with Kiro" series. I'm an AWS Community Builder, and this is the story of what happens when you share something early, people actually care, and the project grows faster than you expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏃 TL;DR
&lt;/h2&gt;

&lt;p&gt;Since the first article, KiroGraph went from a CLI agent prototype (v0.5.0) to a multi-client, 30+ language, architecture-aware code knowledge graph (v0.13.1). It's now published on npm, has a full documentation site, an interactive graph dashboard, and supports Claude Code and Codex alongside Kiro. Most of this happened because people showed up, contributed, and pushed the project in directions I hadn't planned.&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;
        kirograph
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Semantic code knowledge graph for Kiro: fewer tool calls, instant symbol lookups, 100% local.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Flogo.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;KiroGraph&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/terminal.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Fterminal.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Semantic code knowledge graph for &lt;a href="https://kiro.dev" rel="nofollow noopener noreferrer"&gt;Kiro&lt;/a&gt;: fewer tool calls, instant symbol lookups, 100% local.&lt;/p&gt;
&lt;p&gt;Inspired by &lt;a href="https://github.com/colbymchenry/codegraph" rel="noopener noreferrer"&gt;CodeGraph&lt;/a&gt; by &lt;a href="https://github.com/colbymchenry" rel="noopener noreferrer"&gt;colbymchenry&lt;/a&gt; for Claude Code, rebuilt natively for Kiro's MCP and hooks system.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full support is for Kiro only.&lt;/strong&gt; Experimental integrations for 34 other MCP-capable tools (Cursor, Copilot, Claude Code, Windsurf, Cline, and more) are available with auto-detection. See &lt;a href="https://github.com/davide-desio-eleva/kirograph/docs/guide/integrations.md" rel="noopener noreferrer"&gt;Integrations&lt;/a&gt; for the full list.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why KiroGraph?&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;When you ask Kiro to work on a complex task, it explores your codebase using file reads, grep, and glob searches. Every one of those is a tool call, and tool calls consume context and slow things down.&lt;/p&gt;
&lt;p&gt;KiroGraph gives Kiro a semantic knowledge graph that's pre-indexed and always up to date. Instead of scanning files to understand your code, Kiro queries the graph instantly: symbol relationships, call graphs, type hierarchies, impact radius, all in a single MCP tool call.&lt;/p&gt;
&lt;p&gt;The result is fewer tool…&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/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  📈 Where we left off
&lt;/h2&gt;

&lt;p&gt;The first post covered the core idea: pre-index your codebase into a semantic graph so the AI agent queries structure instead of re-reading files. At that point KiroGraph had structural indexing, seven semantic engines, an interactive installer, and a CLI agent for kiro-cli.&lt;/p&gt;

&lt;p&gt;That was v0.5.0. Here's what happened next.&lt;/p&gt;

&lt;h2&gt;
  
  
  👻 Kiro acceleration effect
&lt;/h2&gt;

&lt;p&gt;I want to be honest about something: this pace of development wouldn't have been possible without Kiro.&lt;/p&gt;

&lt;p&gt;KiroGraph went from v0.5.0 to v0.13.1 in about five weeks, adding architecture analysis, 14 new languages, 17 framework resolvers, an interactive graph dashboard, multi-client support, npm publication, a documentation site, and dozens of bug fixes. As a side project. With a day job.&lt;/p&gt;

&lt;p&gt;Kiro's spec-driven development is the reason. When I need to add a new framework resolver, I don't start from a blank file. I write a spec, Kiro breaks it into tasks, and the implementation flows from there. The agent understands the existing patterns (especially with KiroGraph indexing itself), so new code fits the architecture without me hand-holding every decision.&lt;/p&gt;

&lt;p&gt;But here's where it gets interesting for open source specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔄 Simpler to contribute
&lt;/h2&gt;

&lt;p&gt;When contributors work in a TypeScript codebase that isn't their primary stack, it's Kiro that makes that possible. The spec-driven approach gives structure to the work, the agent handles the TypeScript idioms, and the contributor focuses on what they actually know: the domain expertise. Someone who knows Elixir deeply can contribute a full Phoenix framework resolver without being a TypeScript expert. Someone who understands how Claude Code's MCP configuration works can wire up a multi-client integration without knowing the internals of the indexing pipeline.&lt;/p&gt;

&lt;p&gt;I received PRs for features I wouldn't have imagined building myself, in domains where I lack expertise, and they were solid. My role shifted from "implement everything" to "review and guide". That's a fundamentally different way to run an open source project.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔀 It works the other way around as well
&lt;/h2&gt;

&lt;p&gt;Community members bring ideas from their domain. Architecture analysis, coupling metrics, layer detection patterns. These came from people who understood the problem space deeply. I had the codebase context and the TypeScript implementation skills. Kiro bridged the gap: their ideas became specs, specs became tasks, tasks became working code. Fast.&lt;/p&gt;

&lt;p&gt;This is the pattern I didn't anticipate: community members bring domain knowledge and ideas, Kiro accelerates the implementation regardless of who writes the code, and the project ships features that no single person would have built alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌐 People as global reviewer
&lt;/h2&gt;

&lt;p&gt;When you build in public with an AI-accelerated workflow, the community acts as a global reviewer. People try KiroGraph on codebases I've never seen, in languages I don't write daily, with frameworks I've never used. They find the bugs that only surface in production-like conditions.&lt;/p&gt;

&lt;p&gt;he multi-language call edge bug in v0.12.0 had actually existed since v0.1.0. Nobody noticed because nobody was using KiroGraph on a Java project until someone did.&lt;/p&gt;

&lt;p&gt;Speed matters here and when a bug report comes in, I can fix it very quickly. Kiro understands the codebase, the fix is scoped, the PR is merged, and the reporter sees their issue resolved in hours, not weeks. That responsiveness builds trust, and trust brings more contributors.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ Acceleration is real
&lt;/h2&gt;

&lt;p&gt;I'm not being hyperbolic: combination of Kiro's spec-driven development, KiroGraph's self-indexing feedback loop, and a community that both contributes code and stress-tests the result creates a development velocity that feels disproportionate to the effort. One person maintaining a 30+ language code analysis tool with 18 MCP tools, seven semantic engines, and an interactive dashboard shouldn't be sustainable. But it is, because the tooling makes it sustainable.&lt;/p&gt;

&lt;p&gt;If you're hesitant to contribute to a project because the stack isn't your primary one, Kiro removes that barrier: the domain expertise is yours while the implementation details are handled.&lt;/p&gt;

&lt;h2&gt;
  
  
  👥 Kiro Community
&lt;/h2&gt;

&lt;p&gt;As mentioned earlier, the project's trajectory changed because people from *Kiro Community engaged with it. Two main examples:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/argomauro" rel="noopener noreferrer"&gt;Maurizio Argoneto&lt;/a&gt; commented on the first post about "hierarchical summarization" and the "big picture" problem. That conversation directly influenced the architecture analysis feature, one of the most useful additions for understanding large codebases at a high level.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/alessandrofranceschi/" rel="noopener noreferrer"&gt;Alessandro Franceschi&lt;/a&gt; contributed Elixir/Phoenix language support and the multi-client integration (Claude Code and Codex), expanding KiroGraph well beyond its original Kiro-only scope.&lt;br&gt;
I was initially reluctant to open this project to other AI clients, agents, or IDEs, but Alessandro explained the situation to me very clearly: people are using Kiro in a wide range of ways and within a multi-tool AI development process that fits their needs. Restricting this project to Kiro alone would, in my view, mean closing the door to those who actually want to use it in this scenario. So I’m not opening it to competing AIs, but rather to complementary ones.&lt;/p&gt;

&lt;p&gt;The stargazers, the folks who opened issues, the people who tried it on their own codebases and reported what broke. All of that feedback shaped what got built and in what order. That's the kind of feedback loop that makes building in public worthwhile.&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%2F4g3xshdxntpgmssjghdu.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%2F4g3xshdxntpgmssjghdu.png" alt=" " width="799" height="510"&gt;&lt;/a&gt;&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%2Fb8vdaq3t8mio5395cwbg.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%2Fb8vdaq3t8mio5395cwbg.png" alt=" " width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kiro Community is really outstanding in this.&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%2Ft4dp5bccytg41dlhttrz.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%2Ft4dp5bccytg41dlhttrz.png" alt=" " width="800" height="916"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project has been recognized by AWS Community Builders team&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%2Fmdcm486hdmu8z26bh3r8.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%2Fmdcm486hdmu8z26bh3r8.png" alt=" " width="799" height="607"&gt;&lt;/a&gt;&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%2Fg282swa7w4w7ei8q66gd.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%2Fg282swa7w4w7ei8q66gd.png" alt=" " width="710" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And also enthusiastically endorsed by Kiro Ambassadors&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%2Fcs6t93dy4yytl4m2rlke.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%2Fcs6t93dy4yytl4m2rlke.png" alt=" " width="799" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Noted by Vice President at Amazon Web Services&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%2Fat9a8jmon2i732o0wya4.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%2Fat9a8jmon2i732o0wya4.png" alt=" " width="800" height="182"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Loved by far more people than I might imagine (those are just sample, I may be missing some screenshots, apologies for that!)&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%2F3fizcze3eko45cyqjft2.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%2F3fizcze3eko45cyqjft2.png" alt=" " width="564" height="236"&gt;&lt;/a&gt;&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%2Fp4fd0zv8rkjymxmoign7.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%2Fp4fd0zv8rkjymxmoign7.png" alt=" " width="800" height="183"&gt;&lt;/a&gt;&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%2F8bc0e2zxmwxeu5qcp22r.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%2F8bc0e2zxmwxeu5qcp22r.png" alt=" " width="799" height="164"&gt;&lt;/a&gt;&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%2F301xubjwg228t7daamid.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%2F301xubjwg228t7daamid.png" alt=" " width="800" height="158"&gt;&lt;/a&gt;&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%2Fisnsqqci1u3datcvmslc.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%2Fisnsqqci1u3datcvmslc.png" alt=" " width="800" height="191"&gt;&lt;/a&gt;&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%2Fxrou4mkoh2owve51fheo.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%2Fxrou4mkoh2owve51fheo.png" alt=" " width="799" height="193"&gt;&lt;/a&gt;&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%2F06tj4an4kn3myw26b0pn.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%2F06tj4an4kn3myw26b0pn.png" alt=" " width="799" height="161"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  📊 New features by numbers
&lt;/h2&gt;

&lt;p&gt;As a result, from v0.5.0 to v0.13.1, I've added several features:&lt;/p&gt;

&lt;p&gt;30+ languages supported (up from 17)&lt;br&gt;
17 framework resolvers added&lt;br&gt;
18 MCP tools (up from 12)&lt;br&gt;
Interactive graph dashboard with search, path finding, clustering, heat maps&lt;br&gt;
Architecture analysis with coupling metrics&lt;br&gt;
Multi-client support (Kiro, Claude Code, Codex)&lt;br&gt;
Full documentation site&lt;br&gt;
Published on npm&lt;br&gt;
🏗️ What got built&lt;br&gt;
Now let's walk through the features themselves.&lt;/p&gt;
&lt;h2&gt;
  
  
  🏛️ Architecture analysis (v0.6.0)
&lt;/h2&gt;

&lt;p&gt;An architecture layer that detects packages (via manifest files like package.json, go.mod, Cargo.toml, pom.xml, etc.) and assigns files to architectural layers: api, service, data, ui, shared. Each layer uses language-specific glob patterns, so a Python Flask project and a TypeScript Express project both get correctly classified.&lt;/p&gt;

&lt;p&gt;On top of that, KiroGraph now computes coupling metrics between packages: afferent coupling (Ca), efferent coupling (Ce), and instability (Ce / (Ca + Ce)). Three new MCP tools expose this to the agent: kirograph_architecture, kirograph_coupling, kirograph_package.&lt;/p&gt;

&lt;p&gt;The practical effect: when Kiro needs to understand where a new feature should live, or whether a refactor will create circular dependencies between packages, it can query the architecture graph instead of guessing from file paths.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧠 Embedding model selection (v0.7.0)
&lt;/h2&gt;

&lt;p&gt;The installer now presents an arrow-key menu with four curated models plus a custom option. The embeddingDim config field means all vector engines adapt automatically. Migrated from @xenova/transformers (v2) to @huggingface/transformers (v3), enabling support for modern ONNX models.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔧 The esbuild migration (v0.8.0)
&lt;/h2&gt;

&lt;p&gt;The old tsc build took 5-10 seconds. After migrating to esbuild + tsx, builds dropped to ~400ms with incremental watch mode. Type checking is now decoupled (npm run typecheck), so you get fast feedback during development and correctness checks when you need them.&lt;/p&gt;

&lt;p&gt;Small change, big quality-of-life improvement. Especially when you're iterating on the tool using the tool itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  🪨 Caveman mode (v0.9.0)
&lt;/h2&gt;

&lt;p&gt;Inspired by caveman by JuliusBrussee. The insight: KiroGraph's graph tools already return compact, structured data. The bottleneck in long sessions isn't the tool calls, it's the verbose prose the agent wraps around them.&lt;/p&gt;

&lt;p&gt;Caveman mode compresses the agent's communication style. Four levels: off, lite, full, ultra. At ultra, you get abbreviations, arrows for causality, maximum compression. The rules are injected via the steering file and CLI agent prompt, so they're always in context with zero extra tool calls.&lt;/p&gt;

&lt;p&gt;It never touches code blocks, file paths, or technical terms. Only prose. And it auto-reverts to normal for security warnings or irreversible actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;kirograph&lt;/span&gt; &lt;span class="n"&gt;caveman&lt;/span&gt; &lt;span class="n"&gt;ultra&lt;/span&gt;   &lt;span class="c"&gt;# maximum compression
&lt;/span&gt;&lt;span class="n"&gt;kirograph&lt;/span&gt; &lt;span class="n"&gt;caveman&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt;     &lt;span class="c"&gt;# back to normal
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  📊 Hotspots, snapshots &amp;amp; dead code (v0.10.0)
&lt;/h2&gt;

&lt;p&gt;Three new MCP tools that give the agent analytical capabilities:&lt;/p&gt;

&lt;p&gt;kirograph_hotspots finds the most-connected symbols by edge degree. Before you touch a function, you want to know if it's called from 3 places or 300.&lt;br&gt;
kirograph_surprising finds non-obvious cross-file connections. Scores edges by path distance × kind weight. The highest-scoring pairs are the ones that represent unexpected coupling worth investigating.&lt;br&gt;
kirograph_diff compares the current graph against a saved snapshot. Save before a refactor, diff after. See exactly what changed structurally.&lt;br&gt;
Plus CLI commands for all of them, and a snapshot system (kirograph snapshot save|list|diff) that stores lightweight graph states in .kirograph/snapshots/.&lt;/p&gt;
&lt;h2&gt;
  
  
  🎨 Interactive graph dashboard (v0.11.0)
&lt;/h2&gt;

&lt;p&gt;kirograph export now renders a full interactive graph visualization. No server required, works offline, three static files.&lt;/p&gt;

&lt;p&gt;This is the kind of thing that's hard to describe in text. You open it in a browser and suddenly the structure of your codebase is visible. Where the hotspots are, which modules are tightly coupled, where the dead ends live.&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%2Fgic1fqfoeu43uaqpkd6m.webp" 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%2Fgic1fqfoeu43uaqpkd6m.webp" alt=" " width="600" height="326"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🧬 Elixir, Phoenix, Java/C# edge cases (v0.12.0)
&lt;/h2&gt;

&lt;p&gt;Full Elixir language support: modules, functions, macros, protocols, implementations, structs. Plus Phoenix framework detection with route extraction from router.ex.&lt;/p&gt;

&lt;p&gt;The bigger fix in this release was improving multi-language call edge extraction. It turned out that walkForCalls only recognized call_expression (JS/TS/Go/Rust), which meant C#, Java, Python, Ruby, and PHP produced zero call edges. The kirograph_callers and kirograph_callees tools were silently returning nothing for those languages. Fixed now, with per-language name extraction using tree-sitter field lookups.&lt;/p&gt;

&lt;p&gt;Same story for inheritance edges in C# and Java, and namespace/package import resolution. These were the kind of bugs that only surface when real people try the tool on real codebases in languages other than TypeScript.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔄 Sync progress &amp;amp; stability (v0.12.1)
&lt;/h2&gt;

&lt;p&gt;Large codebase support got serious attention here. Paginated embedding (pages of 2,000 instead of loading everything into memory), WASM parser poisoning detection (skip a language if its parser aborts instead of retrying every file), sync progress output, and a staleness warning in kirograph_status when the index is behind.&lt;/p&gt;

&lt;p&gt;The MCP sync awareness is particularly nice: when pending unindexed files exceed a threshold, the agent gets a warning and can choose to wait rather than working with stale data.&lt;/p&gt;
&lt;h2&gt;
  
  
  📚 Documentation site &amp;amp; npm (v0.12.2)
&lt;/h2&gt;

&lt;p&gt;KiroGraph is now published on &lt;code&gt;npm (npm install -g kirograph)&lt;/code&gt; and has a full documentation site with dark theme, responsive layout, scroll-spy navigation, and pages for docs, MCP tools, CLI reference, and changelog.&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%2Fop8m8n1c6etac9t4my06.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%2Fop8m8n1c6etac9t4my06.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was overdue. A tool that saves tokens shouldn't require reading source code to understand how to use it.&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%2F9eu6nyf29llmrb7id5uj.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%2F9eu6nyf29llmrb7id5uj.png" alt=" " width="799" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And also how it evolves&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%2F9p64i4muxoumc4h0b79k.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%2F9p64i4muxoumc4h0b79k.png" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🌍 The big language expansion (v0.13.0)
&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%2Fsp7cy84iuaz2rqbn57kh.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%2Fsp7cy84iuaz2rqbn57kh.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;14 new languages in one release: Scala, Lua, Zig, Bash, OCaml, Elm, Solidity, Vue, Objective-C, YAML, HCL/Terraform, CSS, SCSS, HTML.&lt;/p&gt;

&lt;p&gt;So many languages that I had to add a search feature to the docs.&lt;/p&gt;

&lt;p&gt;17 new framework resolvers: Play (Scala), Nuxt/Vue, Solidity (Hardhat/Foundry/Truffle), SST, AWS CDK, Serverless Framework, AWS SAM, Terraform/OpenTofu, Pulumi, CloudFormation, Kubernetes/Helm, Docker Compose, Ansible, Angular, and AWS Amplify Gen 2.&lt;/p&gt;

&lt;p&gt;My best pick: since I regularly work on IaC projects, why shouldn’t I use KiroGraph for them?&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%2Fx195gmz167p6c6093jbo.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%2Fx195gmz167p6c6093jbo.png" alt=" " width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is where the project's scope expanded significantly. KiroGraph started as a TypeScript-focused tool. Now it indexes infrastructure-as-code (Terraform resources, CloudFormation stacks, Kubernetes manifests), smart contracts (Solidity with Hardhat/Foundry detection), and serverless configurations (SAM, CDK, SST handler resolution).&lt;/p&gt;

&lt;p&gt;The framework resolvers are particularly useful: they understand that a handler string like "src/handlers/auth.handler" in a SAM template points to an actual function symbol, and they create the edge in the graph. The agent can then trace from an API route all the way to the implementation without reading YAML files.&lt;/p&gt;
&lt;h2&gt;
  
  
  🤝 Multi-client support (v0.13.1)
&lt;/h2&gt;

&lt;p&gt;KiroGraph can now be installed for Claude Code and Codex in addition to Kiro.&lt;/p&gt;

&lt;p&gt;kirograph install --target claude  # .mcp.json + .kirograph/claude.md + CLAUDE.md import&lt;br&gt;
kirograph install --target codex   # .kirograph/codex.md + AGENTS.md block&lt;br&gt;
All targets share the same .kirograph/ data. Installing another target only writes that tool's integration files. The graph is the graph, regardless of which agent queries it.&lt;/p&gt;

&lt;p&gt;Kiro remains the primary supported target. Claude Code and Codex are marked experimental, but they work. The community asked for it, and it made sense: the value of a pre-indexed code graph isn't tied to a specific IDE.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔭 What's next
&lt;/h2&gt;

&lt;p&gt;The roadmap from the first post is mostly done.&lt;/p&gt;

&lt;p&gt;What's left and what's new:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smarter sync: content hash per embedding to skip unchanged symbols even when the file was modified. The paginated embedding in v0.12.1 was a step, but there's more to gain.&lt;/li&gt;
&lt;li&gt;Cross-project search: for monorepos and workspaces with shared libraries. The graph is per-project right now.&lt;/li&gt;
&lt;li&gt;Richer graph traversal: "explain this path" semantically, not just the nodes but why each edge exists.&lt;/li&gt;
&lt;li&gt;Plugin system for engines and languages: the abstractions are clean enough that external contributions could be self-contained packages.&lt;/li&gt;
&lt;li&gt;Kiro Power packaging: embedding KiroGraph into a configurable Kiro Power to reduce friction for folks who just want to install and go.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But in reality, the community is shaping the roadmap: this tool is designed to save tokens and improve the AI’s understanding of the provided code, so it is the users who ultimately determine what matters most.&lt;/p&gt;

&lt;p&gt;This is happening daily:&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%2Fnaxugorql0tyr9hp15l7.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%2Fnaxugorql0tyr9hp15l7.png" alt=" " width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What’s really on my mind: I’m particularly curious about what it would mean to integrate features from RTK into KiroGraph. I’m not aiming for a “one tool for everything” approach, but if I had to prioritize, this is probably what I’d focus on first (spoiler here).&lt;/p&gt;

&lt;p&gt;🎯 Try it&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; kirograph
&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
kirograph &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The installer walks you through everything. Start with cosine if you're not sure which engine to pick. Switch later if you need to.&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;
        kirograph
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Semantic code knowledge graph for Kiro: fewer tool calls, instant symbol lookups, 100% local.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Flogo.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;KiroGraph&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/terminal.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Fterminal.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Semantic code knowledge graph for &lt;a href="https://kiro.dev" rel="nofollow noopener noreferrer"&gt;Kiro&lt;/a&gt;: fewer tool calls, instant symbol lookups, 100% local.&lt;/p&gt;

&lt;p&gt;Inspired by &lt;a href="https://github.com/colbymchenry/codegraph" rel="noopener noreferrer"&gt;CodeGraph&lt;/a&gt; by &lt;a href="https://github.com/colbymchenry" rel="noopener noreferrer"&gt;colbymchenry&lt;/a&gt; for Claude Code, rebuilt natively for Kiro's MCP and hooks system.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full support is for Kiro only.&lt;/strong&gt; Experimental integrations for 34 other MCP-capable tools (Cursor, Copilot, Claude Code, Windsurf, Cline, and more) are available with auto-detection. See &lt;a href="https://github.com/davide-desio-eleva/kirograph/docs/guide/integrations.md" rel="noopener noreferrer"&gt;Integrations&lt;/a&gt; for the full list.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why KiroGraph?&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;When you ask Kiro to work on a complex task, it explores your codebase using file reads, grep, and glob searches. Every one of those is a tool call, and tool calls consume context and slow things down.&lt;/p&gt;

&lt;p&gt;KiroGraph gives Kiro a semantic knowledge graph that's pre-indexed and always up to date. Instead of scanning files to understand your code, Kiro queries the graph instantly: symbol relationships, call graphs, type hierarchies, impact radius, all in a single MCP tool call.&lt;/p&gt;

&lt;p&gt;The result is fewer tool…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;The repo is public. PRs are welcome. If you build on it, find a bug, or have thoughts on where it should go next, I'd love to hear from you.&lt;/p&gt;

&lt;p&gt;A very special thanks to the Stargazers, your support means a lot and truly makes a difference.&lt;/p&gt;

&lt;p&gt;🙋 Who am I&lt;br&gt;
I'm D. De Sio and I work as a Head of Software Engineering in Eleva.&lt;br&gt;
As of Feb 2026, I’m an AWS Certified Solution Architect Professional and AWS Certified DevOps Engineer Professional, but also a User Group Leader (in Pavia), an AWS Community Builder and, last but not least, a #serverless enthusiast.&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%2Fllzr7x5kaub59zvn1od3.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%2Fllzr7x5kaub59zvn1od3.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kiro</category>
      <category>ai</category>
      <category>aws</category>
    </item>
    <item>
      <title>Building KiroGraph: a 100% local semantic code knowledge graph for Kiro</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Wed, 08 Apr 2026 14:59:28 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-kirograph-a-100-local-semantic-code-knowledge-graph-for-kiro-2ja4</link>
      <guid>https://dev.to/aws-builders/building-kirograph-a-100-local-semantic-code-knowledge-graph-for-kiro-2ja4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This is part of my "Build in Public" with Kiro series. I'm an AWS Community Builder, and this is the story of building a tool by using the tool itself, which is either very meta or very efficient, depending on how you look at it.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🏃 TL;DR
&lt;/h2&gt;

&lt;p&gt;KiroGraph is a local code indexing system for &lt;a href="https://kiro.dev/" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; AI IDE. It turns your codebase into a queryable structural using a semantic graph, dramatically reducing AI tool calls and token usage (up to 90%).&lt;/p&gt;

&lt;p&gt;Instead of re-reading files with grep/glob, the AI queries a pre-built AST-based graph (plus optional embeddings), making code navigation faster, cheaper, and more scalable.&lt;/p&gt;

&lt;p&gt;It supports multiple vector engines (e.g. SQlite, PGlite, Orama Qdrant, Typesense) and is fully local, with an interactive installer and automatic syncing.&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;
        kirograph
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Semantic code knowledge graph for Kiro: fewer tool calls, instant symbol lookups, 100% local.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Flogo.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;KiroGraph&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/terminal.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Fterminal.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Semantic code knowledge graph for &lt;a href="https://kiro.dev" rel="nofollow noopener noreferrer"&gt;Kiro&lt;/a&gt;: fewer tool calls, instant symbol lookups, 100% local.&lt;/p&gt;
&lt;p&gt;Inspired by &lt;a href="https://github.com/colbymchenry/codegraph" rel="noopener noreferrer"&gt;CodeGraph&lt;/a&gt; by &lt;a href="https://github.com/colbymchenry" rel="noopener noreferrer"&gt;colbymchenry&lt;/a&gt; for Claude Code, rebuilt natively for Kiro's MCP and hooks system.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full support is for Kiro only.&lt;/strong&gt; Experimental integrations for other MCP-capable tools (Claude Code, Codex) are available but not fully tested. See &lt;a href="https://github.com/davide-desio-eleva/kirograph#other-tools-experimental" rel="noopener noreferrer"&gt;Other Tools (Experimental)&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why KiroGraph?&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;When you ask Kiro to work on a complex task, it explores your codebase using file reads, grep, and glob searches. Every one of those is a tool call, and tool calls consume context and slow things down.&lt;/p&gt;
&lt;p&gt;KiroGraph gives Kiro a semantic knowledge graph that's pre-indexed and always up to date. Instead of scanning files to understand your code, Kiro queries the graph instantly: symbol relationships, call graphs, type hierarchies, impact radius, all in a single MCP tool call.&lt;/p&gt;
&lt;p&gt;The result is fewer tool calls, less context used…&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/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  🔭 How it started
&lt;/h2&gt;

&lt;p&gt;A few weeks ago I came across &lt;a href="https://github.com/colbymchenry/codegraph" rel="noopener noreferrer"&gt;CodeGraph&lt;/a&gt; by &lt;a href="https://github.com/colbymchenry" rel="noopener noreferrer"&gt;Colby McHenry&lt;/a&gt;, a semantic code knowledge graph for Claude Code. The idea was brilliant: instead of letting the AI wander through your codebase with grep and file reads, you give it a pre-indexed 100% local graph it can query instantly. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fewer tool calls, less context burned, faster responses.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;, AWS's spec-driven AI IDE, and there was nothing equivalent for it. So I did what any reasonable developer does when they see a good idea: I ported it.&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%2Fy0ombkg46on8mfjqmki1.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%2Fy0ombkg46on8mfjqmki1.png" alt=" " width="800" height="1123"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's how KiroGraph started but the community’s genuine interest motivated me to essentially rebuild CodeGraph from the ground up and significantly expand its functionality.&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%2Fgyz1be6236grtzd6lcew.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%2Fgyz1be6236grtzd6lcew.png" alt=" " width="800" height="1207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  💸 Why this should matters to us (the token problem)
&lt;/h2&gt;

&lt;p&gt;When you ask an AI agent to work on a complex task, "fix the auth bug", "add rate limiting to the API", "refactor the payment service", the agent needs to understand your codebase before it can do anything useful. The way it typically does that is by reading files, running grep, globbing directories. Every one of those is a tool call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool calls cost tokens&lt;/strong&gt;. Lots of them. And they're slow.&lt;br&gt;
Aside from cost, &lt;strong&gt;we’re currently in a period where reasonable plans for using AI are either being rate-limited or subject to increasingly restrictive limits&lt;/strong&gt;. The community is actively exploring solutions to optimize requests and responses, reduce token usage, and improve overall efficiency.&lt;/p&gt;

&lt;p&gt;The insight behind KiroGraph (and CodeGraph before it) is simple: &lt;strong&gt;your codebase doesn't change that often&lt;/strong&gt;. Between agent runs, you might touch a handful of files. &lt;/p&gt;

&lt;p&gt;Why should the agent re-discover the structure from scratch every single time?&lt;/p&gt;

&lt;p&gt;KiroGraph pre-indexes everything, functions, methods, classes, interfaces, types, call relationships, import graphs, type hierarchies, into a 100% local SQLite database. When Kiro needs context, it doesn't read files. It queries the graph. One MCP tool call instead of twenty file reads and multiple tool calls.&lt;/p&gt;

&lt;p&gt;The impact is real: tasks that once consumed entire context windows simply don’t anymore: saving tokens, improving efficiency, and delivering greater speed.&lt;/p&gt;
&lt;h2&gt;
  
  
  ⚖️ Benchmark
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;An image is worth a thousand words&lt;/p&gt;
&lt;/blockquote&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%2Fkt1o01bxal0vidnyaoht.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%2Fkt1o01bxal0vidnyaoht.png" alt=" " width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Up to a 90%+ reduction in token usage for common read patterns in the KiroGraph codebase. I can confirm similar results across different and larger codebases as well.&lt;/p&gt;

&lt;p&gt;These numbers represent the average outcome of identical requests executed with and without KiroGraph, across different semantic engines for comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this practically means for me is that I used up all my tokens with Kiro on the $20 plan in just 10 days. Now, I’ve gone a full month without even reaching the full allowance.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🏗️ KiroGraph architecture
&lt;/h2&gt;

&lt;p&gt;The tool has two indexing layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural indexing&lt;/strong&gt; is always on. &lt;a href="https://tree-sitter.github.io/tree-sitter/" rel="noopener noreferrer"&gt;tree-sitter&lt;/a&gt; parses every source file into an AST and extracts nodes (functions, classes, routes, components, 24 kinds total) and edges (calls, imports, extends, implements, references, and more). Everything lands in &lt;code&gt;kirograph.db&lt;/code&gt;. This powers all the graph traversal tools: find callers, trace impact, detect circular deps, find dead code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic indexing&lt;/strong&gt; is opt-in. When you enable it, KiroGraph generates 768-dimensional vector embeddings for every embeddable symbol using &lt;code&gt;nomic-ai/nomic-embed-text-v1.5&lt;/code&gt; (~130MB, downloaded once to &lt;code&gt;~/.kirograph/models/&lt;/code&gt;). This powers natural-language search, ask for "auth middleware" and get the relevant functions even if they're named &lt;code&gt;validateJwt&lt;/code&gt; or &lt;code&gt;checkPermissions&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The index stays fresh automatically via &lt;strong&gt;Kiro hooks&lt;/strong&gt;. &lt;br&gt;
File saved, mark dirty. Agent stops, sync if dirty. &lt;br&gt;
Batched, efficient, zero overhead during active editing.&lt;/p&gt;

&lt;p&gt;The agent knows what to do through a Kiro steering file, which the final KiroGraph adopter can easily adapt to suit their specific needs.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧠 Is KiroGraph a RAG/GraphRAG?
&lt;/h2&gt;

&lt;p&gt;It’s useful to compare KiroGraph to both a classic RAG system and a GraphRAG approach, because it sits somewhere in between, but also slightly outside both categories.&lt;/p&gt;

&lt;p&gt;1) A local RAG works on unstructured text, splitting documents into chunks and retrieving the most relevant pieces via embeddings.&lt;br&gt;
&lt;strong&gt;KiroGraph instead indexes code structure&lt;/strong&gt;, where functions, classes, and relationships come directly from the AST.&lt;br&gt;
This removes chunking entirely and &lt;strong&gt;replaces text retrieval with symbol-level navigation over a code graph.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;2) GraphRAG builds graphs by extracting entities and relationships from documents, then uses them to improve retrieval quality.&lt;br&gt;
&lt;strong&gt;KiroGraph doesn’t infer the graph from text, it derives it deterministically from the codebase structure itself.&lt;/strong&gt;&lt;br&gt;
As a result, its graph is not an approximation of knowledge, but a direct representation of the system architecture.&lt;/p&gt;

&lt;p&gt;The key difference: RAG retrieves text, GraphRAG organizes text, KiroGraph represents code structure.&lt;br&gt;
And embeddings in KiroGraph are optional, not foundational.&lt;br&gt;
The core idea is not better retrieval, but queryable program structure with semantic enrichment on top.&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%2Fdcmdrcrcijq37arv19v0.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%2Fdcmdrcrcijq37arv19v0.png" alt=" " width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🔍 A lot of semantic engine possibility
&lt;/h2&gt;

&lt;p&gt;Here's where it got interesting. &lt;br&gt;
Once you have embeddings, you need somewhere to put them, and &lt;em&gt;how&lt;/em&gt; you store and search them has real consequences at scale.&lt;/p&gt;

&lt;p&gt;The original approach is &lt;strong&gt;cosine similarity over all vectors in SQLite&lt;/strong&gt;. That's fine for small to medium projects, but for a large codebase with thousands of indexed symbols, you want approximate nearest-neighbour (ANN) search with a proper index structure.&lt;/p&gt;

&lt;p&gt;So I built support for seven engines, each solving a slightly different problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;cosine (default): the original linear scan over the &lt;code&gt;vectors&lt;/code&gt; table in &lt;code&gt;kirograph.db&lt;/code&gt;. No extra deps. Works great up to a few thousand symbols. If you just want to try semantic search without any setup, this is it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/asg017/sqlite-vec" rel="noopener noreferrer"&gt;Alex Garcia's sqlite-vec&lt;/a&gt; brings ANN search to SQLite via a native extension. Sub-linear query time, stays in the SQLite ecosystem. Best for large codebases that don't want to run a separate process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/oramasearch/orama" rel="noopener noreferrer"&gt;Orama&lt;/a&gt; does something clever: hybrid search. One query combines full-text relevance and vector similarity, which produces better results than running them separately and merging. Pure JS, no native compilation. If you want the best result quality and no native dependencies, Orama is the choice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/electric-sql/pglite" rel="noopener noreferrer"&gt;PGlite&lt;/a&gt; is PostgreSQL compiled to WASM with the &lt;code&gt;pgvector&lt;/code&gt; extension. You get &lt;strong&gt;exact&lt;/strong&gt; (not approximate) nearest-neighbour search, &lt;code&gt;ON CONFLICT&lt;/code&gt; upserts, HNSW indexing, all the PostgreSQL semantics, in-process, no server. Pure WASM means no native binaries and no compilation. And because it's exact, results are deterministic and reproducible. &lt;strong&gt;I particularly like this one.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/lancedb/lancedb" rel="noopener noreferrer"&gt;LanceDB&lt;/a&gt; stores embeddings in Apache Lance columnar format. Columnar storage is efficient for batch reads and writes, which matters a lot during indexing. Pure JS, no native deps, sub-linear ANN search.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/qdrant/qdrant" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt; is a dedicated vector database, HNSW index, cosine distance, the full feature set. KiroGraph spawns the Qdrant binary as a managed child process via &lt;code&gt;qdrant-local&lt;/code&gt;. The server runs as a persistent background daemon, state tracked in &lt;code&gt;.kirograph/qdrant-server.json&lt;/code&gt;. &lt;strong&gt;This is the heavy option.&lt;/strong&gt; You get Qdrant's full query capabilities and a proper production-grade vector store. The trade-off is you're now running a binary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/typesense/typesense" rel="noopener noreferrer"&gt;Typesense&lt;/a&gt; is a search engine that added HNSW vector search. KiroGraph auto-downloads the binary (~37MB, cached at &lt;code&gt;~/.kirograph/bin/&lt;/code&gt;) and manages it as a background daemon. State tracked in &lt;code&gt;.kirograph/typesense-server.json&lt;/code&gt;. Similar to Qdrant in concept, persistent binary daemon, but with Typesense's search-engine heritage. Very excellent for hybrid queries.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  🛠️ Adding installer because DevEx always matters
&lt;/h2&gt;

&lt;p&gt;A lot of CLI tools treat configuration as an afterthought. You edit a config file, restart, wonder why nothing worked, read the docs, try again. &lt;strong&gt;It's friction.&lt;/strong&gt; You don't want friction when working within an AI powered IDE like Kiro.&lt;/p&gt;

&lt;p&gt;I wanted KiroGraph's setup to be genuinely good and simple. &lt;/p&gt;

&lt;p&gt;So the installer is interactive, not just &lt;code&gt;yes/no&lt;/code&gt; prompts but arrow-key menus with descriptions for each option. Run it once and you walk away with a fully working setup, no post-install surprises.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kirograph &lt;span class="nb"&gt;install&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F886icfbao6h6g4mywnss.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%2F886icfbao6h6g4mywnss.png" alt=" " width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's every decision the installer walks you through, and why each one matters.&lt;/p&gt;
&lt;h3&gt;
  
  
  Enable semantic embeddings
&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%2Ff3nvn617g7xkyk0w7aag.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%2Ff3nvn617g7xkyk0w7aag.png" alt=" " width="800" height="33"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first question is whether to turn on semantic search at all. Structural indexing is always on and costs nothing extra. Semantic indexing is opt-in because it requires a local embedding model (~130MB, downloaded once) and adds time to every index run.&lt;/p&gt;

&lt;p&gt;If your team mostly does exact symbol lookups, "go to definition" style queries, structural-only is fast and lightweight. If you want to ask things like "where is rate limiting handled?" or "which functions deal with user authentication?", you need embeddings.&lt;/p&gt;

&lt;p&gt;The installer is upfront about this: it tells you what you're signing up for before you say yes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Embedding model
&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%2Ffcldo1okczybjcowc6ls.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%2Ffcldo1okczybjcowc6ls.png" alt=" " width="800" height="82"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you enable embeddings, the installer asks which HuggingFace model to use. The default is &lt;code&gt;nomic-ai/nomic-embed-text-v1.5&lt;/code&gt;, a solid general-purpose model that produces 768-dimensional embeddings and runs well locally via Ollama.&lt;/p&gt;

&lt;p&gt;You can enter any HuggingFace model identifier in &lt;code&gt;org/model-name&lt;/code&gt; format. If you enter something non-standard, the installer rejects it and explains the expected format. If you enter a non-default model, it reminds you to run &lt;code&gt;ollama pull &amp;lt;model&amp;gt;&lt;/code&gt; before indexing.&lt;/p&gt;

&lt;p&gt;The reason this is configurable: &lt;strong&gt;embedding quality varies by domain.&lt;/strong&gt; Code-specific models like &lt;code&gt;jinaai/jina-embeddings-v2-base-code&lt;/code&gt; may outperform general models on certain queries. &lt;br&gt;
&lt;strong&gt;Giving you control here means you're not locked in.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Semantic engine
&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%2Fo544yvt7pcgmf46vnsau.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%2Fo544yvt7pcgmf46vnsau.png" alt=" " width="532" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the arrow-key menu. Each option shows a one-line description so you can make an informed choice without reading docs:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;? Choose the semantic search engine:
  ❯ cosine      In-process cosine similarity. No extra deps. Best for small/medium projects.
    sqlite-vec  ANN index. Sub-linear search. Best for large codebases. Needs: better-sqlite3, sqlite-vec (native).
    orama       Hybrid search (full-text + vector). Pure JS. Needs: @orama/orama, ...
    pglite      Hybrid search via PostgreSQL + pgvector. Exact results. Pure WASM. Needs: @electric-sql/pglite.
    lancedb     ANN search via LanceDB (Apache Lance columnar format). Pure JS. Needs: @lancedb/lancedb.
    qdrant      ANN search via Qdrant embedded binary (HNSW index, Cosine). Needs: qdrant-local.
    typesense   ANN search via Typesense (auto-downloaded binary, HNSW, Cosine). Needs: typesense.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;After you pick one, the installer immediately runs &lt;code&gt;npm install&lt;/code&gt; for the required dependencies. &lt;strong&gt;No separate step, no forgotten follow-up. If the install fails, it tells you exactly what to run manually.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For engines that spawn a binary (qdrant, typesense), it also asks whether you want a dashboard (a web UI to navigate the vectors).&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%2F6ll04lwuaiyfx4vlrhrf.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%2F6ll04lwuaiyfx4vlrhrf.png" alt=" " width="800" height="59"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Extract docstrings
&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%2Fagfyluq16v8sp63e0ua7.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%2Fagfyluq16v8sp63e0ua7.png" alt=" " width="800" height="61"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This controls whether KiroGraph reads JSDoc, Python docstrings, and inline comments from your source files and stores them as symbol metadata. Enabled by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docstrings significantly improve semantic search quality.&lt;/strong&gt; When a function has a good docstring, the embedding captures its &lt;em&gt;intent&lt;/em&gt;, not just its name. A function called &lt;code&gt;proc&lt;/code&gt; with a docstring "processes incoming webhook payloads and dispatches to handlers" will surface correctly when someone searches for "webhook processing". Without the docstring, the name alone gives the model almost nothing to work with.&lt;/p&gt;

&lt;p&gt;The trade-off is slightly longer indexing time. For most projects it's negligible. The installer lets you disable it if you're indexing a very large codebase and want the fastest possible first run.&lt;/p&gt;
&lt;h3&gt;
  
  
  Track call sites
&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%2Fyuyaoh8db8q3a7f7rqua.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%2Fyuyaoh8db8q3a7f7rqua.png" alt=" " width="800" height="59"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This controls whether KiroGraph records the exact line and column of every function call when building call graph edges. Enabled by default.&lt;/p&gt;

&lt;p&gt;With call sites tracked, you get precise "go to call site" information in addition to "which functions call this symbol". The &lt;code&gt;kirograph_callers&lt;/code&gt; and &lt;code&gt;kirograph_callees&lt;/code&gt; MCP tools return not just the caller's name and file, but the exact location of the call. This is what makes the call graph actually useful for debugging and impact analysis.&lt;/p&gt;

&lt;p&gt;The trade-off is index size: call site data adds rows to the edges table. On codebases with millions of call expressions this can get large. If you only care about the structural shape of the call graph and not the precise locations, you can disable this to keep the database lean.&lt;/p&gt;
&lt;h3&gt;
  
  
  Indexing
&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%2Fd7rgcqf7wiufd97y685l.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%2Fd7rgcqf7wiufd97y685l.png" alt=" " width="758" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The final prompt asks whether to run the first full index immediately. If you say yes, the installer runs &lt;code&gt;kirograph index&lt;/code&gt;, shows you a live progress output (files scanned, symbols extracted, embeddings generated), and reports the final counts.&lt;/p&gt;

&lt;p&gt;This is the "zero to working" moment: by the time the installer exits, your graph is built and Kiro can start using it. No deferred setup, no "remember to run this before you start".&lt;/p&gt;

&lt;p&gt;Each phase is surfaced in the output so it's always clear what's happening: scanning files, parsing, resolving references, detecting languages and frameworks, generating embeddings. If something is slow, you know exactly where.&lt;/p&gt;

&lt;p&gt;The philosophy across all of this is the same: the installer should leave you in a working state and make every choice transparent. If something requires a separate binary or native compilation, you know before you commit to it. If a step can be done for you automatically, it is.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔁 The feedback loop that accelerated everything
&lt;/h2&gt;

&lt;p&gt;Here's the part I didn't fully anticipate: because I've used KiroGraph to indexes itself, so I could use it in Kiro &lt;em&gt;while building it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Every time I added a new engine, the Kiro agent could immediately use &lt;code&gt;kirograph_context&lt;/code&gt; and &lt;code&gt;kirograph_callers&lt;/code&gt; to understand the existing codebase structure. It knew which interfaces to implement, where the integration points were, what the existing patterns looked like, without me having to explain any of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is augmenting spec-driven development with actual graph-powered context.&lt;/strong&gt; The agent writes specs and code that fits the existing architecture because it can see the architecture. Not by reading files, by querying the graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The speed difference and saving in tokens is hard to overstate&lt;/strong&gt;. Tasks that would have required multiple rounds of "read this file, now read that file, now understand how they connect" collapsed into a single &lt;code&gt;kirograph_context&lt;/code&gt; call followed by implementation.&lt;/p&gt;
&lt;h2&gt;
  
  
  📊 Adding some dashboard
&lt;/h2&gt;

&lt;p&gt;Once you have Qdrant and Typesense running as daemons, my idea was also to have visibility into what's actually in the vector store as both have community open sourced web UIs.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Qdrant&lt;/strong&gt;: the &lt;a href="https://github.com/qdrant/qdrant-web-ui" rel="noopener noreferrer"&gt;Qdrant Web UI&lt;/a&gt; is served &lt;em&gt;by Qdrant itself&lt;/em&gt;. KiroGraph downloads the &lt;code&gt;dist-qdrant.zip&lt;/code&gt; release asset, extracts it with &lt;code&gt;unzip&lt;/code&gt;, caches it, and sets the env var before spawning the binary. The dashboard is then available at &lt;code&gt;http://127.0.0.1:&amp;lt;port&amp;gt;/dashboard&lt;/code&gt; natively.&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%2Fop0pdbg9bu70150nluyg.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%2Fop0pdbg9bu70150nluyg.png" alt=" " width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Typesense&lt;/strong&gt;: &lt;a href="https://github.com/bfritscher/typesense-dashboard" rel="noopener noreferrer"&gt;bfritscher/typesense-dashboard&lt;/a&gt; is a static React app. KiroGraph downloads it from GitHub, caches it at &lt;code&gt;.kirograph/typesense/dashboard/&lt;/code&gt;, and serves it locally via a Node HTTP server.&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%2F2wylmdqj45uxu352euvh.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%2F2wylmdqj45uxu352euvh.png" alt=" " width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both are unified under &lt;code&gt;kirograph dashboard start&lt;/code&gt; / &lt;code&gt;kirograph dashboard stop&lt;/code&gt;, the command reads &lt;code&gt;semanticEngine&lt;/code&gt; from config and dispatches to the right implementation.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧩 Supported languages and frameworks
&lt;/h2&gt;

&lt;p&gt;As per &lt;a href="https://github.com/colbymchenry/codegraph" rel="noopener noreferrer"&gt;CodeGraph&lt;/a&gt; by &lt;a href="https://github.com/colbymchenry" rel="noopener noreferrer"&gt;Colby McHenry&lt;/a&gt;, KiroGraph supports different languages and framework (and it easy to add new ones).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;React, Next.js, React Native, Svelte, SvelteKit, Express, Fastify, Koa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;React, Next.js, React Native, Svelte, SvelteKit, Express, Fastify, Koa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TSX / JSX&lt;/td&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Django, Flask, FastAPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;Spring, Spring Boot, Spring MVC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C++&lt;/td&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;ASP.NET Core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP&lt;/td&gt;
&lt;td&gt;Laravel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;Rails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swift&lt;/td&gt;
&lt;td&gt;SwiftUI, UIKit, Vapor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kotlin&lt;/td&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dart&lt;/td&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  ➡️ What's next
&lt;/h2&gt;

&lt;p&gt;A few things on the roadmap:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More engines.&lt;/strong&gt; The engine abstraction is clean, adding a new one means implementing four methods (&lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;upsert&lt;/code&gt;, &lt;code&gt;search&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;). &lt;a href="https://github.com/weaviate/weaviate" rel="noopener noreferrer"&gt;Weaviate&lt;/a&gt;, &lt;a href="https://github.com/chroma-core/chroma" rel="noopener noreferrer"&gt;Chroma&lt;/a&gt;, and &lt;a href="https://github.com/milvus-io/milvus" rel="noopener noreferrer"&gt;Milvus&lt;/a&gt; are very interesting candidates. I should evaluate if they fit the ecosystem and what they offer as peculiarity. Maybe a "plugin system" would be a good implementation to let folks implement their preferred semantic engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More languages and frameworks.&lt;/strong&gt; Also here a "plug'n'play" system to add new definition for languages and frameworks could be a good choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embed in a Kiro Power.&lt;/strong&gt; KiroGraph works with Kiro's hooks and steering, and is basically a CLI tool: a good choice could be to embed it into a configurable Kiro power to reduce the friction for folks who wants just install and vibe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smarter sync.&lt;/strong&gt; Currently, sync re-embeds every changed symbol. I’m considering introducing a content hash per embedding so we can skip unchanged symbols, even when the file has been modified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-project search.&lt;/strong&gt; The graph is per-project right now. For monorepos or workspaces with shared libraries, cross-project symbol resolution would be genuinely useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Richer graph traversal.&lt;/strong&gt; &lt;code&gt;kirograph_path&lt;/code&gt; finds the shortest path between two symbols. I would love to add something like "explain this path", not just the nodes, but the semantic reason for each edge.&lt;/p&gt;
&lt;h2&gt;
  
  
  🚀 Just try it
&lt;/h2&gt;

&lt;p&gt;Go to &lt;a href="https://github.com/davide-desio-eleva/kirograph/stargazers" rel="noopener noreferrer"&gt;KiroGraph repository&lt;/a&gt;, fork it, try it. PR's are welcome.&lt;br&gt;
It’s not yet published on npm, so it should be considered an alpha version. Expect significant changes in the future so do not consider it stable.&lt;/p&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/davide-desio-eleva" rel="noopener noreferrer"&gt;
        davide-desio-eleva
      &lt;/a&gt; / &lt;a href="https://github.com/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;
        kirograph
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Semantic code knowledge graph for Kiro: fewer tool calls, instant symbol lookups, 100% local.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Flogo.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;KiroGraph&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/davide-desio-eleva/kirograph/main/assets/terminal.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdavide-desio-eleva%2Fkirograph%2Fmain%2Fassets%2Fterminal.png" alt="KiroGraph terminal"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Semantic code knowledge graph for &lt;a href="https://kiro.dev" rel="nofollow noopener noreferrer"&gt;Kiro&lt;/a&gt;: fewer tool calls, instant symbol lookups, 100% local.&lt;/p&gt;

&lt;p&gt;Inspired by &lt;a href="https://github.com/colbymchenry/codegraph" rel="noopener noreferrer"&gt;CodeGraph&lt;/a&gt; by &lt;a href="https://github.com/colbymchenry" rel="noopener noreferrer"&gt;colbymchenry&lt;/a&gt; for Claude Code, rebuilt natively for Kiro's MCP and hooks system.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full support is for Kiro only.&lt;/strong&gt; Experimental integrations for other MCP-capable tools (Claude Code, Codex) are available but not fully tested. See &lt;a href="https://github.com/davide-desio-eleva/kirograph#other-tools-experimental" rel="noopener noreferrer"&gt;Other Tools (Experimental)&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why KiroGraph?&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;When you ask Kiro to work on a complex task, it explores your codebase using file reads, grep, and glob searches. Every one of those is a tool call, and tool calls consume context and slow things down.&lt;/p&gt;

&lt;p&gt;KiroGraph gives Kiro a semantic knowledge graph that's pre-indexed and always up to date. Instead of scanning files to understand your code, Kiro queries the graph instantly: symbol relationships, call graphs, type hierarchies, impact radius, all in a single MCP tool call.&lt;/p&gt;

&lt;p&gt;The result is fewer tool calls, less context used…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/davide-desio-eleva/kirograph" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;The installer will walk you through everything. If you're not sure which engine to pick, start with &lt;code&gt;cosine&lt;/code&gt;, it works out of the box with no dependencies and you can always switch later. If you're using it on large codebases, pick &lt;code&gt;pglite&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The repo is public. If you build on it, find a bug, or have thoughts on the engine choices, I'd love to hear from you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A very special thanks to the &lt;a href="https://github.com/davide-desio-eleva/kirograph/stargazers" rel="noopener noreferrer"&gt;Stargazers&lt;/a&gt;, your support means a lot and truly makes a difference.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🙋 Who am I
&lt;/h2&gt;

&lt;p&gt;I'm &lt;a href="https://www.linkedin.com/in/desiodavide" rel="noopener noreferrer"&gt;D. De Sio&lt;/a&gt; and I work as a Head of Software Engineering in &lt;a href="https://eleva.it/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;br&gt;
As of Feb 2026, I’m an &lt;a href="https://www.credly.com/badges/9929fdf2-7a3d-4013-9de6-57c80e4920b9/public_url" rel="noopener noreferrer"&gt;AWS Certified Solution Architect Professional&lt;/a&gt; and &lt;a href="https://www.credly.com/badges/8c5a1487-191b-429e-8c2d-7cee43bf316b/public_url" rel="noopener noreferrer"&gt;AWS Certified DevOps Engineer Professional&lt;/a&gt;, but also a &lt;a href="https://www.linkedin.com/company/aws-user-group-pavia/" rel="noopener noreferrer"&gt;User Group Leader (in Pavia)&lt;/a&gt;, an &lt;strong&gt;AWS Community Builder&lt;/strong&gt; and, last but not least, a #serverless enthusiast.&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%2F1vvb6ms9ieiv69fdfy6u.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%2F1vvb6ms9ieiv69fdfy6u.png" alt=" " width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It’s always amusing how AI is convinced developers are basically 80% coffee, 20% code, and somehow still functional.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>kiro</category>
      <category>aws</category>
      <category>ai</category>
    </item>
    <item>
      <title>AWS Lambda Durable Functions vs Step Functions: a real-world comparison</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Mon, 23 Feb 2026 18:03:06 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-lambda-durable-functions-vs-step-functions-a-real-world-comparison-5gij</link>
      <guid>https://dev.to/aws-builders/aws-lambda-durable-functions-vs-step-functions-a-real-world-comparison-5gij</guid>
      <description>&lt;p&gt;Hey devs, I recently built the same order dispatch workflow twice, once with &lt;strong&gt;AWS Step Functions&lt;/strong&gt; and once with &lt;strong&gt;AWS Lambda durable functions&lt;/strong&gt;. The difference in developer experience was significant. Let me walk you through what I learned and why I decided to do this.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AWS Lambda durable functions&lt;/code&gt; &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/lambda-durable-functions-14-additional-regions/" rel="noopener noreferrer"&gt;are relatively new to the AWS ecosystem&lt;/a&gt;, so deciding whether to use them is not always straightforward.&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%2Fc7igoojzw0jcbw6ui9pj.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%2Fc7igoojzw0jcbw6ui9pj.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 The Problem: A Real-World Order Workflow
&lt;/h2&gt;

&lt;p&gt;I needed to build a simple workflow for handling an order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Store the order in &lt;code&gt;DynamoDB&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check inventory &lt;/li&gt;
&lt;li&gt;Wait for human approval&lt;/li&gt;
&lt;li&gt;Automatically find alternatives if rejected or complementary items if confirmed&lt;/li&gt;
&lt;li&gt;Wait 2 days &lt;/li&gt;
&lt;li&gt;Generate an email with &lt;code&gt;Bedrock&lt;/code&gt; (with alternatives or complementary items, based on confirmation or rejection) &lt;/li&gt;
&lt;li&gt;Send it via &lt;code&gt;SES&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a real-world scenario (and also something I needed in production): human-in-the-loop, approvals, timers, and external service integrations. In reality, it’s a bit more complex than that, but for the sake of discussion, we can focus on this key question: should I choose &lt;code&gt;Step Functions&lt;/code&gt; or &lt;code&gt;Lambda durable functions&lt;/code&gt;?&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ Which one should I choose?
&lt;/h2&gt;

&lt;p&gt;The choice framework proposed by AWS is a good start:&lt;/p&gt;

&lt;p&gt;Go with &lt;code&gt;Lambda durable functions&lt;/code&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You prefer using your familiar programming language&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Local testing without cloud dependencies is important to you&lt;/li&gt;
&lt;li&gt;Your compute service of choice is AWS Lambda and your business logic primarily lives in those functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stick with &lt;code&gt;Step Functions&lt;/code&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Visual workflows are important for your stakeholders&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;You're orchestrating many AWS services together&lt;/li&gt;
&lt;li&gt;You want to reduce ops burden (patching, scaling, ..)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wasn’t sure what to choose, as my goal is always to maximize Developer Experience (DevEx) and maintainability. I don’t necessarily need a fully visual workflow, but one of the business requirements is ensuring seamless integration with other AWS services and upcoming workflows. I’m also a big fan of &lt;code&gt;Step Functions&lt;/code&gt; when it comes to &lt;code&gt;CDK&lt;/code&gt;-based projects. On the other hand, I’m really drawn to the simplicity of &lt;code&gt;Lambda durable functions&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🆚 Code comparison
&lt;/h2&gt;

&lt;p&gt;So, this was the perfect scenario to explore both solutions. The workflow is clear and simple enough that it won’t take much time to build them in parallel, allowing for a real-world comparison.&lt;/p&gt;

&lt;p&gt;Let’s look at the actual code: this is where the differences become clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  A TypeScript function using Lambda durable functions
&lt;/h3&gt;

&lt;p&gt;Why does this perfectly suit my scenario?&lt;/p&gt;

&lt;p&gt;It’s a single &lt;code&gt;TypeScript&lt;/code&gt; function, there’s no &lt;code&gt;CDK&lt;/code&gt; involved to implement the workflow. &lt;code&gt;CDK&lt;/code&gt; is used only to create the architecture, thus a single &lt;code&gt;Lambda&lt;/code&gt;, cleanly separating my workflow logic from the infrastructure code. &lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;async/await&lt;/code&gt; feels very natural for a developer working in this environment, and I can encapsulate the entire workflow within a single function, giving me one clear place to understand what’s going on. On top of that, I get full IDE support with autocomplete and type checking (and AI, have you tried &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; yet?)&lt;/p&gt;

&lt;p&gt;Let’s take a look at the code, starting with the imports.&lt;br&gt;
I’ll remove all the pure business logic, as we should focus on the workflow itself (and I can’t disclose my client’s code!).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;withDurableExecution&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we’ve imported it, we should wrap the handler in a durable execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;OrderEvent&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now move on to our workflow steps. &lt;br&gt;
Let’s start by saving the order (in my real-world scenario I've saved it to &lt;code&gt;DynamoDB&lt;/code&gt; using the &lt;code&gt;AWS SDK&lt;/code&gt;) with a first async step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Step 1: Save order&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&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="s1"&gt;save-order&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OrderData&lt;/span&gt;&lt;span class="o"&gt;&amp;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;orderId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`ORD-&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="nf"&gt;now&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;buyerEmail&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;buyerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;items&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;items&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 second step waits for the first one to complete, then checks the inventory and provides the relevant information needed to confirm or reject the order. An interesting aspect is that we can use a logger to record the response of each step and async/await pattern really helps us understand what happens in sequence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Step 2: Check inventory&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;availability&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="s1"&gt;check-inventory&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;return&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;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inStock&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="p"&gt;}));&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Inventory checked&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="nx"&gt;availability&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third step handles human approval using &lt;code&gt;waitForCallback&lt;/code&gt; function. At this stage, in my real-world scenario, an email is sent to the approver (I used &lt;code&gt;SES&lt;/code&gt;, but you could just as easily use an &lt;code&gt;SNS&lt;/code&gt; topic or any other notification system). However, this is just part of the business logic, which I won’t go into here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Step 3: Wait for human approval (up to 48h, no compute cost)&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="s1"&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="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Waiting for approval&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="nx"&gt;callbackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&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;48&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;Once the decision is received, we handle any rejected items and look for suitable alternatives (I retrieved them from &lt;code&gt;DynamoDB&lt;/code&gt;, but you can use any database you prefer). While, if the order is accepted, we instead search for complementary items to recommend to the user.&lt;br&gt;
For simplicity, error handling is omitted here, but in a production scenario this should be wrapped in a try/catch block and handled properly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Step 4: Handle approval or rejection&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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;decision&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;suggestedItems&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;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;discard&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="nx"&gt;suggestedItems&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="s1"&gt;find-similar&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;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIM-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Similar Item&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;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;suggestedItems&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="s1"&gt;find-complementary&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;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COM-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Complementary Item&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;The business then decided to pause the workflow for at least two days, as they don’t want to bother the user with marketing emails immediately after an order is accepted or rejected. This is a good opportunity to see a &lt;code&gt;wait&lt;/code&gt; in action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Step 5: Wait 2 days before marketing follow-up&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="s1"&gt;wait-two-days&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;2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the wait is over, I generate the email via &lt;code&gt;Amazon Bedrock&lt;/code&gt;, using information from previous steps or the database. In practice, I personalize the message based on the approval decision, either suggesting similar products for rejected orders or recommending complementary items for confirmed ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Step 6: Generate marketing email via Bedrock&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&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="s1"&gt;generate-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;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This is where the email has been generate&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally send the email generated and close the function returning the order, status and suggested items.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Step 7: Send email&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="s1"&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="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sending email&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="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&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;buyerEmail&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;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;suggested&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;suggestedItems&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Approach with Step Function
&lt;/h3&gt;

&lt;p&gt;Here's the &lt;strong&gt;same&lt;/strong&gt; workflow implemented using &lt;code&gt;Step Functions&lt;/code&gt; and &lt;code&gt;CDK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, we need to set up all the required &lt;code&gt;Lambda&lt;/code&gt; functions. Doesn’t that feel a bit odd? We want to define the workflow, yet we’re forced to create every individual function before we’ve even written the workflow itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderDispatchStepFunctionStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&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;props&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;saveOrderFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SaveOrderFunction&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;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_20_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;save-order.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lambda&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkInventoryFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CheckInventoryFunction&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;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_20_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;check-inventory.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lambda&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;findSimilarItemsFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FindSimilarItemsFunction&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;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_20_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;find-similar-items.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lambda&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;findComplementaryItemsFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FindComplementaryItemsFunction&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;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_20_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;find-complementary-items.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lambda&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateEmailFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GenerateEmailFunction&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;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_20_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generate-email.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lambda&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendEmailFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SendEmailFunction&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;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_20_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;send-email.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lambda&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ve omitted the permission setup here to keep things simple. Just remember that each Lambda function still needs the appropriate IAM permissions to access the required AWS services. And this is again a lot of boilerplate code.&lt;/p&gt;

&lt;p&gt;Finally, we can define our tasks. Again, this feels mostly like boilerplate: just a way to wrap each individual Lambda function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saveOrderTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LambdaInvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SaveOrder&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;lambdaFunction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;saveOrderFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.Payload&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkInventoryTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LambdaInvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CheckInventory&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;lambdaFunction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checkInventoryFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.Payload&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendApprovalNotification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SnsPublish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SendApprovalNotification&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;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;approvalTopic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TaskInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromJsonPathAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;waitForApproval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WaitForHumanApproval&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;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WaitTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&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="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;findSimilarTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LambdaInvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FindSimilarItems&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;lambdaFunction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;findSimilarItemsFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.Payload&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;findComplementaryTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LambdaInvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FindComplementaryItems&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;lambdaFunction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;findComplementaryItemsFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.Payload&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;waitTwoDays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WaitTwoDays&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;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WaitTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&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="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateEmailTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LambdaInvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GenerateEmail&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;lambdaFunction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;generateEmailFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.Payload&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendEmailTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LambdaInvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SendEmail&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;lambdaFunction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sendEmailFunction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.Payload&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we introduce a bit of workflow logic, mainly to define how the approval step should be handled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;approvalChoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ApprovalDecision&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;rejectedFlow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;findSimilarTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Pass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OrderRejected&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;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sfn&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="nf"&gt;fromObject&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;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;resultPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.orderStatus&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;confirmedFlow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;findComplementaryTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Pass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OrderAccepted&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;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sfn&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="nf"&gt;fromObject&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;accepted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;resultPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.orderStatus&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;span class="nx"&gt;approvalChoice&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.decision&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;confirmedFlow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$.decision&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;discard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;rejectedFlow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;otherwise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rejectedFlow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now, we bring it all together into the final, straightforward workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;saveOrderTask&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkInventoryTask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendApprovalNotification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;waitForApproval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;approvalChoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;waitTwoDays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;generateEmailTask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendEmailTask&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;stateMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StateMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OrderDispatchStateMachine&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="nx"&gt;definition&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="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first thing I immediately notice is &lt;strong&gt;the amount of boilerplate required just to prepare each Lambda before even starting to think about the workflow itself.&lt;/strong&gt; And also, this is just the workflow part, you’ll also have the business logic implemented inside the &lt;code&gt;Lambda&lt;/code&gt; functions.&lt;/p&gt;

&lt;p&gt;I now have a lot of separate Lambda functions to maintain, and this has always been my main concern with &lt;code&gt;Step Functions&lt;/code&gt;, &lt;strong&gt;the workflow logic still ends up mixed with pure infrastructure code.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I have an almost Shakespearean dilemma that keeps me up at night: is the workflow a matter of architecture or business?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The business logic is spread across multiple files, and while that’s perfectly fine (separation of concerns is still a best practice and you should implement it also when using &lt;code&gt;Lambda durable functions&lt;/code&gt;), it makes it much harder to understand the workflow as &lt;strong&gt;you lose the ability to see the entire flow at a glance, and understanding it properly often requires a certain level of expertise, at least with &lt;code&gt;ASL&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
You can see it clearly in the &lt;code&gt;CDK&lt;/code&gt; code, but here you have it mixed up with pure architecture code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And do you know what happens when &lt;code&gt;Step Functions&lt;/code&gt; doesn’t support something I need? I end up writing that logic directly inside the Lambdas.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;This happens when integrating new services that aren’t supported by &lt;code&gt;Step Functions&lt;/code&gt;, implementing complex data transformation logic, handling advanced catch/retry scenarios beyond what the service offers, or simply when something is difficult to express in &lt;code&gt;ASL&lt;/code&gt; but straightforward to implement in code inside a &lt;code&gt;Lambda&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Basically I create another step with a &lt;code&gt;Lambda&lt;/code&gt; to do this work.&lt;/p&gt;

&lt;p&gt;In doing so, I lose all the benefits I chose &lt;code&gt;Step Functions&lt;/code&gt; for in the first place: separation of concerns, &lt;strong&gt;clear workflow visibility&lt;/strong&gt;, &lt;strong&gt;and predictable orchestration&lt;/strong&gt;, basically everything that made it the right choice to begin with.&lt;/p&gt;

&lt;p&gt;Want you see which is the &lt;code&gt;CDK&lt;/code&gt; needed for the &lt;code&gt;Durable Function&lt;/code&gt;?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cdk&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-cdk-lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;lambda&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-cdk-lib/aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;iam&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-cdk-lib/aws-iam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Construct&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;constructs&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;class&lt;/span&gt; &lt;span class="nc"&gt;DurableFunctionStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&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;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Create the durable function&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;durableFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DurableFunction&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;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_22_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;durableConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;executionTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hours&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="na"&gt;retentionPeriod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&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="c1"&gt;// Create version and alias&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;durableFunction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentVersion&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;alias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ProdAlias&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;aliasName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;version&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is pure architecture: &lt;strong&gt;no workflow logic mixed in&lt;/strong&gt;. It can live alongside other core architecture components, like DynamoDB, S3, IAM permissions, and so on. Here is a full working architecture sample.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cdk&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-cdk-lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&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-cdk-lib/aws-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;lambda&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-cdk-lib/aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;logs&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-cdk-lib/aws-logs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;iam&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-cdk-lib/aws-iam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Construct&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;constructs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;path&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;path&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;class&lt;/span&gt; &lt;span class="nc"&gt;OrderDispatchDurableStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&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;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// DynamoDB Tables&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ordersTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OrdersTable&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;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orderId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;billingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BillingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DESTROY&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;inventoryTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;InventoryTable&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;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;itemId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;billingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BillingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DESTROY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;inventoryTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addGlobalSecondaryIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;indexName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CategoryIndex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Log Group for Durable Function&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orchestratorLogGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OrchestratorLogGroup&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;logGroupName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/aws/lambda/order-dispatch-orchestrator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;retention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RetentionDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ONE_WEEK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DESTROY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Main Orchestrator Durable Function&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orchestratorFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OrchestratorFunction&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;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_24_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orchestrator.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lambda&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;logGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orchestratorLogGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;durableConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;executionTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;retentionPeriod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;ORDERS_TABLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ordersTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;INVENTORY_TABLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inventoryTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;SENDER_EMAIL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENDER_EMAIL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noreply@example.com&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;span class="nx"&gt;ordersTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantReadWriteData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orchestratorFunction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;inventoryTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantReadData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orchestratorFunction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;orchestratorFunction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToRolePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bedrock:InvokeModel&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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="nx"&gt;orchestratorFunction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToRolePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ses:SendEmail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ses:SendRawEmail&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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="c1"&gt;// Create version and alias&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;orchestratorFunction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentVersion&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;alias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ProdAlias&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;aliasName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Outputs&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OrchestratorFunctionArn&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Use this qualified ARN to invoke the durable function&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;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OrdersTableName&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ordersTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;InventoryTableName&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inventoryTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableName&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I love it. This is just architectural code. The actual workflow logic isn’t included here.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧪 A crucial point in DevEx: testing
&lt;/h2&gt;

&lt;p&gt;Ok, let's try to go deeper. I've written the code for both solutions, both come with trade-offs. &lt;strong&gt;Now I should test it before even thinking about deploying.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where &lt;code&gt;Lambda durable functions&lt;/code&gt; really stands out for me: it feels as straightforward as using Node's &lt;code&gt;test runner&lt;/code&gt;, &lt;code&gt;Jest&lt;/code&gt; or any other testing framework we’re already familiar with.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test locally with node on Durable Functions
&lt;/h3&gt;

&lt;p&gt;Having just a single function is a big advantage because there’s no AWS infrastructure involved, and no need to mock &lt;code&gt;Step Functions&lt;/code&gt;. You simply write your tests and run &lt;code&gt;npm test&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let’s start by importing the necessary libraries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;LocalDurableTestRunner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;WaitingOperationStatus&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-testing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;OperationType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OperationStatus&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-sdk/client-lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handler&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;../orchestrator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can create the test suite by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;initializing the test environment using the &lt;code&gt;setupTestEnvironment&lt;/code&gt; function and passing skipTime: true in Jest’s &lt;code&gt;beforeAll&lt;/code&gt; hook.&lt;/li&gt;
&lt;li&gt;tearing down the test environment using &lt;code&gt;teardownTestEnvironment&lt;/code&gt; in Jest’s &lt;code&gt;afterAll&lt;/code&gt; hook.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Order Dispatch Durable Function&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;beforeAll&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;await&lt;/span&gt; &lt;span class="nx"&gt;LocalDurableTestRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setupTestEnvironment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;skipTime&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="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;afterAll&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;await&lt;/span&gt; &lt;span class="nx"&gt;LocalDurableTestRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;teardownTestEnvironment&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;We are now ready to initialize our test. In this case, the scope is completing the workflow with confirmation. Let’s define the runner and connect it to the imported handler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should execute complete workflow with 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="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;runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LocalDurableTestRunner&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;handlerFunction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;handler&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;Next, we define our &lt;code&gt;orderEvent&lt;/code&gt; (i.e., the incoming order).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orderEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;buyerEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ITM-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quantity&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now start the execution on the runner, passing our &lt;code&gt;orderEvent&lt;/code&gt;, and wait for it to complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Start execution (will pause at callback)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;executionPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runner&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="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderEvent&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we have a human in the loop to simulate, we can use &lt;code&gt;runner.getOperation&lt;/code&gt; to get the callback operation and wait until it is &lt;code&gt;STARTED&lt;/code&gt;. Then we submit our decision with sendCallbackSuccess and wait for it to be &lt;code&gt;COMPLETED&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Get callback operation and wait for it to be ready&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callbackOp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;await&lt;/span&gt; &lt;span class="nx"&gt;callbackOp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WaitingOperationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STARTED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Send approval callback&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;callbackOp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendCallbackSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;decision&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirm&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;callbackOp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WaitingOperationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COMPLETED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we wait for the execution to finish and verify the expected outcome (in this case, that the order is confirmed).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Wait for execution to complete&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;execution&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;executionPromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify execution succeeded&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUCCEEDED&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getResult&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&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;orderId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^ORD-/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&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;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To complete our test, we can also verify that the other steps executed successfully using &lt;code&gt;runner.getOperation&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Verify operations executed&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saveOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;save-order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saveOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getType&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OperationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STEP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saveOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OperationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUCCEEDED&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;checkInventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;check-inventory&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkInventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getType&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OperationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STEP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkInventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OperationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUCCEEDED&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;waitTwoDays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wait-two-days&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;waitTwoDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getType&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OperationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WAIT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;waitTwoDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OperationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUCCEEDED&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;generateEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generate-email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;generateEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getType&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OperationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STEP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;generateEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OperationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUCCEEDED&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;sendEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getType&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OperationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STEP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OperationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUCCEEDED&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;In the end, writing and testing a durable workflow like this is surprisingly simple and enjoyable. With just a single function and the local test runner, you don’t have to deal with complex AWS infrastructure or mocking, and the code remains clear and easy to follow. It’s genuinely satisfying to see the entire workflow execute and verify each step with minimal setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local testing has always been challenging for Step Functions
&lt;/h3&gt;

&lt;p&gt;Instead, we have a few options when it comes to testing something implemented with &lt;code&gt;Step Functions&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remotely&lt;/strong&gt;, by deploying to AWS and testing against real infrastructure.&lt;br&gt;
&lt;strong&gt;Locally&lt;/strong&gt;, using frameworks or tools that simulate &lt;code&gt;Step Functions&lt;/code&gt;.&lt;br&gt;
Or &lt;strong&gt;Unit testing&lt;/strong&gt;, by testing each Lambda function individually.&lt;/p&gt;

&lt;p&gt;However, even with these approaches, we’re still missing proper end-to-end testing of the entire workflow, which is often the most critical part to validate.&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="c"&gt;# Option 1: Deploy to AWS and test remotely (slow, costs money)&lt;/span&gt;
aws stepfunctions start-execution &lt;span class="nt"&gt;--state-machine-arn&lt;/span&gt; arn:aws:...

&lt;span class="c"&gt;# Option 2: Use Step Functions Local (limited, requires Docker)&lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8083:8083 amazon/aws-stepfunctions-local
&lt;span class="c"&gt;# Still need to mock all Lambda functions&lt;/span&gt;
&lt;span class="c"&gt;# Still need to mock DynamoDB, SNS, SES...&lt;/span&gt;

&lt;span class="c"&gt;# Option 3: Unit test each Lambda separately&lt;/span&gt;
&lt;span class="c"&gt;# But you can't test the workflow orchestration!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;All these options make rapid iteration much harder&lt;/strong&gt;. Only the first approach truly gives me confidence that I’ve tested the workflow end-to-end. But this forces me to mentally switch between deployment and testing, which breaks flow and slows down development. For me, that kind of friction is a productivity killer.&lt;/p&gt;

&lt;p&gt;There is a clear winner for me here, and it's not &lt;code&gt;Step Functions&lt;/code&gt;, &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/aws-step-functions-local-testing-teststate-api/" rel="noopener noreferrer"&gt;while testing continues to improve&lt;/a&gt; thanks to AWS folks.&lt;/p&gt;

&lt;h2&gt;
  
  
  💻 What bothers devs: after deploy ops.
&lt;/h2&gt;

&lt;p&gt;Both the business, and sometimes we developers as well, tend to underestimate the importance of day-to-day operations after the first deployment. Production environments involve change requests, debugging, fixes, and monitoring.&lt;/p&gt;

&lt;p&gt;Here’s what the daily development workflow looks like with each tool in this phase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Durable Functions
&lt;/h3&gt;

&lt;p&gt;What's going on if I receive a change request?&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Edit my function&lt;/li&gt;
&lt;li&gt;Write/update test&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npm test&lt;/code&gt; (1 second to iterate)&lt;/li&gt;
&lt;li&gt;Deploy with &lt;code&gt;cdk deploy&lt;/code&gt; (very short time as this is just one function drift to be released)&lt;/li&gt;
&lt;li&gt;Only now invoke the function endpoint to be sure everything is ok.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;What will I do if I need to debug anything?&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Look in console (or get via CLI) &lt;code&gt;CloudWatch&lt;/code&gt; logs &lt;strong&gt;in a single log group&lt;/strong&gt;: anyone using &lt;code&gt;CloudWatch&lt;/code&gt; should know the nightmare of navigating multiple logs groups. Also, the &lt;code&gt;Lambda durable functions&lt;/code&gt; console surfaces logs and execution details without having to jump to &lt;code&gt;CloudWatch&lt;/code&gt;, letting you focus on your single &lt;code&gt;Lambda&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;See complete execution flow in one place&lt;/li&gt;
&lt;li&gt;Replay the execution locally with tests and see what's broken&lt;/li&gt;
&lt;li&gt;Fix the bug, run test, deploy.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;What if I need to onboard another developer, regardless of seniority?&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Share the single function and walk the developer through the code&lt;/li&gt;
&lt;li&gt;They will probably understand it quickly, since it’s just a single function.&lt;/li&gt;
&lt;li&gt;Hopefully they can contribute within hours, test it locally, give me a PR which contains the "full micro-service workflow" logic without having to change a line of architecture.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;Moreover, now that we live in the era of AI coding assistants, AI powered IDEs, and autonomous agents for coding, it has never been easier to onboard new developers. Providing them with precise, focused context around a single Lambda function is undoubtedly one of the most effective ways to get them productive in a very short time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I wouldn’t be surprised if we soon see dedicated Kiro Power-Ups and SOPs for Durable Functions in the AWS MCP ecosystem.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step Functions
&lt;/h3&gt;

&lt;p&gt;Let's see the similar dev scenarios with &lt;code&gt;Step Functions&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What's going on if I need to implement a change request?&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Modify state machine definition in CDK with &lt;code&gt;ASL&lt;/code&gt; language&lt;/li&gt;
&lt;li&gt;Modify one or multiple Lambda functions&lt;/li&gt;
&lt;li&gt;No quick way to test locally (or should have a setup to do it, and so your teammates)&lt;/li&gt;
&lt;li&gt;Deploy with &lt;code&gt;cdk deploy&lt;/code&gt; (2-3 minutes as the drift would be much more than a single &lt;code&gt;Lambda&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Go and test manually in AWS Console to be sure of the implementation, but get back to the code if anything isn't right (oh my..)&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;And if I catch an error and I should debug it?&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Again open &lt;code&gt;Step Functions&lt;/code&gt; execution in AWS Console&lt;/li&gt;
&lt;li&gt;Click through each state to see input/output&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;CloudWatch&lt;/code&gt; logs for relevant &lt;code&gt;Lambda&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Correlate timestamps across services&lt;/li&gt;
&lt;li&gt;Maybe use &lt;code&gt;X-Ray&lt;/code&gt; for tracing&lt;/li&gt;
&lt;li&gt;Fix the bug, redeploy, restart again.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't get me wrong, that’s perfectly fine. I genuinely like &lt;code&gt;Step Functions&lt;/code&gt; because, despite those "velocity" trade-offs in DevEx, they enforce a proper orchestration of distributed systems. They also provide a clear visualization of the workflow in the console, make it easy to catch errors at the failing state, and help you understand what’s happening inside complex orchestrations, ultimately simplifying what would otherwise be a very intricate system.&lt;/p&gt;

&lt;p&gt;But what if I need to onboard someone who isn’t an AWS expert and isn’t very familiar with &lt;code&gt;Step Functions&lt;/code&gt; or workflow architecture in general?&lt;/p&gt;

&lt;p&gt;I’d have to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Introduce &lt;code&gt;Amazon States Language&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Explain each &lt;code&gt;Lambda&lt;/code&gt; functions&lt;/li&gt;
&lt;li&gt;Walk through the &lt;code&gt;CDK&lt;/code&gt; stack&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means that a developer would typically become productive only after a few days, and it really depends on their seniority and prior experience with AWS. Trust me, this can easily become a waste of time and a nightmare, both from the mentor’s perspective and the learner’s.&lt;/p&gt;

&lt;p&gt;From a DevEx perspective, &lt;code&gt;Lambda durable functions&lt;/code&gt; are a major step forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤔 So when do Step Functions still make sense?
&lt;/h2&gt;

&lt;p&gt;However, &lt;code&gt;Lambda durable functions&lt;/code&gt; won’t always be the right answer.&lt;br&gt;
&lt;code&gt;Step Functions&lt;/code&gt; has genuine advantages in two main cases.&lt;/p&gt;
&lt;h3&gt;
  
  
  Visual workflows matter
&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%2Fs5wrh43nneqey9oic2n7.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%2Fs5wrh43nneqey9oic2n7.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of the biggest advantages of &lt;code&gt;Step Functions&lt;/code&gt; is that stakeholders can see the workflow visually: this visual representation is not just a “nice-to-have.” It’s crucial for stakeholder demos, where non-technical team members can quickly understand the workflow and see how processes progress.&lt;/p&gt;

&lt;p&gt;It also simplifies compliance reviews as auditors can trace exactly what happens at each step without digging through code.&lt;/p&gt;

&lt;p&gt;What about operations monitoring? DevOps and support teams can spot failures or bottlenecks immediately, understand dependencies between steps, and react faster (sometimes without having access to the code itself).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In short, having a clear, visual workflow turns complex orchestration into something everyone can comprehend, communicate about, and trust.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Native AWS Service Integration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Step Functions&lt;/code&gt; has native integrations with 200+ AWS services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Directly invoke services without Lambda&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DynamoPutItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SaveOrder&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;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ordersTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;item&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="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SqsSendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QueueOrder&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;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messageBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TaskInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromObject&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="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EcsRunTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ProcessOrder&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;cluster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ecsCluster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;taskDefinition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderProcessor&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;CDK&lt;/code&gt; you have a lot of options for simple task basically for each AWS Service.&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%2Ftxw0uss2ap2vqfkl98qx.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%2Ftxw0uss2ap2vqfkl98qx.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For AWS service orchestrations, this is actually pretty clean.&lt;br&gt;
While if you want to implement something directly in code with a &lt;code&gt;Lambda durable function&lt;/code&gt;, obviously you need to use the &lt;code&gt;SDK&lt;/code&gt;, and that logic becomes part of your business layer.&lt;/p&gt;
&lt;h2&gt;
  
  
  🚨 So when should we prefer &lt;code&gt;Lambda durable functions&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;First, when you need a &lt;strong&gt;code-first philosophy&lt;/strong&gt; based on widely used languages such as &lt;code&gt;TypeScript&lt;/code&gt; or &lt;code&gt;Python&lt;/code&gt;.&lt;br&gt;
You write orchestration in the same language as your business logic. No need to learn a domain-specific language. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also, if you want a simple local development and testing option.&lt;/strong&gt;&lt;br&gt;
For the first time, you can test complex workflows locally without AWS infrastructure.&lt;/p&gt;

&lt;p&gt;There’s no need to learn &lt;code&gt;Amazon States Language&lt;/code&gt; (&lt;code&gt;ASL&lt;/code&gt;) and what used to feel awkward and complex is now trivial: you can define, modify, and visualize workflows in the code, without diving into verbose JSON or mastering intricate patterns.&lt;/p&gt;

&lt;p&gt;As for an example of complex nested workflows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&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;runInChildContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;parent&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;parent&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;child1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runInChildContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child1&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;c1&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;grandchild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runInChildContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grandchild&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;gc&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="c1"&gt;// Deeply nested orchestration - easy!&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;grandchild&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;child1&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;What about parallel executions?&lt;br&gt;
As simple as using a map&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Process N items in parallel (N determined at runtime)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&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="mi"&gt;5&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;results&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;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;process-items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;items&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;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&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;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&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;`process-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&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;processItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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;maxConcurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;completionConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;minSuccessful&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="na"&gt;toleratedFailureCount&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throwIfError&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;allResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getResults&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or saga patterns for distributed transactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;compensations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;try&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;payment&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="s1"&gt;charge-payment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chargeCustomer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;compensations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;refundCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&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;inventory&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="s1"&gt;reserve-inventory&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reserveItems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;compensations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;releaseItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inventory&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;shipment&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="s1"&gt;create-shipment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shipOrder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// All succeeded!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Automatically compensate in reverse order&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;const&lt;/span&gt; &lt;span class="nx"&gt;compensate&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;compensations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&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="s1"&gt;compensate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;compensate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&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;Remember you will be an early adopter!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Being on the cutting edge means the community is smaller and there are fewer examples available, but the ecosystem is growing rapidly. Documentation is still maturing, and you may encounter some rough edges along the way.&lt;/p&gt;

&lt;p&gt;On the other hand: AWS is actively improving it, you have the opportunity to adopt modern patterns early, you will get skills that will become more valuable over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 My recommendation and final thoughts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For new projects, really consider &lt;code&gt;Lambda durable functions&lt;/code&gt;. It's not just hype for a new pattern: the developer experience and local testing capabilities are significant advantages.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Existing &lt;code&gt;Step Functions&lt;/code&gt;? No need to rush a migration. Try &lt;code&gt;Lambda durable functions&lt;/code&gt; for your next new workflow and compare the experience.&lt;/p&gt;

&lt;p&gt;Do you really have to choose between them? &lt;br&gt;
The short answer is no, and you shouldn't. &lt;br&gt;
You can use both solutions depending on your use case and also use a &lt;code&gt;Lambda durable function&lt;/code&gt; in a wider orchestration built with &lt;code&gt;Step Functions&lt;/code&gt;. This is a great pattern for creating ‘leaf’ workflows focused on a specific concern, decoupled from others. You can "enforce" architectural decoupling when needed and benefit from a single Lambda’s advantages when convenient.&lt;/p&gt;

&lt;p&gt;Let’s be clear: both tools work. &lt;br&gt;
But they reflect different eras of &lt;code&gt;serverless&lt;/code&gt; thinking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Step Functions&lt;/strong&gt; (2016): It's a safe, proven choice. Visual workflows, mature ecosystem, smooth integration with AWS ecosystem, battle-tested. It is still a very good choice for mature and ops teams.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Durable Functions&lt;/strong&gt; (2025): Code-first, local testing, modern patterns. Good for devs, new workflows, specific ones to integrate in a wider orchestration and time-to-market.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After spending weeks working with both, I can say that, to me, &lt;strong&gt;Durable Functions feels like where serverless orchestration should've been all along&lt;/strong&gt;. &lt;/p&gt;
&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;AWS Lambda durable functions Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html" rel="noopener noreferrer"&gt;AWS Step Functions Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also don't miss this awesome presentation video by Michael Gasch and Eric Johnson at the latest re:Invent in Dec 2025.&lt;/p&gt;

&lt;p&gt;

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


&lt;/p&gt;

&lt;p&gt;If you'd like a walkthrough of this excellent presentation, you can find one on &lt;a href="https://repost.aws/articles/ARc8wmu4l9TKywZCHX-_nn6w/re-invent-2025-deep-dive-on-aws-lambda-durable-functions" rel="noopener noreferrer"&gt;re:Post&lt;/a&gt; or an autogenerated &lt;a href="https://dev.to/kazuya_dev/aws-reinvent-2025-new-launch-deep-dive-on-aws-lambda-durable-functions-cns380-3edi"&gt;here on dev.to&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🙋 Who am I
&lt;/h2&gt;

&lt;p&gt;I'm &lt;a href="https://www.linkedin.com/in/desiodavide" rel="noopener noreferrer"&gt;D. De Sio&lt;/a&gt; and I work as a Head of Software Engineering in &lt;a href="https://eleva.it/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;br&gt;
As of Feb 2026, I’m an &lt;a href="https://www.credly.com/badges/9929fdf2-7a3d-4013-9de6-57c80e4920b9/public_url" rel="noopener noreferrer"&gt;AWS Certified Solution Architect Professional&lt;/a&gt; and &lt;a href="https://www.credly.com/badges/8c5a1487-191b-429e-8c2d-7cee43bf316b/public_url" rel="noopener noreferrer"&gt;AWS Certified DevOps Engineer Professional&lt;/a&gt;, but also a &lt;a href="https://www.linkedin.com/company/aws-user-group-pavia/" rel="noopener noreferrer"&gt;User Group Leader (in Pavia)&lt;/a&gt;, an &lt;strong&gt;AWS Community Builder&lt;/strong&gt; and, last but not least, a #serverless enthusiast.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For those of you who’ve made it this far, I’m not exactly the person in the image (and for those who know me, just not in this photo!) but it’s always fun and interesting to see how GenAI imagines you.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>aws</category>
      <category>lambda</category>
      <category>stepfunctions</category>
      <category>durablefunctions</category>
    </item>
    <item>
      <title>Nevermore.dev: LLM-as-judge on Lambda Durable Functions</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Tue, 27 Jan 2026 17:47:03 +0000</pubDate>
      <link>https://dev.to/aws-builders/nevermoredev-llm-as-judge-on-lambda-durable-functions-2j22</link>
      <guid>https://dev.to/aws-builders/nevermoredev-llm-as-judge-on-lambda-durable-functions-2j22</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“The past is dead. The future? Let’s make it less painful.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Writing post‑mortems is one of those things everyone agrees are important and everyone secretly hates doing. They’re tedious, emotionally draining, and they require the worst kind of energy: &lt;strong&gt;clear thinking after chaos&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And you find yourself thinking both: “I want this to happen never more” and “I want to write this never more”. And in that quiet, the chaos lingers. Undebuggable, relentless, eternal.&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%2Fmedia3.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExN3RhNzh0bGk3d3JldnlhN3NyeXB2YTd2NngzMmF6ZW9oZzE0NGVxdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FcirKPyIvk9Wo6uoqnX%2Fgiphy.gif" 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%2Fmedia3.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExN3RhNzh0bGk3d3JldnlhN3NyeXB2YTd2NngzMmF6ZW9oZzE0NGVxdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FcirKPyIvk9Wo6uoqnX%2Fgiphy.gif" width="500" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nevermore.dev&lt;/strong&gt; was born from that very specific kind of developer pain: a post‑mortem generator with two moods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Professional&lt;/strong&gt;: calm, neutral, executive‑friendly (but so boring)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creepy&lt;/strong&gt;: full Addams‑family vibes, because if we have to revisit horror, we might as well embrace it 🦇&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%2Fhjkslu6ar1kpm6ldksrs.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%2Fhjkslu6ar1kpm6ldksrs.png" alt=" " width="800" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dark UI aside, the interesting tech geeky part lives under the hood: brand new &lt;strong&gt;AWS Lambda Durable Function&lt;/strong&gt; powering an &lt;strong&gt;LLM‑as‑Judge&lt;/strong&gt; workflow on &lt;strong&gt;Amazon Bedrock (using Nova models)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏗️ Architecture
&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%2Fovfbk8g04kl6b0uwjenq.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%2Fovfbk8g04kl6b0uwjenq.png" alt=" " width="800" height="1199"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The solution I had in mind was fairly simple in shape, even if layered in execution.&lt;/p&gt;

&lt;p&gt;The flow starts from an &lt;code&gt;Amplify Gen 2&lt;/code&gt; frontend. An &lt;code&gt;AppSync GraphQL&lt;/code&gt; mutation triggers a lightweight &lt;code&gt;Lambda&lt;/code&gt; whose only job is to start the AI workflow, not to run it (it acts as the sync backend). From there, everything moves asynchronously into a &lt;code&gt;Durable Lambda&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This durable function is where the real logic lives&lt;/strong&gt;. Instead of relying on a single model, the workflow follows an &lt;strong&gt;LLM as a judge&lt;/strong&gt; pattern. Generation happens in parallel: a fast model produces a first candidate, while a more balanced one generates an alternative. The point here is diversity, not consensus.&lt;/p&gt;

&lt;p&gt;Once both candidates are available, a higher-quality model steps in as a judge. It evaluates the outputs and selects the best result, acting as a decision layer rather than a generator.&lt;/p&gt;

&lt;p&gt;All model calls go through &lt;code&gt;Amazon Bedrock&lt;/code&gt;, keeping the system decoupled and letting each model focus on what it does best: speed, balance, or quality.&lt;/p&gt;

&lt;p&gt;In this way, the main benefit I was aiming for was avoiding the setup of a &lt;code&gt;Step Function&lt;/code&gt; with all its inherent complexity, while still having pure code to manage a durable, asynchronous workflow directly inside a &lt;code&gt;Lambda&lt;/code&gt;, a &lt;code&gt;Durable Lambda&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🕯️ A bit more about Nevermore.dev
&lt;/h2&gt;

&lt;p&gt;At its core, I wanted a simple but effective CRUD panel to help me manage post-mortems. I've &lt;code&gt;semi-vibed&lt;/code&gt; (using specs, refining it..) it with &lt;code&gt;Kiro&lt;/code&gt; with my personal &lt;a href="https://dev.to/aws-builders/building-a-kiro-power-for-aws-amplify-gen-2-54gk"&gt;Amplify Gen 2 Kiro Power&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The product comes with all the usual Amplify Gen 2 built-in features.&lt;br&gt;
Fully integrated with &lt;code&gt;Cognito&lt;/code&gt; for auth.&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%2F7ddpdfyefcz23ka2jy87.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%2F7ddpdfyefcz23ka2jy87.png" alt=" " width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CRUD operations featured by an API wiht &lt;code&gt;AppSync&lt;/code&gt; and &lt;code&gt;DynamoDb&lt;/code&gt; as storage.&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%2Fnsuy3nhs762bxpmggfhw.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%2Fnsuy3nhs762bxpmggfhw.png" alt=" " width="800" height="740"&gt;&lt;/a&gt;&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%2Fb6m79o9t8rg0edasbj1b.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%2Fb6m79o9t8rg0edasbj1b.png" alt=" " width="800" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything deployed with just &lt;code&gt;npx amplify deploy&lt;/code&gt;.&lt;br&gt;
Awesome to cut-off time to production and give me the product I was searching for.&lt;/p&gt;
&lt;h2&gt;
  
  
  🤖 AI to the rescue
&lt;/h2&gt;

&lt;p&gt;The real core of the product, by the way, is using AI to generate clearer, more useful incident descriptions and root cause analyses (which is why I’m building and using it in the first place). What truly bores me about writing post-mortems isn’t the incident itself, but the ritual around it: &lt;strong&gt;finding the right tone, the right template, the right wording&lt;/strong&gt;. With AI, all of that can be &lt;strong&gt;reduced to a single prompt that produces exactly what I need, but relying on a single model often forces me to switch models, manually evaluate the output, or ask another model to judge it&lt;/strong&gt;. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It’s all still far too manual for something that’s supposed to be part of my daily routine.&lt;/p&gt;
&lt;/blockquote&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%2Fxh41mps9q6t4nvworovq.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%2Fxh41mps9q6t4nvworovq.png" alt=" " width="800" height="835"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thus, I wanted the ability to parallelize multiple generations across different models, then use another model to evaluate the results and pick the best one, which is where the &lt;code&gt;Lambda Durable Function&lt;/code&gt; comes into play. Finally, I can have the best output to be immediately available in Markdown, ready to be copied into team cards or any other notification system.&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%2Fft6xr55bbhhwjk3rnbn2.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%2Fft6xr55bbhhwjk3rnbn2.png" alt=" " width="800" height="834"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🪦 But it wasn't funny
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;As expected, it wasn’t funny enough.&lt;/strong&gt;&lt;br&gt;
Post-mortems aren’t funny, at least, not yet.&lt;br&gt;
But they should &lt;strong&gt;NEVERMORE&lt;/strong&gt; be boring.&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%2Fmedia4.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExMnh3OHdkcGpnaG51OGl4MmloMmh3eTNzc2E4cXQ5bDdpdnI3NTQ0cCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FKuIPUkypeEZPHuDyOI%2Fgiphy.gif" 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%2Fmedia4.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExMnh3OHdkcGpnaG51OGl4MmloMmh3eTNzc2E4cXQ5bDdpdnI3NTQ0cCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FKuIPUkypeEZPHuDyOI%2Fgiphy.gif" width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As I was &lt;code&gt;semi-vibing&lt;/code&gt; the frontend with &lt;code&gt;Kiro&lt;/code&gt; and my personal &lt;a href="https://dev.to/aws-builders/building-a-kiro-power-for-aws-amplify-gen-2-54gk"&gt;Amplify Gen 2 Kiro Power&lt;/a&gt;, it only took a couple of prompts to add a button theme switch and fully embrace a creepy mode for dark theme users (aren't we talking about post-mortems?). Since I &lt;strong&gt;nevermore&lt;/strong&gt; wanted to write a dull post-mortem, what better muse than the ever-macabre Addams Family?&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%2Flcrs9j3dd7envuny4m4j.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%2Flcrs9j3dd7envuny4m4j.png" alt=" " width="800" height="835"&gt;&lt;/a&gt;&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%2Fvjmfume92qy3fh2n489m.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%2Fvjmfume92qy3fh2n489m.png" alt=" " width="800" height="831"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now reading a post-mortem in full Addams Family tone is incredibly satisfying, and I regret nothing.&lt;/strong&gt;&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%2Fmedia0.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExaG1jZmNnaWE0cXpzbWo4c2k2N3lxZWhuYmhvNnZ2b214OTE1dG54NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FhrUeHm9PkCu3fM07bn%2Fgiphy.gif" 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%2Fmedia0.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExaG1jZmNnaWE0cXpzbWo4c2k2N3lxZWhuYmhvNnZ2b214OTE1dG54NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FhrUeHm9PkCu3fM07bn%2Fgiphy.gif" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🧱 The stack
&lt;/h2&gt;

&lt;p&gt;Okay, creepy awesome. But let’s forget about the UI for a moment and get to the underline technical part.&lt;br&gt;
Nevermore.dev is built entirely on AWS with a fairly modern setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amplify Gen 2&lt;/strong&gt;: frontend and full‑stack wiring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Cognito&lt;/strong&gt;: authentication and authorization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS AppSync&lt;/strong&gt;: GraphQL API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt;: NoSQL records persistence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lambda Durable Functions&lt;/strong&gt;: AI orchestration layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Bedrock (Nova)&lt;/strong&gt;: AI models engine for text generation &amp;amp; evaluations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As we said, the brain of the system is a single &lt;code&gt;Durable Lambda Function&lt;/code&gt; that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generates multiple enhanced versions of a post‑mortem section&lt;/li&gt;
&lt;li&gt;Uses another LLM to &lt;em&gt;judge&lt;/em&gt; them&lt;/li&gt;
&lt;li&gt;Returns the best one&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of this really happens &lt;strong&gt;inside one function&lt;/strong&gt;, with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parallel execution&lt;/li&gt;
&lt;li&gt;checkpointing&lt;/li&gt;
&lt;li&gt;resumability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The point here is: no Step Functions. No external state machines. No need to define complex architectures or multiple lambdas.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s where &lt;code&gt;Durable Functions&lt;/code&gt; really shine.&lt;br&gt;
Let's see why comparing to &lt;code&gt;Step Functions&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  ⚔️ Why Lambda Durable Functions (instead of Step Functions)
&lt;/h2&gt;

&lt;p&gt;Traditionally, a workflow like this would scream &lt;code&gt;Step Functions&lt;/code&gt;.&lt;br&gt;
They work and are a very good choice, but they come with trade‑offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON‑heavy definitions&lt;/li&gt;
&lt;li&gt;state management between steps&lt;/li&gt;
&lt;li&gt;mental context switching between states&lt;/li&gt;
&lt;li&gt;orchestration logic separated from business logic (this could be a pro, but we should see the use case)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Lambda Durable Functions&lt;/code&gt; flip the model:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;You write normal async code in just one function and AWS handles durability.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With a single Lambda you can get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;long-running executions (without losing state)&lt;/li&gt;
&lt;li&gt;automatic checkpointing&lt;/li&gt;
&lt;li&gt;deterministic replay&lt;/li&gt;
&lt;li&gt;parallel fan‑out / fan‑in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For LLM workflows, where latency, retries, partial failures, and cost control matter, this is huge.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here was my architecture map before starting, the core is an LLM workflow which should implement LLM-as-judge pattern. Having a solution to be placed all in the &lt;code&gt;Durable Lambda&lt;/code&gt;, while front-end act just as a client, is a big milestone to cut off time to production.&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%2Fql2ekiq1d481emyvsr5y.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%2Fql2ekiq1d481emyvsr5y.png" alt=" " width="800" height="1178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How would this look if implemented with &lt;code&gt;AWS Step Functions&lt;/code&gt;?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lambda&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Extra elements required&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lambda 1&lt;/td&gt;
&lt;td&gt;State 1&lt;/td&gt;
&lt;td&gt;Call LLM&lt;/td&gt;
&lt;td&gt;Must be separate; JSON definition required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda 2&lt;/td&gt;
&lt;td&gt;State 2&lt;/td&gt;
&lt;td&gt;Process response&lt;/td&gt;
&lt;td&gt;Another separate Lambda or branch logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda 3&lt;/td&gt;
&lt;td&gt;State 3&lt;/td&gt;
&lt;td&gt;Write to DB&lt;/td&gt;
&lt;td&gt;Separate Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda 4&lt;/td&gt;
&lt;td&gt;State 4&lt;/td&gt;
&lt;td&gt;Map / Parallel&lt;/td&gt;
&lt;td&gt;For fan-out of multiple LLM calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda 5&lt;/td&gt;
&lt;td&gt;State 5&lt;/td&gt;
&lt;td&gt;Wait / Choice&lt;/td&gt;
&lt;td&gt;For retry / fallback logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda 6&lt;/td&gt;
&lt;td&gt;State 6&lt;/td&gt;
&lt;td&gt;Aggregate results&lt;/td&gt;
&lt;td&gt;Another separate Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda 7&lt;/td&gt;
&lt;td&gt;State 7&lt;/td&gt;
&lt;td&gt;Success / Fail&lt;/td&gt;
&lt;td&gt;Final orchestration state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using &lt;code&gt;AWS Step Functions&lt;/code&gt; would require a far larger amount of architectural and logical code compared to a single &lt;code&gt;Lambda Durable Function&lt;/code&gt;. &lt;strong&gt;It’s a huge time saver, eliminates constant context-switching, and reinforces a DDD-inspired approach where my Lambda acts as a fully responsible micro-service, handling parallel execution and the orchestration of results end-to-end.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I always embrace DDD when it makes sense, and I stick to KISS: keep the model focused, the boundaries explicit, and the moving parts to the absolute minimum.&lt;/p&gt;

&lt;p&gt;If you’re looking for a solid framework to choose between the two options, &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-step-functions.html#durable-sfn-decision-framework" rel="noopener noreferrer"&gt;there’s an excellent decision framework here&lt;/a&gt;. Moreover, as suggested in the hybrid architecture chapter, you may even benefit from applying both approaches in your application.&lt;/p&gt;
&lt;h2&gt;
  
  
  ⚖️ LLM‑as‑a‑Judge
&lt;/h2&gt;

&lt;p&gt;So, as written before, instead of trusting a single model output, Nevermore.dev uses this powerful pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generate candidates&lt;/strong&gt; post-mortem using multiple fast/cheap models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge them&lt;/strong&gt; using a more capable reasoning model&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This, compared with a single model response, gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;better quality&lt;/li&gt;
&lt;li&gt;more consistency&lt;/li&gt;
&lt;li&gt;controllable cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my case, I'm using three models of the &lt;code&gt;Nova&lt;/code&gt; family:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;SMALL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eu.amazon.nova-micro-v1:0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;MEDIUM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eu.amazon.nova-lite-v1:0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;LARGE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eu.amazon.nova-pro-v1:0&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;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🛠️ Deploying Durable Function with CDK
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Durable functions&lt;/code&gt;, per documentation, are still officially not supported by Amplify itself but a &lt;a href="https://github.com/aws-amplify/amplify-backend/pull/3069" rel="noopener noreferrer"&gt;PR&lt;/a&gt; has been &lt;a href="https://github.com/aws-amplify/amplify-backend/releases/tag/%40aws-amplify%2Fbackend%401.20.0" rel="noopener noreferrer"&gt;merged&lt;/a&gt; and I expect this soon to come. &lt;/p&gt;

&lt;p&gt;Meanwhile, it's very simple to deploy a durable function with &lt;code&gt;CDK&lt;/code&gt; (or other IaC tools). It also enforce to me the concept that the durable function is a specific component which should be decoupled by "app architecture" created with &lt;code&gt;Amplify&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It’s mostly a matter of configuration of the &lt;code&gt;Durable Lambda Function&lt;/code&gt; itself.&lt;br&gt;
&lt;strong&gt;And it feels exactly how it should do: an extension of what we already are able to do with CDK.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;durableFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DurableFunction&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;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_22_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nevermore-dev-durable-ai-generator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;memorySize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&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;cfnFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;durableFunction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultChild&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CfnFunction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;cfnFunction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;durableConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;executionTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hours&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="nf"&gt;toSeconds&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;Giving it the right permissions (security first!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;durableFunction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToRolePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lambda:CheckpointDurableExecution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lambda:GetDurableExecutionState&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;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;//better restrict this permission!&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-security.html" rel="noopener noreferrer"&gt;Be aware to restrict this permission resources attribute as suggested  here &lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I've used CDK as it's a good fit for a full Typescript project with &lt;code&gt;AWS Amplify Gen 2&lt;/code&gt; and &lt;code&gt;React&lt;/code&gt; but you can choose and learn how to deploy with your preferred IaC method (Cloudformation, CDK or SAM) a durable function &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-getting-started-iac.html" rel="noopener noreferrer"&gt;here in AWS docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you’re using CDK to deploy your &lt;code&gt;Lambda Durable Function&lt;/code&gt;, you should create a "proxy" function that acts as a backend to invoke it. The code is as simple as &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-invoking.html" rel="noopener noreferrer"&gt;described here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✍️ Writing the durable handler
&lt;/h2&gt;

&lt;p&gt;Again, this is the core part: no state machines, no glue code.&lt;br&gt;
&lt;strong&gt;Just code logic.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;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;originalText&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;context&lt;/span&gt; &lt;span class="o"&gt;||&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;theme&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;theme&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;addams&lt;/span&gt;&lt;span class="dl"&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;originalText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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="nf"&gt;getEmptyContextMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&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;enhancementPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createEnhancementPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalText&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;fieldType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&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;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateCandidates&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SMALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MEDIUM&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="nx"&gt;enhancementPrompt&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;judgePrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createJudgePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;candidates&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;fieldType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&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;judgment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;judgeAndSelectBest&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;MODEL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;judgePrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;originalText&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;judgment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enhancedText&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;Parallelism is implemented with a very simple &lt;code&gt;context.map&lt;/code&gt;&lt;br&gt;
Parallel, checkpointed, resumable. Exactly what flaky LLM calls need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidateResults&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;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Generate enhanced versions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;models&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;modelId&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;enhancement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&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="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;enhancement&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;Judging is implemented as a subsequent durable step&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&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;judge-best-version&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;judgeResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;converse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;judgeModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;judgePrompt&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 parsing fails, I fall back gracefully.&lt;br&gt;
No wasted inference. No duplicate cost.&lt;/p&gt;

&lt;p&gt;This complete pattern code is available &lt;a href="https://github.com/aws-samples/sample-ai-workflows-in-aws-lambda-durable-functions/blob/main/typescript/src/llm-as-judge.ts" rel="noopener noreferrer"&gt;here in aws samples repo&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  ✍️ Prompting
&lt;/h2&gt;

&lt;p&gt;The best thing about this stack is that once the pattern is implemented, you can easily reuse it across different use cases by simply adapting the prompts. Below are my (very simple) examples.&lt;/p&gt;

&lt;p&gt;This one is for generating candidate responses (where &lt;code&gt;fieldName&lt;/code&gt; is the name of the field I want to generate, e.g. description or root cause, and the &lt;code&gt;originalText&lt;/code&gt; is the starting point).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an experienced SRE reviewing a technical post-mortem {fieldName}.
Your task is to enhance this {fieldName} with professional insights and technical depth.

Original {fieldName}:
"""
{originalText}
"""

Requirements:
1. Expand and enhance the technical details with clarity and precision
2. Add relevant technical insights, metrics, and potential implications
3. Maintain a professional, clear, and concise tone
4. Use markdown formatting for better readability (headers, lists, code blocks)
5. Focus on actionable insights and lessons learned
6. If the original is empty or minimal, generate a comprehensive {fieldName} based on the context
7. Length: 200-400 words
8. Include specific technical recommendations and next steps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the prompt for the judge&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an experienced SRE Lead reviewing post-mortem documents for quality and accuracy.

Original {fieldName}:
"""
{originalText}
"""

Enhanced Versions:
{candidatesList}

Evaluate each version based on:
- Technical accuracy and depth
- Clarity and readability
- Appropriate use of markdown formatting
- Professional tone and structure
- Actionable insights and recommendations
- Completeness and thoroughness

Reply with JSON only (no other text):
{
  "bestIndex": &amp;lt;1-based index&amp;gt;,
  "reasoning": "&amp;lt;2-3 sentences explaining your choice&amp;gt;"
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interest part of this prompt is that even if i needed just the best response, I've tracked also 2 or 3 sentences explaining the choice. This could be useful to review the result if you wan to introduce a human in the loop with a notification review patter for which &lt;code&gt;Durable functions&lt;/code&gt; are a good fit too (&lt;a href="https://github.com/aws-samples/sample-ai-workflows-in-aws-lambda-durable-functions/blob/main/typescript/src/human-review.ts" rel="noopener noreferrer"&gt;see this example&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;We can also observe that the LLM-as-judge pattern is essentially a composition of other patterns: &lt;a href="https://github.com/aws-samples/sample-ai-workflows-in-aws-lambda-durable-functions/blob/main/typescript/src/parallel-invocation.ts" rel="noopener noreferrer"&gt;parallelism&lt;/a&gt; and &lt;a href="https://github.com/aws-samples/sample-ai-workflows-in-aws-lambda-durable-functions/blob/main/typescript/src/prompt-chaining.ts" rel="noopener noreferrer"&gt;prompt chain&lt;/a&gt; with &lt;a href="https://github.com/aws-samples/sample-ai-workflows-in-aws-lambda-durable-functions/blob/main/typescript/src/structured-output.ts" rel="noopener noreferrer"&gt;structured output&lt;/a&gt;.&lt;br&gt;
By combining these patterns, you gain the flexibility to tailor the solution more precisely to your specific use case.&lt;/p&gt;

&lt;p&gt;The creepy Addams theme give me the opportunity to test that just changing the prompt you can get the custom tone needed or fit your use case.&lt;/p&gt;

&lt;p&gt;Here is the adapted prompt for candidate responses&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are Grandmama Addams, an ancient and wise debugger from the Addams Family mansion. 
Your task is to enhance this technical post-mortem {fieldName} with your dark wisdom and supernatural insight.

Original {fieldName}:
"""
{originalText}
"""

Requirements:
1. Expand and enhance the technical details with clarity and depth
2. Add relevant technical insights and potential implications
3. Maintain an Addams Family tone - creepy, darkly humorous, but technically accurate
4. Use markdown formatting for better readability (headers, lists, code blocks)
5. Keep it professional yet delightfully macabre
6. If the original is empty or minimal, generate a comprehensive {fieldName} based on the context
7. Length: 200-400 words
8. Include specific technical recommendations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the adapted prompt for the judge&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are Morticia Addams, reviewing post-mortem documents for quality and accuracy.

Original {fieldName}:
"""
{originalText}
"""

Enhanced Versions:
{candidatesList}

Evaluate each version based on:
- Technical accuracy and depth
- Clarity and readability
- Appropriate use of markdown formatting
- Addams Family tone while remaining professional
- Actionable insights and recommendations

Reply with JSON only (no other text):
{
  "bestIndex": &amp;lt;1-based index&amp;gt;,
  "reasoning": "&amp;lt;2-3 sentences explaining your choice&amp;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%2Fmedia1.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExbHBxdTYwYzluY2Z2NHozNDlyNXdydGd3bjc2aHMzNTRiNzVwbjQ1MSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FkUS0uhiMyGqLfyN6pr%2Fgiphy.gif" 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%2Fmedia1.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExbHBxdTYwYzluY2Z2NHozNDlyNXdydGd3bjc2aHMzNTRiNzVwbjQ1MSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FkUS0uhiMyGqLfyN6pr%2Fgiphy.gif" width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I picked Morticia as the judge because her personality fits the role beautifully, but it was extremely funny to see how dramatically the tone changed just by switching to another member of the Addams family (choosing Fester to add a touch of madness was absolutely absurd).&lt;/p&gt;

&lt;h2&gt;
  
  
  👀 Let's see it in action
&lt;/h2&gt;

&lt;p&gt;When invoking the function we can see the execution in &lt;code&gt;Lambda&lt;/code&gt; Console under brand new &lt;code&gt;Durable Executions&lt;/code&gt; tab.&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%2F8pk99o0buqplryyc2032.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%2F8pk99o0buqplryyc2032.png" alt=" " width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You have a high level of detail at every step&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%2Fqg5kpe7l386vbmsi7ru6.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%2Fqg5kpe7l386vbmsi7ru6.png" alt=" " width="800" height="639"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 So, why this matters?
&lt;/h2&gt;

&lt;p&gt;Durable Functions make Lambda viable for &lt;strong&gt;serious AI workflows&lt;/strong&gt; without needing a &lt;code&gt;Step Function&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi‑step reasoning&lt;/li&gt;
&lt;li&gt;fan‑out / fan‑in&lt;/li&gt;
&lt;li&gt;partial failures&lt;/li&gt;
&lt;li&gt;cost‑aware retries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my use case: post‑mortems are still painful.&lt;br&gt;
But now, at least, they’re &lt;em&gt;elegantly painful&lt;/em&gt;, ai-assisted with the generation and judge of this generation in a single scoped micro-service without an external workflow handler tool.&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%2Fmedia0.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExaG1xemgwajdpajUwaHVkMzVud2E0cnByY25nbHU2cnFobW9lb2J1bSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FB4L7i0Kuhi5b2%2Fgiphy.gif" 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%2Fmedia0.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExaG1xemgwajdpajUwaHVkMzVud2E0cnByY25nbHU2cnFobW9lb2J1bSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FB4L7i0Kuhi5b2%2Fgiphy.gif" width="500" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📚 Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/aws-samples/sample-ai-workflows-in-aws-lambda-durable-functions" rel="noopener noreferrer"&gt;Sample ai workflows in AWS Lambda Durable Functions repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-getting-started-iac.html" rel="noopener noreferrer"&gt;Deploy Lambda durable functions with Infrastructure as Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-security.html" rel="noopener noreferrer"&gt;Security and permissions for Lambda durable functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-invoking.html" rel="noopener noreferrer"&gt;Invoking durable Lambda functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-step-functions.html" rel="noopener noreferrer"&gt;Durable functions or Step functions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🙋 Who am I
&lt;/h2&gt;

&lt;p&gt;I'm &lt;a href="https://www.linkedin.com/in/desiodavide" rel="noopener noreferrer"&gt;D. De Sio&lt;/a&gt; and I work as a Head of Software Engineering in &lt;a href="https://eleva.it/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;br&gt;
I'm currently (Apr 2025) an &lt;a href="https://www.credly.com/badges/9929fdf2-7a3d-4013-9de6-57c80e4920b9/public_url" rel="noopener noreferrer"&gt;AWS Certified Solution Architect Professional&lt;/a&gt; and &lt;a href="https://www.credly.com/badges/8c5a1487-191b-429e-8c2d-7cee43bf316b/public_url" rel="noopener noreferrer"&gt;AWS Certified DevOps Engineer Professional&lt;/a&gt;, but also a &lt;a href="https://www.linkedin.com/company/aws-user-group-pavia/" rel="noopener noreferrer"&gt;User Group Leader (in Pavia)&lt;/a&gt;, an &lt;strong&gt;AWS Community Builder&lt;/strong&gt; and, last but not least, a #serverless enthusiast.&lt;/p&gt;

&lt;p&gt;For the occasion, I proudly count myself among the students of &lt;a href="https://www.nevermoreacademy.com/" rel="noopener noreferrer"&gt;Nevermore Academy&lt;/a&gt; for outcasts.&lt;br&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%2F8r2qfatbmmjoadteol48.jpg" 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%2F8r2qfatbmmjoadteol48.jpg" alt=" " width="800" height="1143"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>genai</category>
      <category>lambda</category>
    </item>
    <item>
      <title>Building a Kiro Power for AWS Amplify Gen 2</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Fri, 09 Jan 2026 09:44:10 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-a-kiro-power-for-aws-amplify-gen-2-54gk</link>
      <guid>https://dev.to/aws-builders/building-a-kiro-power-for-aws-amplify-gen-2-54gk</guid>
      <description>&lt;h2&gt;
  
  
  🏃 TL;DR
&lt;/h2&gt;

&lt;p&gt;There’s a moment that often comes after big conferences.&lt;br&gt;
A brief pause, when the excitement fades and only the right questions remain.&lt;/p&gt;

&lt;p&gt;For me, that moment arrived after the latest AWS re:Invent in December, with the announcement of &lt;a href="https://kiro.dev/powers/" rel="noopener noreferrer"&gt;Kiro Powers&lt;/a&gt; and then almost by accident, when I stumbled upon a brand-new page in the AWS Amplify Gen 2 documentation: &lt;a href="https://docs.amplify.aws/react/start/mcp-server/" rel="noopener noreferrer"&gt;Build with AI assistants&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It made me ask a simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What if working with &lt;code&gt;Amplify Gen 2&lt;/code&gt; could feel more guided, more intentional, and less repetitive, every single time?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question eventually became &lt;a href="https://github.com/davide-desio-eleva/amplify-gen2-kiro-power" rel="noopener noreferrer"&gt;AWS Amplify Gen 2 Kiro Power&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ AWS MCP server and AWS SOPs
&lt;/h2&gt;

&lt;p&gt;I immediately started experimenting &lt;a href="https://docs.aws.amazon.com/aws-mcp/latest/userguide/what-is-mcp-server.html" rel="noopener noreferrer"&gt;AWS MCP SOPs&lt;/a&gt; integration for &lt;code&gt;AWS Amplify Gen 2&lt;/code&gt;, as prompts and guidance rules as suggested by documentation. I tried it in a few real scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building a &lt;strong&gt;full application from scratch&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Adding a &lt;strong&gt;new backend&lt;/strong&gt; to an existing frontend&lt;/li&gt;
&lt;li&gt;Creating a &lt;strong&gt;frontend&lt;/strong&gt; for a project where only the backend existed&lt;/li&gt;
&lt;li&gt;Being guided step by step through &lt;strong&gt;deployment and configuration&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What surprised me wasn’t just that it worked, it was &lt;em&gt;how well it worked&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The agent didn’t just execute commands: it followed patterns, respected best practices and reduced the mental overhead of remembering &lt;em&gt;how&lt;/em&gt; &lt;code&gt;AWS Amplify Gen 2&lt;/code&gt; wants things done.&lt;/p&gt;

&lt;p&gt;But what I finally wanted to achieve was not to use prompts, but for the agent to be able to guide me autonomously. &lt;/p&gt;

&lt;p&gt;At this point this was my new question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Why am I loading MCP SOPs upfront for every request, when the agent could just “know” when to use it dinamically?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  👻 From idea to a Kiro power
&lt;/h2&gt;

&lt;p&gt;Instead of treating &lt;code&gt;AWS MCP SOPs&lt;/code&gt; as something external plugged into the agent and loaded upfront, I wanted the agent to &lt;strong&gt;know when activate it and use it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That’s where &lt;strong&gt;Kiro Power&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;Traditional MCP servers are loaded upfront, while &lt;strong&gt;a power enables Dynamic MCP tool loading saving context (and thus tokens!)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Let the agent know how &lt;strong&gt;Amplify Gen 2 actually works&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Encode best practices, workflows, and conventions&lt;/li&gt;
&lt;li&gt;Make those rules automatically available whenever &lt;code&gt;AWS Amplify&lt;/code&gt; is part of the conversation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So every time the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Designs a backend with &lt;code&gt;AWS Amplify&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Modifies an existing &lt;code&gt;AWS Amplify&lt;/code&gt; project&lt;/li&gt;
&lt;li&gt;Generates frontend code for an &lt;code&gt;AWS Amplify&lt;/code&gt; app&lt;/li&gt;
&lt;li&gt;Handles environment setup or deployment via &lt;code&gt;AWS Amplify&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;it does so without loading the &lt;code&gt;AWS MCP Server&lt;/code&gt; upfront but having &lt;code&gt;AWS Amplify Gen 2&lt;/code&gt; &lt;em&gt;in mind&lt;/em&gt; and knowing when &lt;em&gt;activate&lt;/em&gt; the power, without me having to restate the rules every time i need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  📦 What I've build.
&lt;/h2&gt;

&lt;p&gt;I started by following the official instructions to create a Kiro Power, which you can find &lt;a href="https://kiro.dev/docs/powers/create/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That’s when I realized something amusing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is a power to create powers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I installed it and let it guide me in building my own, a personal Kiro Power tailored specifically for &lt;code&gt;AWS Amplify Gen 2&lt;/code&gt;.&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%2F7sz5njhhb5p0g8l2g7pq.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%2F7sz5njhhb5p0g8l2g7pq.png" alt=" " width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From there, it became an iterative process: I reviewed the generated output, tightened the rules, explicitly blocked &lt;code&gt;AWS Amplify Gen 1&lt;/code&gt; related commands, and added behaviors based on my hands-on experience with &lt;code&gt;AWS Amplify Gen 2&lt;/code&gt; in real projects.&lt;/p&gt;

&lt;p&gt;The final repository contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Kiro Power definition focused on &lt;code&gt;AWS Amplify Gen 2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Embedded &lt;code&gt;AWS MCP SOPs&lt;/code&gt; that guide architecture, setup, and evolution&lt;/li&gt;
&lt;li&gt;A structure designed to be reusable and extensible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;It’s not meant to replace documentation but to operationalize it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can find the full implementation and details here:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/davide-desio-eleva/amplify-gen2-kiro-power" rel="noopener noreferrer"&gt;AWS Amplify Gen 2 Kiro Power&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🤝 Contributing
&lt;/h2&gt;

&lt;p&gt;Also I've made a &lt;a href="https://github.com/kirodotdev/powers/pull/43/commits" rel="noopener noreferrer"&gt;PR&lt;/a&gt; to the official &lt;a href="https://github.com/kirodotdev/powers" rel="noopener noreferrer"&gt;kirodotdev/powers&lt;/a&gt; repo hoping this will be merged for all folks out there building with &lt;code&gt;AWS Amplify Gen 2&lt;/code&gt;. You can use also &lt;a href="https://github.com/davide-desio-eleva/powers" rel="noopener noreferrer"&gt;this repo&lt;/a&gt; if you want to try all powers officially available but including mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  👀 See it in action
&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%2Fjrr6wod69xbyp33ot99v.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%2Fjrr6wod69xbyp33ot99v.png" alt=" " width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  After Installation
&lt;/h3&gt;

&lt;p&gt;Once the power is installed, Kiro will show you a confirmation and overview of what the power provides:&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%2Fwk9zkgk0yq5uvmmxl4ul.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%2Fwk9zkgk0yq5uvmmxl4ul.png" alt=" " width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Power Usage Guide
&lt;/h3&gt;

&lt;p&gt;When you ask Kiro for help with Amplify Gen 2, it will propose the available workflows and guide you through the process:&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%2Fvf6klujiiytjeuju9gs7.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%2Fvf6klujiiytjeuju9gs7.png" alt=" " width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;AWS Amplify Gen 2&lt;/code&gt; is powerful, but it also introduces new mental models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backend-first thinking&lt;/li&gt;
&lt;li&gt;Strong conventions&lt;/li&gt;
&lt;li&gt;Opinionated workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are great, until you context-switch, forget a detail, or come back to a project weeks later.&lt;/p&gt;

&lt;p&gt;Also there is still a lot of confusion with &lt;code&gt;AWS Amplify Gen 1&lt;/code&gt; doc and samples (at least for me) and folks migrating from Gen 1 projects can easily feel overwhelmed (I’ve definitely felt that pain!).&lt;br&gt;
&lt;strong&gt;You can test this yourself: ask Kiro to initialize a project without mentioning the generation. It will default to Gen1, or worse, it may switch back and forth between Gen 1 and Gen 2 as you iterate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By encoding Gen 2 guidance via &lt;code&gt;AWS MCP SOPs&lt;/code&gt; into Kiro agent with a power:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You reduce cognitive load&lt;/li&gt;
&lt;li&gt;You avoid subtle mistakes between generation&lt;/li&gt;
&lt;li&gt;You keep architectural decisions consistent over time&lt;/li&gt;
&lt;li&gt;You use best practices&lt;/li&gt;
&lt;li&gt;You use a security first approach&lt;/li&gt;
&lt;li&gt;You don't waste tokens (and money) when you're not speaking about Amplify!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, you let Kiro agent worry about &lt;em&gt;remembering AWS Amplify Gen 2 doc and best practices&lt;/em&gt;, so you can focus on &lt;em&gt;building your app&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🙏 Acknowledgements
&lt;/h2&gt;

&lt;p&gt;This work wouldn’t be the same without thoughtful feedback and sharp reviews.&lt;/p&gt;

&lt;p&gt;A big shout-out to &lt;strong&gt;&lt;a href="https://www.linkedin.com/in/catalinborsan/" rel="noopener noreferrer"&gt;Catalin Borsan&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://www.linkedin.com/in/frabertani/" rel="noopener noreferrer"&gt;Francesco Bertani&lt;/a&gt;&lt;/strong&gt;: their input helped shape this from an experiment into something actually useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  🙋 Who am I
&lt;/h2&gt;

&lt;p&gt;I'm &lt;a href="https://www.linkedin.com/in/desiodavide" rel="noopener noreferrer"&gt;D. De Sio&lt;/a&gt; and I work as a Head of Software Engineering in &lt;a href="https://eleva.it/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;br&gt;
I'm currently (Apr 2025) an &lt;a href="https://www.credly.com/badges/9929fdf2-7a3d-4013-9de6-57c80e4920b9/public_url" rel="noopener noreferrer"&gt;AWS Certified Solution Architect Professional&lt;/a&gt; and &lt;a href="https://www.credly.com/badges/8c5a1487-191b-429e-8c2d-7cee43bf316b/public_url" rel="noopener noreferrer"&gt;AWS Certified DevOps Engineer Professional&lt;/a&gt;, but also a &lt;a href="https://www.linkedin.com/company/aws-user-group-pavia/" rel="noopener noreferrer"&gt;User Group Leader (in Pavia)&lt;/a&gt;, an &lt;strong&gt;AWS Community Builder&lt;/strong&gt; and, last but not least, a #serverless enthusiast.&lt;/p&gt;

&lt;p&gt;My work in this field is to advocate about serverless and help as more dev teams to adopt it, as well as customers break their monolith into API and micro-services using it.&lt;/p&gt;

</description>
      <category>kiro</category>
      <category>aws</category>
      <category>amplify</category>
    </item>
    <item>
      <title>2025 Wrapped: still building, sharing, and finding my place in the community</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Mon, 29 Dec 2025 09:33:57 +0000</pubDate>
      <link>https://dev.to/aws-builders/2025-wrapped-still-building-sharing-and-finding-my-place-in-the-community-3bme</link>
      <guid>https://dev.to/aws-builders/2025-wrapped-still-building-sharing-and-finding-my-place-in-the-community-3bme</guid>
      <description>&lt;p&gt;Every year I try to set myself a simple goal: build things that are useful, write about what I learn, and show up for the community.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This year, I've set a metric: at least 12 meetups as AWS user group leader or member. I've failed it, but that goal turned into something much bigger.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🏃 TL;DR
&lt;/h2&gt;

&lt;p&gt;I tried to measure a year with numbers.&lt;/p&gt;

&lt;p&gt;This is a story about setting goals, missing some of them, and accidentally building something much bigger in the process. It’s about communities that start from zero chairs and end up full of conversations, about writing that turns into thinking, and thinking that only works because someone on the other side is paying attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No grand finale. This is my last article of the year: the only one without code, diagrams, or architectures, but maybe one that mattered most to me.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Starting from the ground up: AWS User Groups
&lt;/h2&gt;

&lt;p&gt;One of the most meaningful challenges this year was founding a new &lt;strong&gt;&lt;a href="https://www.awsugcuneo.it/" rel="noopener noreferrer"&gt;AWS User Group in Cuneo&lt;/a&gt;&lt;/strong&gt;.&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%2Fwzx8fefgu9nhfb8h24f4.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%2Fwzx8fefgu9nhfb8h24f4.png" alt=" " width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Starting a UG from scratch is very different from joining an established one. There’s no audience, no routine, no guarantees. You need to convince people that showing up is worth their time, that there is value in sharing experiences even when things are still rough around the edges. &lt;/p&gt;

&lt;p&gt;Everything has felt easier thanks to &lt;a href="https://www.linkedin.com/in/leonardoviada/" rel="noopener noreferrer"&gt;Leonardo Viada&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/gioeleblanc/" rel="noopener noreferrer"&gt;Gioele Blanc&lt;/a&gt; as partners in crime, but also because people have responded with genuine energy and curiosity.&lt;/p&gt;

&lt;p&gt;A special thanks goes to &lt;a href="https://www.linkedin.com/in/alessandroponzo/" rel="noopener noreferrer"&gt;Alessandro Ponzo&lt;/a&gt;, who acted as our sponsor and mentor, supporting every meetup: not only through his talks, but by actively guiding and nurturing the community.&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%2Fb9u3roi0tm8vf6zfh994.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%2Fb9u3roi0tm8vf6zfh994.png" alt=" " width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Seeing the first meetup come to life, with real conversations, real questions, and real enthusiasm, made all the effort worth it. It confirmed something I strongly believe in: strong communities don’t start with stages or sponsors, they start with trust and curiosity.&lt;/p&gt;

&lt;p&gt;In parallel, &lt;strong&gt;AWS User Group Pavia&lt;/strong&gt; kept being a good playground for experimentation. We started in 2024 and this year we pushed things further with &lt;strong&gt;5 meetups and a developer challenge&lt;/strong&gt;, getting hands-on with &lt;strong&gt;Amazon Q Developer&lt;/strong&gt;, with the huge help of &lt;a href="https://www.linkedin.com/in/catalinborsan/" rel="noopener noreferrer"&gt;Catalin Borsan&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/frabertani/" rel="noopener noreferrer"&gt;Francesco Bertani&lt;/a&gt;, and turning learning into something tangible and fun. Watching people build, compete, and collaborate reminded me why UG formats work so well when they are practical and inclusive. &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%2Fvur7lho7t12gtutmx3nc.jpg" 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%2Fvur7lho7t12gtutmx3nc.jpg" alt=" " width="800" height="998"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We also experimented with new formats, such as the “re:Cap AWS Milan Summit 2025”, conceived as a counterpoint to the traditional “re:Cap re:Invent”. In this format, we explored the key announcements and most relevant moments from Italy’s main AWS-focused event, following the same approach traditionally used in December for AWS’s most important global conference.&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%2Fpa64hbyqw6mbkbjkqnre.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%2Fpa64hbyqw6mbkbjkqnre.png" alt=" " width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, organizing and experimenting felt effortless thanks to the community superheroes from Pavia and beSharp: &lt;a href="https://www.linkedin.com/in/lucaballista/" rel="noopener noreferrer"&gt;Luca Ballista&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/damiano-giorgi/" rel="noopener noreferrer"&gt;Damiano Giorgi&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/a-callegari/" rel="noopener noreferrer"&gt;Antonio Callegari&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;AWS User Groups were the constant thread throughout the year: not just events, but places where ideas are tested before becoming blog posts, talks, or projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  🇮🇹 From local to national: being part of the Italian community’s voice and Community Days
&lt;/h2&gt;

&lt;p&gt;I'll start from this: I had the honor of representing AWS User Group Pavia during the live streaming of the re:Invent CEO Keynote for the AWS UG Italy community.&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%2Fayonp0bj6y7z9wk1x5uj.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%2Fayonp0bj6y7z9wk1x5uj.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being invited was already meaningful.&lt;br&gt;
Meeting such expert people from other cities and AWS User Groups made it even better.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those conversations made one thing clear: there’s a lot of energy moving inside the Italian AWS community, and many shared ideas that could turn into great collaborations in 2026. Let’s just say some of them might involve familiar faces, we’ll see what happens &lt;a href="https://www.linkedin.com/in/andysal/" rel="noopener noreferrer"&gt;Andrea Saltarello&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The same energy was clear to me when Italian AWS community deliver a strong, unified response to &lt;a href="https://www.linkedin.com/in/michal-salanci-%F0%9F%AB%90-3496724b/" rel="noopener noreferrer"&gt;Michal Salanci&lt;/a&gt; 's exciting initiative, AWS Community pre:Invent Warmup, giving participants the chance to win a trip to re:Invent in Las Vegas.&lt;/p&gt;

&lt;p&gt;I personally took the opportunity to amplify the message, helping as many Italian UG as possible get involved as it was a wonderful opportunity for Italian UG members to win a very big prize (going to Vegas)!&lt;/p&gt;

&lt;p&gt;In Italy, August is sacred: everyone disappears on vacation, so I was definitely not expecting much engagement. &lt;strong&gt;Instead, the response was immediate: posts across Italian AWS User Groups, emails through Meetup, and genuine enthusiasm to share this opportunity with everyone. Italy became the national community with the largest representation among AWS UG partners in this initiative&lt;/strong&gt;!&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%2F32pfwuffpigdworfimav.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%2F32pfwuffpigdworfimav.png" alt=" " width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’d like to give a big shout-out and heartfelt thanks to the to AWS UG leaders across cities &lt;a href="https://www.linkedin.com/in/simonemerlini/" rel="noopener noreferrer"&gt;Simone Merlini&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/lucaballista/" rel="noopener noreferrer"&gt;Luca Ballista&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/guidonebiolo/" rel="noopener noreferrer"&gt;Guido Maria Nebiolo&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/leonardoviada/" rel="noopener noreferrer"&gt;Leonardo Viada&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/immacolata-smelzo-97959072/" rel="noopener noreferrer"&gt;Immacolata Smelzo&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/monicacolangelo/" rel="noopener noreferrer"&gt;Monica Colangelo&lt;/a&gt; whose energy made this possible.&lt;/p&gt;

&lt;p&gt;And finally this energy scaled up beautifully at the &lt;strong&gt;AWS Community Day Italy&lt;/strong&gt;: I've been here only to partecipate, just as a member of the community and not as an organizer.&lt;/p&gt;

&lt;p&gt;At this point, it feels to me less like an event and more like a reunion of experts and friends, all coming from AWS initiatives and AWS User Groups. You don’t need external validation anymore, the room itself is proof of how much skill, passion, and experimentation is happening in the Italian cloud community.&lt;/p&gt;

&lt;p&gt;One message that really stuck with me was shared by &lt;a href="https://www.linkedin.com/in/rlosio/" rel="noopener noreferrer"&gt;Renato Losio&lt;/a&gt; and it should probably be an aspiration for anyone working in tech.&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%2Fihk56vw6q5lrpctod6yg.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%2Fihk56vw6q5lrpctod6yg.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 AWS Community Builders: the multiplier effect
&lt;/h2&gt;

&lt;p&gt;Yet this year, something truly special happened.&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%2F6o9gk0fxdzlus39zbolf.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%2F6o9gk0fxdzlus39zbolf.png" alt=" " width="800" height="85"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I officially joined the AWS Community Builders program&lt;/strong&gt;, in the Serverless category.&lt;/p&gt;

&lt;p&gt;And suddenly I feel &lt;strong&gt;more responsibility to give back&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Being part of this program didn’t change what I do day to day. I was already writing, building, and playing with real-world use cases. &lt;strong&gt;What changed was the amplification opportunity.&lt;/strong&gt; supported from people like &lt;a href="https://www.linkedin.com/in/jasonrobertdunn/" rel="noopener noreferrer"&gt;Jason Dunn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This program became the foundation that amplified everything else I've made this year: articles, talks, experiments, and community work.&lt;/p&gt;

&lt;p&gt;You also get rewarded for good content, as happened to me after joining the program and giving me the opportunity to get some cool swag products.&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%2Fif2gsk34fd1vbgwe5z0a.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%2Fif2gsk34fd1vbgwe5z0a.png" alt=" " width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ✍️ When writing starts to echo back
&lt;/h2&gt;

&lt;p&gt;I have always written my articles in English because I consider it the ideal language to integrate seamlessly with the code being explained, and because I believe that impact scales when knowledge crosses borders. This year, amplified by AWS Community Builders community, that choice paid off in unexpected ways.&lt;/p&gt;

&lt;p&gt;Seeing my articles cited in international newsletters felt surreal at first. &lt;/p&gt;

&lt;p&gt;Being featured multiple times by &lt;a href="https://www.linkedin.com/in/allenheltondev/" rel="noopener noreferrer"&gt;Allen Helton&lt;/a&gt; in &lt;a href="https://www.linkedin.com/company/readysetcloud/" rel="noopener noreferrer"&gt;Ready, Set, Cloud&lt;/a&gt;, by &lt;a href="https://www.linkedin.com/in/lee-james-gilmore/" rel="noopener noreferrer"&gt;Lee Gilmore&lt;/a&gt; in multiple issues of &lt;a href="https://www.serverlessadvocate.com/" rel="noopener noreferrer"&gt;Serverless Advocate&lt;/a&gt;, &lt;strong&gt;&lt;a href="https://serverlessadvocate.substack.com/i/168133236/ask-the-expert" rel="noopener noreferrer"&gt;including being selected as a Serverless expert&lt;/a&gt;&lt;/strong&gt;, and by &lt;a href="https://www.linkedin.com/in/jones-zachariah-noel-n/" rel="noopener noreferrer"&gt;Jones Zachariah Noel N&lt;/a&gt; in &lt;a href="https://blog.theserverlessterminal.com/" rel="noopener noreferrer"&gt;Serverless Terminal&lt;/a&gt;, was a huge honor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not because of visibility, but because my work appeared cited and next to people I’ve been learning from for years, who were also genuinely open, approachable, and generous with their time and feedback&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;One of my technical articles was also cited in the &lt;strong&gt;Spanish&lt;/strong&gt; newsletter of &lt;a href="https://www.linkedin.com/in/marciavillalba/" rel="noopener noreferrer"&gt;Marcia Villalba&lt;/a&gt;: &lt;a href="https://desplegando.substack.com/i/175186368/construye-un-agente-serverless-con-contexto-persistente-usando-strands-agents-sdk" rel="noopener noreferrer"&gt;desplegando.cloud&lt;/a&gt;. Something I honestly didn’t see coming. Being referenced in English newsletters or Italian communities already feels meaningful, but seeing my work cited in another language adds an entirely different perspective.&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%2F9x10orzhfayb15p0f5s8.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%2F9x10orzhfayb15p0f5s8.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At some point, even companies shaping the AI space started referencing my projects. &lt;strong&gt;Seeing work around agent memory highlighted by teams like &lt;a href="https://www.linkedin.com/company/mem0/" rel="noopener noreferrer"&gt;Mem0&lt;/a&gt;&lt;/strong&gt;. &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%2Faximp1ur4ppqep5mjg6i.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%2Faximp1ur4ppqep5mjg6i.png" alt=" " width="800" height="966"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you’ve ever been part of a real community, you know that&lt;br&gt;
what you give is never comparable to what you get back.&lt;/strong&gt;&lt;br&gt;
This year, I can assure you, what I’ve received in return far exceeds what I’ve given.&lt;/p&gt;

&lt;p&gt;I have also started published content in Italian, &lt;a href="https://www.tomshw.it/business/perche-il-tuo-progetto-ai-fallira-se-non-gli-dai-il-giusto-contesto" rel="noopener noreferrer"&gt;including a recent article on Tom’s Hardware &lt;/a&gt;. I believe that contributing in my native language is equally important, as it allows me to give back to my local community and make knowledge more accessible.&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%2Fph7ez3d4hywuapdxzyxe.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%2Fph7ez3d4hywuapdxzyxe.png" alt=" " width="800" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 From writing to thinking
&lt;/h2&gt;

&lt;p&gt;I’ve come to realize that this feedback loop exists only because I started writing differently.&lt;/p&gt;

&lt;p&gt;At some point, &lt;strong&gt;I stopped treating blog posts as explanations and started using them as a way to reason about systems and architectures&lt;/strong&gt;. Each article became a place to slow down, question my own assumptions, and test whether an idea could survive contact with reality, with experts and could be shipped to production.&lt;/p&gt;

&lt;p&gt;I've worked hard on &lt;strong&gt;&lt;a href="https://dev.to/aws-builders/deploy-your-first-ai-agent-with-strands-agents-sdk-j85"&gt;Strands Agents SDK series&lt;/a&gt;&lt;/strong&gt; which turn from a simple quick start into a deeper exploration of how agents behave in real environments: adding memory in serverless setups, managing context in stateless architectures, introducing guardrails and designing an agent that don’t collapse outside a demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/aws-builders/deploy-a-minimal-mcp-server-on-aws-lambda-with-serverless-framework-3e42"&gt;MCP series&lt;/a&gt;&lt;/strong&gt; followed the same philosophy. Instead of amplifying hype around MCP, I focused on making it deployable and understandable: minimal MCP servers in serverless on AWS Lambda, different IaC frameworks using Serverless Framework, CDK, and SAM, and eventually a small CLI to help choose the right approach based on actual constraints.&lt;/p&gt;

&lt;p&gt;I wrote also a mini &lt;strong&gt;&lt;a href="http://dev.to/ddesio/rag-on-aws-building-an-ai-powerd-knowledge-base-with-amazon-bedrock-and-pinecone-using-aws-nl1"&gt;RAG on AWS series&lt;/a&gt;&lt;/strong&gt;, starting from a solid way to do it with Pinecone to an experimental one with Amazon S3 Vectors which could be anyway shipped to production to reduce costs.&lt;/p&gt;

&lt;p&gt;The common goal was clarity. Turning abstract concepts into something you can deploy, break, observe, and improve.&lt;/p&gt;

&lt;p&gt;That’s where writing stopped being just teaching or showing off something, and became a tool to think better about architecture and to share that thinking with people who were walking the same path (and give me real feedback).&lt;/p&gt;

&lt;h2&gt;
  
  
  🎤 Conferences as convergence points
&lt;/h2&gt;

&lt;p&gt;For the same reason, conferences this year were not just places to listen to empty stories, but spaces where I can find ideas to test, challenge, and ground in reality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go Serverless&lt;/strong&gt; was probably the clearest expression of this. An event organized together with the Eleva team, it became a stage for real production stories, where teams talked openly about trade-offs, constraints, and decisions. No polished marketing narratives, just architectures that exist because they solve real business problems. &lt;strong&gt;After three editions, our takeaway is simple: serverless is no longer experimental. It’s a deliberate, strategic choice.&lt;/strong&gt;&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%2F0fd14062cwwic8grmfaf.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%2F0fd14062cwwic8grmfaf.png" alt=" " width="800" height="532"&gt;&lt;/a&gt;&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%2F39g97ltholil6f89cywd.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%2F39g97ltholil6f89cywd.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ServerlessDays Milan&lt;/strong&gt; played a different but equally important role. Joining as a speaker, I experienced firsthand how these events sit in a unique space between conferences and meetups. &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%2Fet0p5lhwcned6pb7ppae.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%2Fet0p5lhwcned6pb7ppae.png" alt=" " width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They’re where patterns meet people, and where conversations start immediately after the last slide.&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%2Fxa791tm662r1xs454tlk.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%2Fxa791tm662r1xs454tlk.png" alt=" " width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Topics discussed online suddenly had faces, voices, and follow-up debates. People you read, quote, or learn from turn into peers you can challenge, agree with, or build alongside. That continuity is what turns isolated content into an ecosystem, and a real community.&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%2F75ch5my2bfnz3h29kzd1.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%2F75ch5my2bfnz3h29kzd1.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;AWS Summit in Milan&lt;/strong&gt; tied everything together. Not as a single highlight, but as a confirmation. Seeing the Italian community show up, participate in Game Day challenges powered by Amazon Q Developer, and actively occupy the community spaces was a reminder of how much energy there is when builders are given room to connect. Spending the day with the Eleva team made it even clearer that community is not something parallel to work. It’s part of how good work happens.&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%2F8fi7wlh2wvd7rt2voxxq.jpg" 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%2F8fi7wlh2wvd7rt2voxxq.jpg" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Across all these events, the pattern stayed the same: ideas move faster when people meet, and your work get better when stories are shared where they can be questioned, reused, and improved.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ GenAI workshops: real needs avoiding the hype
&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%2Ftccm1iehs4ukwf1dkb8l.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%2Ftccm1iehs4ukwf1dkb8l.png" alt=" " width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An important part of the year was running GenAI workshops for AWS. I ran quite a few of them this year (at least 10), including one hosted at the AWS office in Milan, with 25 seats filled by people coming from different companies. &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%2F8vp7jsna6jcorhwy264s.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%2F8vp7jsna6jcorhwy264s.png" alt=" " width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've worked on concrete use cases, real business processes, and scenarios where GenAI can deliver tangible and immediate value. Only after understanding why, we move to the how: models, architectures, Amazon Bedrock, security, and data governance.&lt;/p&gt;

&lt;p&gt;The goal was never to sell, it was to understand where GenAI truly makes sense, and how Eleva and AWS can help organizations adopt it responsibly.&lt;/p&gt;

&lt;p&gt;Because in the end, technology is a tool, not the goal.&lt;br&gt;
It exists to solve real problems, not to create hype.&lt;/p&gt;

&lt;h2&gt;
  
  
  📚 Continuous Learning: helping shaping new certifications
&lt;/h2&gt;

&lt;p&gt;Another highlight of 2025 was being invited by &lt;a href="https://www.linkedin.com/in/pamyoungbrown/" rel="noopener noreferrer"&gt;Pamela Brown&lt;/a&gt; as  from AWS to join the beta program shaping the future of hands-on AWS certifications, focused on serverless and agentic AI. Since I hadn’t planned to pursue any certifications this year, it came as a great surprise and an amazing opportunity to challenge myself.&lt;/p&gt;

&lt;p&gt;These new microcredentials aren’t about memorizing services. They’re about applying knowledge: solving real scenarios, building, debugging, and finding working solutions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&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%2Fa97xw347st0bse5k2sdd.png" width="600" height="600"&gt;
&lt;/td&gt;
&lt;td&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%2Fxqwv08n46hrzzq9awrs4.png" width="600" height="600"&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Being part of the beta wasn’t just about taking exams; it was about contributing feedback to how future builders will learn. The self-paced exam labs were a great refresher, but also a strong reminder that &lt;strong&gt;hands-on first is what truly makes skills stick perfectly aligned with the shift I’ve made this year in my articles and writing, focusing on practical, applicable knowledge rather than just theory.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔺 A special note on Eleva
&lt;/h2&gt;

&lt;p&gt;None of this would have been possible without &lt;a href="https://www.linkedin.com/company/eleva-srl/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Not just as a company, but as an environment that genuinely supported this journey day after day. Eleva gave me the space, trust, and encouragement to grow, explore ideas, and invest time in communities, writing, and learning, knowing I was never doing it alone.&lt;/p&gt;

&lt;p&gt;What truly made the difference are the people.&lt;br&gt;
&lt;a href="https://www.linkedin.com/in/lucaformenti/" rel="noopener noreferrer"&gt;Luca&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/claudiabelloli/" rel="noopener noreferrer"&gt;Claudia&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/salvatore-russo-s22/" rel="noopener noreferrer"&gt;Salvatore&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/adriana-rava/" rel="noopener noreferrer"&gt;Adriana&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/lorenzodefilippi/" rel="noopener noreferrer"&gt;Lorenzo&lt;/a&gt; your constant support, openness, and belief in my growth shaped much of what I was able to achieve this year. Having people who care, who listen, and who actively invest in your development changes everything.&lt;/p&gt;

&lt;p&gt;I also want to take a moment to recognize the developers on my team. Much of their work happens quietly, out of sight, but it’s the solid ground that supports every opportunity described here. This year, in agreement with Lorenzo, I took on the role of &lt;strong&gt;Head of Software Engineering&lt;/strong&gt;, which brought new challenges to my table: &lt;strong&gt;guiding others in their technical and professional growth has been both a joy and an honor. I hope I have done a good job for devs in Eleva, fully aware that I can always improve, and determined to give my best in this new role.&lt;/strong&gt;&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%2Fr9fyo1s3m0fxg0egaa9v.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%2Fr9fyo1s3m0fxg0egaa9v.png" alt=" " width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Growth doesn’t happen without the right people around you: thank you for making this year possible.&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%2Fs2oupz59ftnih7bv0bmj.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%2Fs2oupz59ftnih7bv0bmj.png" alt=" " width="800" height="1037"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔄 Closing the loop
&lt;/h2&gt;

&lt;p&gt;Looking back at 2025, I didn’t reach the number of meetups I had in mind.&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%2Fzekmoibbdp2olfxz2mbh.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%2Fzekmoibbdp2olfxz2mbh.png" alt=" " width="800" height="1421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I have done instead was something harder to measure, but far more meaningful.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Over the year, through AWS user groups, articles, workshops, conferences, and long conversations, relationships slowly took shape. Ideas evolved because people engaged with them. Writing changed because others reacted, questioned, and shared their perspectives. &lt;/p&gt;

&lt;p&gt;This year reminded me that growth in tech, and beyond, doesn’t come from isolated effort. It comes from people thinking together, learning together, and trusting the process together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;As 2025 comes to a close, the lesson I carry forward is: keep building with intention, keep writing to understand better, and keep nurturing the communities that make all of this possible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s how the year closes and there could be no better way to begin a new one.&lt;/p&gt;

&lt;h2&gt;
  
  
  🙋 Who am I
&lt;/h2&gt;

&lt;p&gt;I'm &lt;a href="https://www.linkedin.com/in/desiodavide" rel="noopener noreferrer"&gt;D. De Sio&lt;/a&gt; and I work as a Head of Software Engineering in &lt;a href="https://eleva.it/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;br&gt;
I'm currently (Apr 2025) an &lt;a href="https://www.credly.com/badges/9929fdf2-7a3d-4013-9de6-57c80e4920b9/public_url" rel="noopener noreferrer"&gt;AWS Certified Solution Architect Professional&lt;/a&gt; and &lt;a href="https://www.credly.com/badges/8c5a1487-191b-429e-8c2d-7cee43bf316b/public_url" rel="noopener noreferrer"&gt;AWS Certified DevOps Engineer Professional&lt;/a&gt;, but also a &lt;a href="https://www.linkedin.com/company/aws-user-group-pavia/" rel="noopener noreferrer"&gt;User Group Leader (in Pavia)&lt;/a&gt;, an &lt;strong&gt;AWS Community Builder&lt;/strong&gt; and, last but not least, a #serverless enthusiast.&lt;/p&gt;

&lt;p&gt;My work in this field is to advocate about serverless and help as more dev teams to adopt it, as well as customers break their monolith into API and micro-services using it.&lt;/p&gt;

</description>
      <category>community</category>
      <category>leadership</category>
      <category>devjournal</category>
      <category>yearinreview</category>
    </item>
    <item>
      <title>🤖 RAG on AWS: Building an AI-powered Knowledge Base, with Amazon Bedrock and S3 Vectors</title>
      <dc:creator>Davide De Sio</dc:creator>
      <pubDate>Tue, 02 Sep 2025 12:40:16 +0000</pubDate>
      <link>https://dev.to/aws-builders/rag-on-aws-building-an-ai-powered-knowledge-base-with-amazon-bedrock-and-s3-vectors-11kc</link>
      <guid>https://dev.to/aws-builders/rag-on-aws-building-an-ai-powered-knowledge-base-with-amazon-bedrock-and-s3-vectors-11kc</guid>
      <description>&lt;h2&gt;
  
  
  🏃‍♂️ TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS released &lt;code&gt;Amazon S3 Vectors&lt;/code&gt; as &lt;strong&gt;native vector storage inside S3&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Store, index, and query billions of vectors with sub-second latency.&lt;/li&gt;
&lt;li&gt;Up to 90% cheaper than traditional vector DB setups.&lt;/li&gt;
&lt;li&gt;Integrated with Bedrock Knowledge Bases, SageMaker Studio, and OpenSearch out of the box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Still in preview! No CloudFormation/CDK support yet, so it's not ready for core prod systems but a perfect playground for builders who want to experiment with AI-ready storage.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚀 Rethinking how we store and query vectors
&lt;/h2&gt;

&lt;p&gt;If you read the first article in this series, I've explored how to build a RAG pipeline with &lt;code&gt;Amazon Bedrock Knowledge Bases&lt;/code&gt; using &lt;code&gt;Pinecone&lt;/code&gt;. The reasoning was simple: &lt;code&gt;Pinecone&lt;/code&gt; is a vector database designed for AI, natively integrated with &lt;code&gt;Bedrock&lt;/code&gt;, and way more cost-effective than running &lt;code&gt;Amazon OpenSearch&lt;/code&gt; just for embeddings.&lt;/p&gt;

&lt;p&gt;But today, I’d like to talk about something new that could completely change how we think about vector storage: &lt;code&gt;Amazon S3 Vectors&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you’ve been building AI agents, semantic search, or anything that relies on embeddings, you already know the story: vectors are everywhere. But storing, indexing, and querying them at scale?&lt;/p&gt;

&lt;p&gt;That’s usually been a pain: costly, complex, and often involving extra infra you don’t really want to handle.&lt;/p&gt;

&lt;p&gt;That’s where &lt;code&gt;Amazon S3 Vectors&lt;/code&gt; comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 What is actually S3 Vectors?
&lt;/h2&gt;

&lt;p&gt;S3 Vectors is the first cloud object store with native vector support. Basically, &lt;code&gt;Amazon S3&lt;/code&gt; &lt;strong&gt;now has built-in APIs to store, access, and query vectors directly&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Why this is a big deal for builders?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;90% cost savings compared to traditional vector databases (uploading, storing, querying).&lt;/li&gt;
&lt;li&gt;Sub-second query performance, even at massive scale.&lt;/li&gt;
&lt;li&gt;S3 durability and elasticity&lt;/li&gt;
&lt;li&gt;AI-native: purpose built for AI agents, semantic search, and &lt;strong&gt;RAG&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  💡 Build faster with AI-ready storage
&lt;/h2&gt;

&lt;p&gt;What I really love about this new S3 option, is the out of the box integration for &lt;code&gt;Amazon Bedrock Knowledge Bases&lt;/code&gt; (among others) which makes Retrieval Augmented Generation (RAG) way simpler and cheaper.&lt;/p&gt;

&lt;p&gt;A picture is worth a thousand words (&lt;a href="https://aws.amazon.com/it/blogs/aws/introducing-amazon-s3-vectors-first-cloud-storage-with-native-vector-support-at-scale/" rel="noopener noreferrer"&gt;credits to awesome article "Introducting Amazon S3 Vectors&lt;/a&gt;) &lt;br&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%2Fp0s0bh8uotre6iute0s9.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%2Fp0s0bh8uotre6iute0s9.png" alt=" " width="800" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What about &lt;code&gt;Amazon OpenSearch&lt;/code&gt; service or solution as &lt;code&gt;Pinecone&lt;/code&gt;? &lt;br&gt;
You could tier your vector data: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep the “long-term memory” cheap in S3 &lt;/li&gt;
&lt;li&gt;while “short-term memory” hot in &lt;code&gt;Pinecone&lt;/code&gt;/&lt;code&gt;OpenSearch&lt;/code&gt; for fast inference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This combo means you don’t have to choose between cost-efficiency and performance. &lt;strong&gt;You can choose the best in class for your use case.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Create a S3 vectors-powered RAG with Amazon Knowledge Bases
&lt;/h2&gt;

&lt;p&gt;First of all, go to &lt;code&gt;Amazon Knowledge Bases&lt;/code&gt; console, click on &lt;code&gt;create&lt;/code&gt; button then select vector option&lt;br&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%2F8a26a354k9fnlo36dqkd.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%2F8a26a354k9fnlo36dqkd.png" alt=" " width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As second step, give our Knowledge Base a name as we are familiar.&lt;br&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%2Faatxf8w98spvk28nya4t.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%2Faatxf8w98spvk28nya4t.png" alt=" " width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should now select a source, let's go with &lt;code&gt;standard S3 object storage&lt;/code&gt;. We'll store some &lt;code&gt;csv&lt;/code&gt; files here as document source for our RAG.&lt;br&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%2Fplvmil4q2s1jlf05wrfg.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%2Fplvmil4q2s1jlf05wrfg.png" alt=" " width="800" height="402"&gt;&lt;/a&gt;&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%2Fqg50a6nbt955crsat26j.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%2Fqg50a6nbt955crsat26j.png" alt=" " width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should now create a &lt;code&gt;S3 vector store&lt;/code&gt; &lt;br&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%2F1uni9agtiuurnbpt4v5a.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%2F1uni9agtiuurnbpt4v5a.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;or select a previously created &lt;code&gt;S3 vector store&lt;/code&gt;&lt;br&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%2Fodrtr0c21qptii3a3lzf.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%2Fodrtr0c21qptii3a3lzf.png" alt=" " width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, just review your selections and create an &lt;code&gt;Amazon Bedrock Knowledge Base&lt;/code&gt;&lt;br&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%2F04uyyjs62ko655vl4w00.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%2F04uyyjs62ko655vl4w00.png" alt=" " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the section of &lt;code&gt;S3 Vector Store&lt;/code&gt;&lt;br&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%2F0lo0rqtffkoc967yu0ih.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%2F0lo0rqtffkoc967yu0ih.png" alt=" " width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🧪 Test it out
&lt;/h2&gt;

&lt;p&gt;You can simply test your RAG powered by your newly created vector store. Let's start uploading some file to your source standard &lt;code&gt;S3 bucket&lt;/code&gt;.&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%2F9o66gq0787h8dnfkx5px.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%2F9o66gq0787h8dnfkx5px.png" alt=" " width="800" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then sync your &lt;code&gt;Amazon Bedrock Knowledge Base&lt;/code&gt; and try some relevant question for your data: as I've added big mac cost around the world and Tokyo Olympics medal results, I'm asking some simple question about it.&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%2F0cwoxqw2a0cf0553iyxg.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%2F0cwoxqw2a0cf0553iyxg.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can easily review details of retrieved data in the test panel&lt;br&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%2Fhw3bvqsl17evqbn8b8bc.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%2Fhw3bvqsl17evqbn8b8bc.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚠️ Things to keep in mind
&lt;/h2&gt;

&lt;p&gt;As exciting as &lt;code&gt;S3 Vectors&lt;/code&gt; is, &lt;strong&gt;it’s still in preview&lt;/strong&gt;. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not production-ready (yet)&lt;/strong&gt;: it’s awesome for experiments, prototyping, and side projects, but I wouldn’t bet the core of a production system on it right now. Expect some rough edges and possible &lt;strong&gt;changes in APIs or behavior before GA&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No CloudFormation/CDK integration (yet)&lt;/strong&gt;: this is a big one. Right now, you can’t just spin up &lt;code&gt;S3 Vectors&lt;/code&gt; resources via Infrastructure as Code (IaC). For builders who rely on repeatable, automated deployments, that’s a blocker for serious production adoption. Once CloudFormation and CDK support land, that’s when I think we’ll see this become a &lt;strong&gt;mainstream building block in real world AI projects&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📌 Final thoughts
&lt;/h2&gt;

&lt;p&gt;If you’re a builder, now’s the perfect time to experiment and get familiar with &lt;code&gt;S3 Vectors&lt;/code&gt;. But if you’re running a mission critical app, &lt;strong&gt;you should treat it as a preview: learn it, play with it, and be ready to adopt when the full production tooling support arrives.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For me, this feels like one of those “AWS building block” that changes the game and I’m already thinking about how to re-architect some of my RAGs to cut costs and simplify cloud infrastructure.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🌐 Resources
&lt;/h2&gt;

&lt;p&gt;You can find some useful resources about S3 vector &lt;a href="https://aws.amazon.com/it/s3/features/vectors/" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://aws.amazon.com/it/blogs/aws/introducing-amazon-s3-vectors-first-cloud-storage-with-native-vector-support-at-scale/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Moreover, you can find &lt;a href="https://github.com/awslabs/s3vectors-embed-cli" rel="noopener noreferrer"&gt;here&lt;/a&gt; a useful cli to interact directly with your S3 vector: don’t miss the API to query with metadata as it’s super handy!&lt;/p&gt;

&lt;h2&gt;
  
  
  🙋 Who am I
&lt;/h2&gt;

&lt;p&gt;I'm &lt;a href="https://www.linkedin.com/in/desiodavide" rel="noopener noreferrer"&gt;D. De Sio&lt;/a&gt; and I work as a Head of Software Engineering in &lt;a href="https://eleva.it/" rel="noopener noreferrer"&gt;Eleva&lt;/a&gt;.&lt;br&gt;
I'm currently (Apr 2025) an &lt;a href="https://www.credly.com/badges/9929fdf2-7a3d-4013-9de6-57c80e4920b9/public_url" rel="noopener noreferrer"&gt;AWS Certified Solution Architect Professional&lt;/a&gt; and &lt;a href="https://www.credly.com/badges/8c5a1487-191b-429e-8c2d-7cee43bf316b/public_url" rel="noopener noreferrer"&gt;AWS Certified DevOps Engineer Professional&lt;/a&gt;, but also a &lt;a href="https://www.linkedin.com/company/aws-user-group-pavia/" rel="noopener noreferrer"&gt;User Group Leader (in Pavia)&lt;/a&gt;, an &lt;strong&gt;AWS Community Builder&lt;/strong&gt; and, last but not least, a #serverless enthusiast.&lt;/p&gt;

&lt;p&gt;My work in this field is to advocate about serverless and help as more dev teams to adopt it, as well as customers break their monolith into API and micro-services using it.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>aws</category>
      <category>s3</category>
      <category>serverless</category>
    </item>
  </channel>
</rss>
