<?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: Vadym Kazulkin</title>
    <description>The latest articles on DEV Community by Vadym Kazulkin (@vkazulkin).</description>
    <link>https://dev.to/vkazulkin</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%2F501061%2Ffa23b5ee-9c5f-48bd-b3bb-341a26a773c6.JPG</url>
      <title>DEV Community: Vadym Kazulkin</title>
      <link>https://dev.to/vkazulkin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vkazulkin"/>
    <language>en</language>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 5 Map and child context</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-3kmg</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-3kmg</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1k6e"&gt;part 2&lt;/a&gt; of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to create and execute durable steps synchronously and asynchronously.  Later, in parts &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-oo4"&gt;3&lt;/a&gt; and &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1f1n"&gt;4&lt;/a&gt; of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to implement wait for callback and wait for condition. &lt;/p&gt;

&lt;p&gt;In this part, we'll explore map and child context operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample application with map and child context
&lt;/h2&gt;

&lt;p&gt;Let's explore the &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java/blob/main/docs/core/child-contexts.md" rel="noopener noreferrer"&gt;child context&lt;/a&gt; operation first.  Child contexts run an isolated stream of work with their own operation counter and checkpoint log. They support the full range of durable operations — step, wait, invoke, createCallback, and nested child contexts.  In our sample application, we'll add a &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java/blob/main/docs/core/map.md" rel="noopener noreferrer"&gt;map&lt;/a&gt; operation, which uses a child context underneath. We get access to it through &lt;em&gt;DurableContext&lt;/em&gt;. A &lt;em&gt;Map&lt;/em&gt; operation applies a function to each item in a collection concurrently, with each item running in its own child context.&lt;/p&gt;

&lt;p&gt;Now let's add an artificial use case: for each YouTube video found, we add it to the author's YouTube playlist. I first copied the application that we created in &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-java-25" rel="noopener noreferrer"&gt;part 2&lt;/a&gt; into the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-with-map-and-child-context-java-25" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-with-map-and-child-context-java-25&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;The main business logic we have to add is in the &lt;em&gt;saveYouTubeVideosToPlayList&lt;/em&gt; method of the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-map-and-child-context-java-25/src/main/java/dev/vkazulkin/handler/AbstractAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AbstractAuthorContentExtractor&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;saveYouTubeVideosToPlayList&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DurableContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;YouTubeVideos&lt;/span&gt; &lt;span class="n"&gt;youtubeVideos&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="o"&gt;{&lt;/span&gt;
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;youtubePlayList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;findOrCreateYouTubePlayListForTheAuthor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mapConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MapConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nestingType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NestingType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NESTED&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxConcurrency&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                                   
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completionConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CompletionConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;allCompleted&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;    
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mapResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"saveYouTubeVideoToPlayList-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;youtubeVideos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;youtubeVideos&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; 
      &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;youtubeVideo&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;childCtx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;       
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;childCtx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"saveYouTubeVideoToPlayList-step-"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
       &lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;saveYouTubeVideoToPlayList&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;youtubeVideo&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;youtubePlayList&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStepConfig&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;},&lt;/span&gt; &lt;span class="n"&gt;mapConfig&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;


&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;findOrCreateYouTubePlayListForTheAuthor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
     &lt;span class="c1"&gt;// Use YouTube API to log in to the YouTube Account and search for the author's playlist&lt;/span&gt;
     &lt;span class="c1"&gt;// and return it if it already exists or create a new one&lt;/span&gt;
     &lt;span class="c1"&gt;// move this logic into a separate Lambda function and invoke it as a durable step&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="nf"&gt;saveYouTubeVideoToPlayList&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;YouTubeVideo&lt;/span&gt; &lt;span class="n"&gt;youtubeVideo&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;playListId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//Use YouTube API to save the video URL to the playlist. &lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We invoke this &lt;em&gt;saveYouTubeVideosToPlayList&lt;/em&gt; method in the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-map-and-child-context-java-25/src/main/java/dev/vkazulkin/handler/AuthorContentExtractor.java" rel="noopener noreferrer"&gt;AuthorContentExtractor&lt;/a&gt; or &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-map-and-child-context-java-25/src/main/java/dev/vkazulkin/handler/AsyncAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AsyncAuthorContentExtractor&lt;/a&gt; classes.&lt;/p&gt;

&lt;p&gt;First of all, I won't use the YouTube API to create or retrieve the YouTube playlist for the given author and store each found video there. I use some static output, as the goal of this article is to demonstrate the functionality of the &lt;em&gt;map&lt;/em&gt; operation. &lt;/p&gt;

&lt;p&gt;Let's go step by step through this code. &lt;em&gt;Map&lt;/em&gt; applies a function to each item in a collection concurrently, with each item running in its own child context. Results are collected into a &lt;em&gt;MapResult&lt;/em&gt; that maintains input order. Each item's function receives its own DurableContext, so you can use any durable operation (step(), wait(), invoke(), etc.) inside the map function.  The function passed to map() is a &lt;em&gt;MapFunction&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@FunctionalInterface&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;MapFunction&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;I&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;O&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="no"&gt;O&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;I&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;DurableContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The index parameter is the zero-based position of the item in the input collection, useful for naming operations or correlating results. For each found YouTube video, we invoke the &lt;em&gt;saveYouTubeVideoToPlayList&lt;/em&gt; method, which returns the result of type &lt;em&gt;Boolean&lt;/em&gt;. In our case, it is always true, indicating that the video was stored in the playlist. &lt;/p&gt;

&lt;p&gt;The input collection must have deterministic iteration order. List, LinkedList, and TreeSet are accepted. HashSet and unordered map views are not, leading to the IllegalArgumentException. That's why I changed the implementation of the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-map-and-child-context-java-25/src/main/java/dev/vkazulkin/entity/YouTubeVideos.java" rel="noopener noreferrer"&gt;YouTubeVideos&lt;/a&gt; class to use List instead of Set there.&lt;/p&gt;

&lt;p&gt;Next, let's explore &lt;em&gt;MapConfig&lt;/em&gt; in more detail. There are several main settings there:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;maxConcurrency&lt;/strong&gt; controls how many items execute concurrently. When set, items beyond the limit are queued and started as earlier items complete. The default is null (unlimited).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nestingType&lt;/strong&gt;, with &lt;strong&gt;Nested&lt;/strong&gt; as the default. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested&lt;/strong&gt;. Create CONTEXT operations for each branch/iteration with full checkpointing. Operations within each branch/iteration are wrapped in their own context. &lt;strong&gt;Observability&lt;/strong&gt;: High - each branch/iteration appears as a separate operation in execution history. &lt;strong&gt;Cost&lt;/strong&gt;: Higher - consumes more operations due to CONTEXT creation overhead. &lt;strong&gt;Scale&lt;/strong&gt;: Lower maximum iterations due to operation limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flat&lt;/strong&gt;.  Skip CONTEXT operations for branches/iterations using virtual contexts. Operations execute directly without individual context wrapping. &lt;strong&gt;Observability&lt;/strong&gt;: Lower - branches/iterations don't appear as separate operations. &lt;strong&gt;Cost&lt;/strong&gt;: ~30% lower - reduces operation consumption by skipping CONTEXT overhead. &lt;strong&gt;Scale&lt;/strong&gt;: Higher maximum iterations possible within operation limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CompletionConfig&lt;/strong&gt; controls when the map operation stops starting new items. Here, there are many possibilities from which we can choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;allCompleted&lt;/strong&gt; (we use it). All items run regardless of failures. Failures are captured per-item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;allSuccessful&lt;/strong&gt;. All items must succeed. Zero failures tolerated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;firstSuccessful&lt;/strong&gt;. Complete as soon as the first item succeeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;minSuccessful&lt;/strong&gt;. Complete when the specified number of items have succeeded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;toleratedFailureCount&lt;/strong&gt;. Complete when more than the specified number of failures have occurred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;toleratedFailurePercentage&lt;/strong&gt;. Complete when the failure percentage exceeds the specified threshold (0.0 to 1.0).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When early termination is triggered, items that were never started have SKIPPED status with null for both result and error in the MapResult.&lt;/p&gt;

&lt;p&gt;Each &lt;em&gt;MapResultItem&lt;/em&gt; contains the following methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;status&lt;/strong&gt;: SUCCEEDED, FAILED, or SKIPPED.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;result&lt;/strong&gt;:  the result value, or null if failed/skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;error&lt;/strong&gt;: the error details as MapError, or null if succeeded/skipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Failed items store error details as &lt;em&gt;MapError&lt;/em&gt;, a serializable record that survives checkpoint-and-replay cycles with the following methods that can be invoked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;errorType&lt;/strong&gt;:  Fully qualified exception class name (e.g., java.lang.RuntimeException).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;errorMessage&lt;/strong&gt;: The exception message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stackTrace&lt;/strong&gt;:  Stack trace frames as a list of strings, or null.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One item's failure does not prevent other items from completing. &lt;em&gt;MapResult&lt;/em&gt; captures the failed items at their corresponding index.&lt;/p&gt;

&lt;p&gt;Map operations are fully durable. On replay after interruption:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Completed items return cached results without re-execution.&lt;/li&gt;
&lt;li&gt;Incomplete items resume from their last checkpoint.&lt;/li&gt;
&lt;li&gt;Items that never started execute fresh.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small results (&amp;lt; 256KB) are checkpointed directly, whereas large results are reconstructed from individual child context checkpoints on replay.&lt;/p&gt;

&lt;p&gt;That's it. There are no changes in the Infrastructure as Code part that we need to make. Now we can build and package our application with &lt;em&gt;mvn clean package&lt;/em&gt; and deploy it with &lt;em&gt;sam deploy&lt;/em&gt;.  The deployment process can take up to 10 minutes because of the creation and mounting of S3 Files.&lt;/p&gt;

&lt;p&gt;To test our Lambda durable function, we can navigate to the Lambda service, search for the &lt;em&gt;AuthorContentExtractor&lt;/em&gt; or &lt;em&gt;AsyncAuthorContentExtractor&lt;/em&gt; function, and go to the "Test" tab.&lt;/p&gt;

&lt;p&gt;We need to pass the following sample JSON Event to it, which represents the author:&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;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vadym"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kazulkin"&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;Then we can test it. After that, we go to the "Durable execution" tab and can see all the execution details:&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%2Fsj1489ohgljy9lud4gsq.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%2Fsj1489ohgljy9lud4gsq.png" alt=" " width="799" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we see that the operation with the name &lt;em&gt;saveYouTubeVideoToPlayList-step&lt;/em&gt; and the subtype &lt;em&gt;Map&lt;/em&gt;. We also see 2 iterations (for each of the 2 YouTube videos found) with the subtype &lt;em&gt;MapIteration&lt;/em&gt;. Each iteration is executed as an individual durable step.&lt;/p&gt;

&lt;p&gt;Let's also look at event history:&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%2Fjtqhquzb7wlhtd03mazx.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%2Fjtqhquzb7wlhtd03mazx.png" alt=" " width="800" height="579"&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%2Fnpzsq3fnnwjw04xhj8jk.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%2Fnpzsq3fnnwjw04xhj8jk.png" alt=" " width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we see all events (durable operations) in the right order required for the execution of our Lambda durable function.&lt;/p&gt;

&lt;p&gt;There is also the &lt;em&gt;mapAsync&lt;/em&gt; operation. It starts the map operation without blocking, returning a &lt;em&gt;DurableFuture&lt;/em&gt;.&lt;/p&gt;

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

&lt;p&gt;In this part of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to implement map and child context operations.  &lt;/p&gt;

&lt;p&gt;Still, our Lambda durable function itself contains too much business logic. Ideally, it should simply be the orchestrator and contain as little business logic as possible. That's why, in the next part, we'll move the logic for each step into a separate Lambda function. With that, we'll explore how to invoke another Lambda function within the durable step. We'll also show how to invoke multiple Lambda functions in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>s3files</category>
      <category>lambdadurablefunctions</category>
    </item>
    <item>
      <title>Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore - Part 4 Upgrade sample application to Embabel 1.5</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 07 Sep 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-4-upgrade-sample-e4o</link>
      <guid>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-4-upgrade-sample-e4o</guid>
      <description>&lt;h2&gt;
  
  
  Upgrade sample application to Embabel 1.5 to support Spring Boot 4.x and Spring AI 2.x
&lt;/h2&gt;

&lt;p&gt;In parts &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb"&gt;2&lt;/a&gt; and &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-agents-as-2d94"&gt;3&lt;/a&gt;, we developed our &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/embabel-1.x-conference-app-agent-local" rel="noopener noreferrer"&gt;sample application&lt;/a&gt; and interacted with it via the shell and HTTP. We used Embabel version 1.0 there, which targets Spring Boot 3.x / Spring AI 1.x.&lt;/p&gt;

&lt;p&gt;Recently, the Embabel team released &lt;a href="https://github.com/embabel/embabel-agent/releases/tag/v1.5.0" rel="noopener noreferrer"&gt;version 1.5&lt;/a&gt;, which supports Spring Boot 4.x / Spring AI 2.x.  In this article, we'll explain how to migrate our sample application to the Embabel 1.5.x version. You can find the final version of the application in &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/embabel-1.5.x-conference-app-agent-local" rel="noopener noreferrer"&gt;my GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The migration for my use case is really simple. First, in &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.5.x-conference-app-agent-local/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt; we need to declare this Embabel version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;java.version&amp;gt;&lt;/span&gt;25&lt;span class="nt"&gt;&amp;lt;/java.version&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;embabel-agent.version&amp;gt;&lt;/span&gt;1.5.1&lt;span class="nt"&gt;&amp;lt;/embabel-agent.version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please note that we use version 1.5.1 here. With version 1.5.0, I ran into a &lt;a href="https://github.com/embabel/embabel-agent/issues/1923" rel="noopener noreferrer"&gt;specific problem&lt;/a&gt; while using the Amazon Bedrock Converse API. This was due to the fact that BedrockOptionsConverter created generic ToolCallingChatOptions. Spring AI 2.0’s BedrockProxyChatModel expects BedrockChatOptions and casts the request options accordingly, causing:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ClassCastException: DefaultToolCallingChatOptions cannot be cast to BedrockChatOptions&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With version 1.5.1, the Embabel team fixed the problem. Next, as Spring Boot 4.x / Spring AI 2.x uses &lt;a href="https://spring.io/blog/2025/10/07/introducing-jackson-3-support-in-spring" rel="noopener noreferrer"&gt;Jackson version 3 by default&lt;/a&gt;, we need to declare a Jackson 3 mapper Spring bean. We do it in the Spring Boot main &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.5.x-conference-app-agent-local/src/main/java/dev/vkazulkin/EmbabelConferenceApplication.java" rel="noopener noreferrer"&gt;EmbabelConferenceApplication&lt;/a&gt; application class of our sample application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;JsonMapper&lt;/span&gt; &lt;span class="nf"&gt;objectMapper&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jackson&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;databind&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;JsonMapper&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Now you can compile, start, and interact with our sample application the same way we described in parts &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb"&gt;2&lt;/a&gt; and &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-agents-as-2d94"&gt;3&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springai</category>
      <category>embabel</category>
      <category>bedrockagentcore</category>
    </item>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 4 Wait for condition</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 31 Aug 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1f1n</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1f1n</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1k6e"&gt;part 2&lt;/a&gt; of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to create and execute durable steps synchronously and asynchronously. &lt;br&gt;
Later, in &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-oo4"&gt;part 3&lt;/a&gt; of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to implement wait and callback. &lt;/p&gt;

&lt;p&gt;In this part, we'll explore the &lt;em&gt;wait for condition&lt;/em&gt; operation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sample application with wait for condition
&lt;/h2&gt;

&lt;p&gt;Let's explore the "wait for condition" operation. We get access to it through &lt;em&gt;DurableContext&lt;/em&gt;.  It repeatedly calls a check function until it signals done. Between polls, the Lambda suspends without consuming compute. State is checkpointed after each check, so progress survives interruptions.&lt;/p&gt;

&lt;p&gt;I first copied the application that we created in &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25" rel="noopener noreferrer"&gt;part 3&lt;/a&gt; into the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-with-wait-for-condition-java-25" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-with-wait-for-condition-java-25&lt;/a&gt; repository. The only change we had to make was to reimplement the &lt;em&gt;waitForUpcomingTalksApproval&lt;/em&gt; method of the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-wait-for-condition-java-25/src/main/java/dev/vkazulkin/handler/AbstractAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AbstractAuthorContentExtractor&lt;/a&gt; class.  Here we'll now use the "wait for condition" instead of the "wait for callback" operation.&lt;/p&gt;

&lt;p&gt;This is how the code now looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt; &lt;span class="nf"&gt;waitForUpcomingTalksApproval&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DurableContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WaitForConditionConfig&lt;/span&gt;&lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;                
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitStrategy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WaitStrategies&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultStrategy&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialState&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;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
         &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PENDING&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitForCondition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
     &lt;span class="s"&gt;"wait-for-approval"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
     &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
         &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentStatus&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepCtx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
         &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
         &lt;span class="k"&gt;return&lt;/span&gt;  
         &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPROVED&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;                     
         &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;WaitForConditionResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stopPolling&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;WaitForConditionResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;continuePolling&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt; &lt;span class="nf"&gt;getUpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
     &lt;span class="c1"&gt;// Generate random integers in range 0 to 3&lt;/span&gt;
     &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RANDOM&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; 
       &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
          &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NOT_APPROVED&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
       &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
          &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPROVED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go step by step through this code. The &lt;em&gt;ctx.waitForCondition&lt;/em&gt; invocation receives the current state and a StepContext, and returns a &lt;em&gt;WaitForConditionResult&lt;/em&gt;. WaitForConditionResult contains the result type, in our case &lt;em&gt;UpcomingTalkApprovalStatus&lt;/em&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;WaitForConditionResult.stopPolling(value)&lt;/em&gt;  - condition met (in our case, status "APPROVED"), return value as the final result.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;WaitForConditionResult.continuePolling(value)&lt;/em&gt; - keep polling (bacause the status is "NOT_APPROVED"), pass value to the next check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the sake of simplicity, we haven't implemented the proper endpoint to poll the status. We provided a simple implementation in the &lt;em&gt;getUpcomingTalkApprovalStatus&lt;/em&gt; method, which generates a random integer between 0 and 3. If 3 is generated, the "APPROVED" status will be returned, indicating that the condition is met and the polling should stop. If 0, 1, or 2 are generated, the operation will keep polling.&lt;/p&gt;

&lt;p&gt;Next, let's look at how to configure &lt;em&gt;WaitForConditionConfig&lt;/em&gt;. First, we pass the &lt;em&gt;initialState&lt;/em&gt; with the status "PENDING," and this status is passed to the first check invocation. After that, we also configure the &lt;em&gt;waitStrategy&lt;/em&gt;. The wait strategy controls the delay between polls. By default, &lt;em&gt;waitForCondition&lt;/em&gt; uses an exponential backoff wait strategy (60 max attempts, 5s initial delay, 300s max delay, 1.5x backoff rate, FULL jitter). We can invoke the &lt;em&gt;WaitStrategies.exponentialBackoff&lt;/em&gt; method to configure exponential backoff with different settings than the default ones. Alternatively, we can use the &lt;em&gt;WaitStrategies.fixedDelay&lt;/em&gt; method. With this wait strategy, we need to configure: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;fixedDelay&lt;/em&gt; - a constant delay between polls and &lt;/li&gt;
&lt;li&gt;
&lt;em&gt;maxAttempts&lt;/em&gt; - a maximum number of attempts before throwing &lt;em&gt;WaitForConditionFailedException&lt;/em&gt;. This exception is thrown when the maximum attempts are exceeded (thrown by the wait strategy).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can also implement &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java/blob/main/docs/core/wait-for-condition.md#custom-wait-strategies" rel="noopener noreferrer"&gt;Custom Wait Strategies&lt;/a&gt;, which we won't cover in this article.&lt;/p&gt;

&lt;p&gt;That's it. There are no changes in the Infrastructure as Code part that we need to make. Now we can build and package our application with &lt;em&gt;mvn clean package&lt;/em&gt; and deploy it with &lt;em&gt;sam deploy&lt;/em&gt;.  The deployment process can take up to 10 minutes because of the creation and mounting of S3 Files.&lt;/p&gt;

&lt;p&gt;To test our Lambda durable function, we can navigate to the Lambda service, search for the &lt;em&gt;AuthorContentExtractor&lt;/em&gt; or &lt;em&gt;AsyncAuthorContentExtractor&lt;/em&gt; function, and go to the "Test" tab.&lt;/p&gt;

&lt;p&gt;We need to pass the following sample JSON Event to it, which represents the author:&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;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vadym"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kazulkin"&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;Then we can test it. After that, we go to the "Durable execution" tab and can see all the execution details:&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%2Fuiupzpbq96rme4wd1bo8.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%2Fuiupzpbq96rme4wd1bo8.png" alt=" " width="799" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we see that the operation with the name &lt;em&gt;wait-for-approval&lt;/em&gt; and the subtype &lt;em&gt;WaitForCondition&lt;/em&gt; succeeded after 3 attempts. The number of retries is 2. This means that it took 3 attempts to generate the integer 3. We defined the generation of this value as the condition to stop polling and return the result.&lt;/p&gt;

&lt;p&gt;Let's also look at event history:&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%2F8agkrjthysnm8iwskq3h.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%2F8agkrjthysnm8iwskq3h.png" alt=" " width="800" height="571"&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%2Fl5wlzye6qcaximoo9e6k.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%2Fl5wlzye6qcaximoo9e6k.png" alt=" " width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we see all events (durable operations) in the right order required for the execution of our Lambda durable function.&lt;/p&gt;

&lt;p&gt;There is also the &lt;em&gt;waitForConditionAsync&lt;/em&gt;  operation for non-blocking polling that you can explore on your own. It starts polling but returns a &lt;em&gt;DurableFuture&lt;/em&gt; immediately, allowing other operations to run concurrently.&lt;/p&gt;

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

&lt;p&gt;In this part of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to implement the &lt;em&gt;wait for condition&lt;/em&gt; operation.  &lt;/p&gt;

&lt;p&gt;In the next part, we'll explore map and child context operations. Still, our Lambda durable function itself contains too much business logic. Ideally, it should simply be the orchestrator and contain as little business logic as possible. That's why, in the subsequent parts, we'll move the logic for each step into a separate Lambda function. With that, we'll explore how to invoke another Lambda function within the durable step. We'll also show how to invoke multiple Lambda functions in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambdadurablefunctions</category>
      <category>s3files</category>
    </item>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 3 Waits and callbacks</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 24 Aug 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-oo4</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-oo4</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1k6e"&gt;part 2&lt;/a&gt; of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to create and execute durable steps synchronously and asynchronously. In this part, we'll extend our application by adding and implementing &lt;em&gt;wait&lt;/em&gt; and &lt;em&gt;callback&lt;/em&gt; operations to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample application with wait for callback
&lt;/h2&gt;

&lt;p&gt;Let's explore waits and callbacks. We get access to them through &lt;em&gt;DurableContext&lt;/em&gt;. Waits are planned pauses where your function stops running and stops charging until it's time to continue. Use them for time periods, external callbacks, or polling for a condition. Callbacks suspend execution until an external system sends a result. Use this for human approvals, webhooks, or any event-driven workflow. In our sample application, we'll combine both and use &lt;em&gt;waitForCallback&lt;/em&gt;. It simplifies callback handling by combining callback creation and submission in one operation.&lt;/p&gt;

&lt;p&gt;I first copied the application that we created in &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1k6e"&gt;part 2&lt;/a&gt; into the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25&lt;/a&gt; repository. We'll work on it to extend our application. Please go through the content of part 2 to understand the sample application and the basic concepts of Lambda durable functions. We'll completely reuse the Infrastructure as Code part as well.&lt;/p&gt;

&lt;p&gt;Let's add another, a bit artificial use case to our sample application. After searching for the author's YouTube videos and upcoming talks, the author should confirm those talks. For this, let's extend our &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25/src/main/java/dev/vkazulkin/handler/AsyncAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AsyncAuthorContentExtractor&lt;/a&gt; Lambda function and add the following invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;upcomingTalkApprovalStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitForUpcomingTalksApproval&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let's look at the implementation of the &lt;em&gt;waitForUpcomingTalksApproval&lt;/em&gt; method in the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25/src/main/java/dev/vkazulkin/handler/AbstractAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AbstractAuthorContentExtractor&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt; &lt;span class="nf"&gt;waitForUpcomingTalksApproval&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DurableContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;waitCallbackConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WaitForCallbackConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
           &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;callbackConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CallbackConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
           &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofHours&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
           &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitForCallback&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"wait-for-approval"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
             &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
             &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callbackId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepCtx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; 
              &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendApprovalRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callbackId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
             &lt;span class="n"&gt;waitCallbackConfig&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendApprovalRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;callbackId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="no"&gt;LOGGER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"get approval for the talk of  "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" for the talks "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;upcomingTalks&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; 
       &lt;span class="s"&gt;" with the callback id "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;callbackId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's explain step by step what is happening here. We use &lt;em&gt;DurableContext&lt;/em&gt; to create the &lt;em&gt;waitForCallback&lt;/em&gt; operation. We give it a name and the return type. This return type corresponds to the type of the object that will be passed in the callback. We also provide the submitter function &lt;em&gt;sendApprovalRequest&lt;/em&gt;, receiving the callback ID and a &lt;em&gt;StepContext&lt;/em&gt;. In this function, we can perform the desired operation, like sending the talk approval request to the author. We won't implement this functionality but only log the callback ID, which we'll require later to send the callback. We can optionally pass the &lt;em&gt;WaitForCallbackConfig&lt;/em&gt;. In our case, we only set the &lt;em&gt;CallbackConfig&lt;/em&gt; with a timeout of 1 hour. This means that if no callback arrives within this period of time, the Lambda durable function will fail. We can also set the &lt;em&gt;StepConfig&lt;/em&gt;. Please review part 2 for more information. &lt;/p&gt;

&lt;p&gt;When we invoke the &lt;em&gt;waitForCallback&lt;/em&gt; method, the invocation will block until the callback arrives. After its arrival, the payload of the callback is deserialized into the object of type &lt;em&gt;UpcomingTalkApprovalStatus&lt;/em&gt;, and this object will be returned.&lt;/p&gt;

&lt;p&gt;Now we can build and package our application with &lt;em&gt;mvn clean package&lt;/em&gt; and deploy it with &lt;em&gt;sam deploy&lt;/em&gt;.  The deployment process can take up to 10 minutes because of the creation and mounting of S3 Files.&lt;/p&gt;

&lt;p&gt;To test our Lambda durable function, we can navigate to the Lambda service, search for the &lt;em&gt;AsyncAuthorContentExtractor&lt;/em&gt; function, and go to the "Test" tab.&lt;/p&gt;

&lt;p&gt;We need to pass the following sample JSON Event to it, which represents the author:&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;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vadym"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kazulkin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bla@bla.de"&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;Then we can test it. After that, we go to the "Durable execution" tab and can see all the execution details:&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%2Fvj1tfu7l7qxabcz7yw8z.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%2Fvj1tfu7l7qxabcz7yw8z.png" alt=" " width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What we see here is that the first 2 steps (searching for YouTube videos and upcoming talks) have already been executed asynchronously. Now, our Lambda function is in the "wait for the approval" state. The invocation of the Lambda durable function is now completed. Why? Because waits suspend our function without incurring charges. &lt;/p&gt;

&lt;p&gt;Now let's send the reply (callback) to our Lambda durable function. We first need to grab the callback ID from the log. There are several ways to send the callback (success or failure). We can use the Lambda CLI &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/lambda/send-durable-execution-callback-success.html" rel="noopener noreferrer"&gt;send-durable-execution-callback-success&lt;/a&gt; or &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/lambda/send-durable-execution-callback-failure.html" rel="noopener noreferrer"&gt;execution-callback-failure&lt;/a&gt; commands. Alternatively, we can use the AWS SDK to do the same. Let's use the AWS Java SDK for it. First, we need to add the following dependency to &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;software.amazon.awssdk&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;lambda&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We implemented the functionality to send the callback in the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25/src/main/java/dev/vkazulkin/callback/SendDurableExecutionCallback.java" rel="noopener noreferrer"&gt;SendDurableExecutionCallback&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendDurableExecutionCallback&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;CALLBACK_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="s"&gt;"Ab9hZXiWYXJuOmF3czpsYW1iZGE6dXMtZWFzdC0xOjI2NTYzNDI1NzYxMDpmdW5jdGlvbjpBdXRob3JDb250ZW50RXh0cmFjdG9yOiRMQVRFU1QvZHVyYWJsZS1leGVjdXRpb24vc2VhcmNoRm9yVmFkeW0xMzUvMmM2NzRiOWEtNTQ0OC0zZDVjLWI3MzUtNTQ1NWM5ODA5MWUwYWl4JDA2N2JkNmRkLTViN2MtNGZmYi05NmY4LTRkM2Y1Yjk3MWE0Zf8"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;LambdaClient&lt;/span&gt; &lt;span class="no"&gt;LAMBDA_CLIENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
  &lt;span class="nc"&gt;LambdaClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Region&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;US_EAST_1&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;ObjectMapper&lt;/span&gt; &lt;span class="no"&gt;MAPPER&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;ObjectMapper&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;approvedStatus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;MAPPER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeValueAsString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
         &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultUpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"approved"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;LAMBDA_CLIENT&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendDurableExecutionCallbackSuccess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                  &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;callbackId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CALLBACK_ID&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SdkBytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromUtf8String&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;approvedStatus&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we need to configure the callback ID (please replace it with your individual one). Then, we create the Lambda client. Next, we invoke its &lt;em&gt;sendDurableExecutionCallbackSuccess&lt;/em&gt; method to send the successful callback. We pass the callback ID and payload as JSON. This payload should be deserializable into the object of type &lt;em&gt;UpcomingTalkApprovalStatus&lt;/em&gt; (see above). Similarly, we can send the failure callback by using the &lt;em&gt;sendDurableExecutionCallbackFailure&lt;/em&gt; operation. Please move this logic with all required dependencies to the service that sends the callbacks. Now let's send the callback (approximately 23 minutes later) and see what 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet212hn5pb0xlw2bnfxj.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%2Fet212hn5pb0xlw2bnfxj.png" alt=" " width="800" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We observe that the callback was successfully received and processed. After it, the last &lt;em&gt;writeAuthorContentToFile-step&lt;/em&gt; step was executed as well. But we also observe that execution of our Lambda durable function started from the beginning. What happened here exactly? After our function resumes from a pause or interruption, the system performs replay. During replay, our code runs from the beginning but skips over completed checkpoints, using stored results instead of re-executing completed operations. This replay mechanism ensures consistency while enabling long-running executions.&lt;/p&gt;

&lt;p&gt;Here is the picture, which describes the concept that I took from this &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;source&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%2Fb4lrx9jv7f51ujh8725g.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%2Fb4lrx9jv7f51ujh8725g.png" alt=" " width="800" height="777"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are also other individual operations that &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java" rel="noopener noreferrer"&gt;AWS Lambda Durable Execution SDK for Java&lt;/a&gt; supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;wait&lt;/em&gt; - wait suspends the function and resumes after the specified duration. There is no charge during suspension.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;waitAsync&lt;/em&gt; - starts the wait timer but returns a DurableFuture immediately, allowing other operations to run concurrently. The execution only suspends when you call .get() on the future (if the wait hasn't completed yet).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;createCallback&lt;/em&gt; - callbacks suspend execution until an external system sends a result. Use this for human approvals, webhooks, or any event-driven workflow.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;In this part of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to implement wait and callback.  &lt;/p&gt;

&lt;p&gt;In the next part, we'll explore the "wait for condition" operation to poll a condition until it's met. Still, our Lambda durable function itself contains too much business logic. Ideally, it should simply be the orchestrator and contain as little business logic as possible. That's why, in the subsequent parts, we'll move the logic for each step into a separate Lambda function. With that, we'll explore how to invoke another Lambda function within the durable step. We'll also show how to invoke multiple Lambda functions in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambdadurablefunctions</category>
      <category>s3files</category>
    </item>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 2 Durable synchronous and asynchronous steps</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 17 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1k6e</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1k6e</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1gp3"&gt;part 1&lt;/a&gt;, we introduced what we'll cover in this series and our sample application. We also gave a short introduction to the services that we'll use throughout this series: &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;AWS Lambda durable functions&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files.html#s3-files-what-is" rel="noopener noreferrer"&gt;Amazon S3 Files&lt;/a&gt;. In this part of the series, we'll explore how to use the AWS Lambda Durable Execution SDK for Java to create and execute durable steps synchronously and asynchronously.&lt;/p&gt;

&lt;h2&gt;
  
  
  First implementation attempt of our sample application
&lt;/h2&gt;

&lt;p&gt;Throughout this article series, we'll develop a simple application to show how to build multi-step applications and orchestrate workflows using AWS Lambda durable functions. This simple application provides the functionality to extract the content for the provided user. To this content belongs searching for YouTube videos of the authors and their upcoming talks. We'll store the result on Amazon S3 Files in JSON format. As this operation may potentially take time, we'll additionally provide access to the author content through the Amazon API Gateway:&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%2F0l2gsqbvyfq2dh2gjpzn.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%2F0l2gsqbvyfq2dh2gjpzn.png" alt=" " width="510" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll start with the search for YouTube videos and upcoming talks of the author synchronously. For the sake of simplicity, we won't implement any persistent layer and store the static content in memory. We'll store the result on Amazon S3 Files in JSON format. &lt;/p&gt;

&lt;p&gt;You can find the code examples in my &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-java-sdk&lt;/a&gt; repository. We'll go through all the examples in more detail in the subsequent articles.&lt;/p&gt;

&lt;p&gt;First of all, let's declare some important dependencies in &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt;. The most important one is the &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java" rel="noopener noreferrer"&gt;AWS Lambda Durable Execution SDK for Java&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;software.amazon.lambda.durable&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;aws-durable-execution-sdk-java&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why do we need such an SDK?  &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html#durable-functions-how-it-works" rel="noopener noreferrer"&gt;Under the hood&lt;/a&gt;, durable functions are regular Lambda functions using a checkpoint/replay mechanism to track progress and support long-running operations through user-defined suspension points, commonly referred to as durable execution. After your function resumes from a pause or interruption, the system performs replay. During replay, your code runs from the beginning but skips over completed checkpoints, using stored results instead of re-executing completed operations. This replay mechanism ensures consistency while enabling long-running executions.&lt;/p&gt;

&lt;p&gt;To harness this checkpoint-and-replay mechanism in your applications, Lambda provides a durable execution SDK. The SDK abstracts away the complexity of managing checkpoints and replay, exposing simple primitives called durable operations that you use in your code. The SDK integrates seamlessly with your existing Lambda development workflow.&lt;/p&gt;

&lt;p&gt;To provide the implementation of our Lambda durable function, we need the class to extend &lt;em&gt;DurableHandler&lt;/em&gt; and implement only the method &lt;em&gt;handleRequest&lt;/em&gt;.  Let's do it for our &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/src/main/java/dev/vkazulkin/handler/AuthorContentExtractor.java" rel="noopener noreferrer"&gt;AuthorContentExtractor&lt;/a&gt; Lambda durable function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthorContentExtractor&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DurableHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AuthorContent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;AbstractAuthorContentExtractor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;AuthorContent&lt;/span&gt; &lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;DurableContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStepConfig&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;       
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForUpcomingTalks-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchForUpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;youtubeVideos&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForYouTubeVideos-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;YouTubeVideos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchForYouTubeVideos&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;authorContent&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;AuthorContent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;youtubeVideos&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

   &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"writeAuthorContentToFile-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Void&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeAuthorContentToFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;      

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go step by step through what's happening here. First of all, we also implement the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/src/main/java/dev/vkazulkin/handler/AbstractAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AbstractAuthorContentExtractor&lt;/a&gt; interface. We place some common methods there that we'll use when improving our application.&lt;/p&gt;

&lt;p&gt;Now let's introduce the concept of durable steps. Steps run business logic with built-in retries and automatic checkpointing. Each step saves its result, so your function resumes from the last completed step after an interruption. With the SDK, you wrap your Lambda event handler, which then provides a &lt;em&gt;DurableContext&lt;/em&gt; alongside your event. This context gives you access to durable operations like steps. You write your function logic as normal sequential code, but instead of calling services directly, you wrap those calls in steps for automatic checkpointing and retries. When you need to pause execution, you add waits that suspend your function without incurring charges.  We'll show it in the next part of the series. The SDK handles all the complexity of state management and replay behind the scenes, so your code remains clean and readable.&lt;/p&gt;

&lt;p&gt;Here is the picture, which describes the concept that I took from this &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;source&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%2Fb4lrx9jv7f51ujh8725g.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%2Fb4lrx9jv7f51ujh8725g.png" alt=" " width="800" height="777"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We wrapped several operations into separate durable steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;searchForUpcomingTalks-step&lt;/li&gt;
&lt;li&gt;searchForYouTubeVideos-step&lt;/li&gt;
&lt;li&gt;writeAuthorContentToFile-step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's look into the &lt;em&gt;searchForUpcomingTalks-step&lt;/em&gt; first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForUpcomingTalks-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchForUpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To create a step, we need to give it a name and the result type. We also have to give it the function to execute, receiving a &lt;em&gt;StepContext&lt;/em&gt;. The return type of the function should be the same as the result type. In our case, we execute the &lt;em&gt;searchForUpcomingTalks&lt;/em&gt; function, which returns the static list of the upcoming talks. Optionally, we can also pass the &lt;em&gt;StepConfig&lt;/em&gt;. In our case, we create it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stepConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StepConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;semanticsPerRetry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StepSemantics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AT_LEAST_ONCE_PER_RETRY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                 
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retryStrategy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RetryStrategies&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exponentialBackoff&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;                                
      &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// max attempts&lt;/span&gt;
      &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// initial delay  &lt;/span&gt;
      &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// max delay&lt;/span&gt;
      &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;// backoff multiplier&lt;/span&gt;
      &lt;span class="nc"&gt;JitterStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FULL&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
   &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We create the &lt;em&gt;StepConfig&lt;/em&gt; by passing the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry strategy: fixed delay, linear backoff, or exponential backoff. We pass the latter here.&lt;/li&gt;
&lt;li&gt;semantics per retry: at-least-once delivery (default, which we also pass). The step may be re-executed if interrupted. START checkpoint is fire-and-forget. At-most-once delivery per retry attempt. START checkpoint is awaited before user code runs.&lt;/li&gt;
&lt;li&gt;custom serializer for the step with &lt;em&gt;serDes&lt;/em&gt; method. We don't provide it here, as we're happy with the default one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;searchForYouTubeVideos-step&lt;/em&gt; step works the same. After we have collected the result of the search for the upcoming talks and YouTube videos, we use the &lt;em&gt;writeAuthorContentToFile-step&lt;/em&gt; step to serialize the result to JSON and write it to the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nc"&gt;Void&lt;/span&gt; &lt;span class="nf"&gt;writeAuthorContentToFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuthorContent&lt;/span&gt; &lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;authorContentAsJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OBJECT_MAPPER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeValueAsString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="o"&gt;()+&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="o"&gt;()+&lt;/span&gt;&lt;span class="s"&gt;".json"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;Path&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Paths&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;WORKSPACE_MOUNT&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;strToBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authorContentAsJson&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strToBytes&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use the standard Java Path API to do it.  This is because we use Amazon S3 Files, and it supports POSIX. Strictly speaking, we could also use S3 here, but I wanted to show the functionality of S3 Files.&lt;/p&gt;

&lt;p&gt;The directory where we store the file is passed as an environment variable &lt;em&gt;WORKSPACE_MOUNT&lt;/em&gt;. In our case, it is &lt;em&gt;/mnt/workspace&lt;/em&gt;. See the Infrastructure as Code explanation below, especially the &lt;em&gt;LocalMountPath&lt;/em&gt; setting of the file system configuration for the Lambda function.&lt;/p&gt;

&lt;p&gt;For infrastructure as code (IaC), I use AWS SAM, and you can find it &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/template.yaml" rel="noopener noreferrer"&gt;here&lt;/a&gt;. To indicate that our Lambda function is durable, we need to provide some additional &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-basic-concepts.html" rel="noopener noreferrer"&gt;durable config properties&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;AuthorContentExtractorFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AuthorContentExtractor&lt;/span&gt;
      &lt;span class="na"&gt;Handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev.vkazulkin.handler.AuthorContentExtractor::handleRequest&lt;/span&gt;
      &lt;span class="na"&gt;DurableConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ExecutionTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;
        &lt;span class="na"&gt;RetentionPeriodInDays&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
        &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Execution timeout. The execution timeout controls how long a durable execution can run from start to completion. This is different from the Lambda function timeout, which controls how long a single function invocation can run. A durable execution can span multiple Lambda function invocations as it progresses through checkpoints, waits, and replays. The execution timeout applies to the total elapsed time of the durable execution, not to individual function invocations. Set the Execution timeout value in seconds (default: 86400 seconds / 24 hours, minimum: 60 seconds, maximum: 31536000 seconds / 1 year).&lt;/li&gt;
&lt;li&gt;Retention period. The retention period controls how long Lambda retains execution history and checkpoint data after a durable execution completes. This data includes step results, execution state, and the complete checkpoint log. After the retention period expires, Lambda deletes the execution history and checkpoint data. You can no longer retrieve execution details or replay the execution. Set the Retention period value in days (default: 14 days, minimum: 1 day, maximum: 90 days).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a very detailed explanation of how to create S3 Files and mount them to the Lambda function, please read the brilliant article &lt;a href="https://edjgeek.com/blog/s3-files-lambda-agents/" rel="noopener noreferrer"&gt;Lambda Just Got a File System. I Put AI Agents on It&lt;/a&gt; by Eric Johnson. I mostly copied the IaC part from it and adjusted it to my needs. I only describe the main steps here:&lt;/p&gt;

&lt;p&gt;First, we reference the networking stack, which we place in a separate file. Then we create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPC networking for Lambda functions using S3 Files.&lt;/li&gt;
&lt;li&gt;VPC with private subnets (for Lambda + mount targets).&lt;/li&gt;
&lt;li&gt;Security groups for NFS traffic.&lt;/li&gt;
&lt;li&gt;Public subnet with a NAT gateway. We'll need them for internet access to Bedrock and Bedrock AgentCore Web Search Tool. In later parts, we'll use both services.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;NetworkingStack&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Application&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/main/resources/stacks/network.yaml&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Next, we need to create an S3 Bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt; &lt;span class="na"&gt;WorkspaceBucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::S3::Bucket&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;BucketName &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vadym-s3-files-workspace&lt;/span&gt;
      &lt;span class="na"&gt;BucketEncryption&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ServerSideEncryptionConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ServerSideEncryptionByDefault&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;SSEAlgorithm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AES256&lt;/span&gt;
      &lt;span class="na"&gt;PublicAccessBlockConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;BlockPublicAcls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;BlockPublicPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;IgnorePublicAcls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;RestrictPublicBuckets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;VersioningConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please don't forget to rename the S3 Bucket. After that, we need to create S3 Files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;S3FileSystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::S3Files::FileSystem&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;WorkspaceBucket.Arn&lt;/span&gt;
      &lt;span class="na"&gt;RoleArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;S3FilesRole.Arn&lt;/span&gt;
      &lt;span class="na"&gt;AcceptBucketWarning&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we reference an already created S3 Bucket. We also need to profile the appropriate IAM Role (see the SAM template for the IaC). Next, we need to create 2 different mount targets for the S3 File. They should use private subnets in different availability zones. Here is an example of one such mount target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;MountTargetA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::S3Files::MountTarget&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FileSystemId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;S3FileSystem.FileSystemId&lt;/span&gt;
      &lt;span class="na"&gt;SubnetId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;NetworkingStack.Outputs.PrivateSubnetAId&lt;/span&gt;
      &lt;span class="na"&gt;SecurityGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;NetworkingStack.Outputs.MountTargetSGId&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we need to create an S3 Files access point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;S3FilesAccessPoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::S3Files::AccessPoint&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FileSystemId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;S3FileSystem.FileSystemId&lt;/span&gt;
      &lt;span class="na"&gt;PosixUser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Uid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1000'&lt;/span&gt;
        &lt;span class="na"&gt;Gid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1000'&lt;/span&gt;
      &lt;span class="na"&gt;RootDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/lambda&lt;/span&gt;
        &lt;span class="na"&gt;CreationPermissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;OwnerUid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1000'&lt;/span&gt;
          &lt;span class="na"&gt;OwnerGid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1000'&lt;/span&gt;
          &lt;span class="na"&gt;Permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;755'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;em&gt;CreationPermissions&lt;/em&gt; property is crucial. It auto-creates the &lt;em&gt;/lambda&lt;/em&gt; directory within our S3 bucket with the right ownership when a client first connects. Without it, the root directory is owned by root (UID 0), and Lambda (running as UID 1000 through the access point) can’t create subdirectories.&lt;/p&gt;

&lt;p&gt;Lastly, we need to provide configuration and give permissions to our Lambda function to use S3 Files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;AuthorContentExtractorFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;DependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MountTargetA&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MountTargetB&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AuthorContentExtractor&lt;/span&gt;
      &lt;span class="na"&gt;Handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev.vkazulkin.handler.AuthorContentExtractor::handleRequest&lt;/span&gt;
      &lt;span class="na"&gt;DurableConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ExecutionTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;
        &lt;span class="na"&gt;RetentionPeriodInDays&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
      &lt;span class="na"&gt;VpcConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;SecurityGroupIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;NetworkingStack.Outputs.LambdaSGId&lt;/span&gt;
        &lt;span class="na"&gt;SubnetIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;NetworkingStack.Outputs.PrivateSubnetAId&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;NetworkingStack.Outputs.PrivateSubnetBId&lt;/span&gt;
      &lt;span class="na"&gt;FileSystemConfigs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;S3FilesAccessPoint.AccessPointArn&lt;/span&gt;
          &lt;span class="na"&gt;LocalMountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/mnt/workspace&lt;/span&gt;
      &lt;span class="na"&gt;Policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
          &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Sid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MountS3Files&lt;/span&gt;
              &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
              &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;s3files:ClientMount&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;s3files:ClientWrite&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;s3files:ClientRootAccess&lt;/span&gt;
              &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;S3FileSystem.FileSystemArn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we provide the VPC and file system configuration (including local mount path) and the policy for our Lambda function to use the S3 Files.&lt;/p&gt;

&lt;p&gt;Now we can build and package our application with &lt;em&gt;mvn clean package&lt;/em&gt; and deploy it with &lt;em&gt;sam deploy&lt;/em&gt;.  The deployment process can take up to 10 minutes because of the creation and mounting of S3 Files.&lt;/p&gt;

&lt;p&gt;To test our Lambda durable function, we can navigate to the Lambda service, search for the &lt;em&gt;AuthorContentExtractor&lt;/em&gt; function, and go to the "Test" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbz7obubst4toraw8urlp.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%2Fbz7obubst4toraw8urlp.png" alt=" " width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We need to pass the following sample JSON Event to it, which represents the author:&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;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vadym"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kazulkin"&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;Then we can test it. After that, we go to the "Durable execution" tab and can see all the execution details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input and output as JSON&lt;/li&gt;
&lt;li&gt;Logger output&lt;/li&gt;
&lt;li&gt;Details like durable operations (with individual inputs and outputs of each step) and event history. If something goes wrong, the step status will be set to failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our case, we invoked the durable function directly from the Lambda console. As an alternative, we can also use the Lambda SDK for it. We can also put an API Gateway in front of it if you wish and even invoke the Lambda function &lt;a href="https://dev.toSet%20up%20asynchronous%20invocation%20of%20the%20backend%20Lambda%20function"&gt;asynchronously&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%2Ffajhzt1bgyku05ikqjtl.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%2Ffajhzt1bgyku05ikqjtl.png" alt=" " width="800" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can also see the JSON file with author content in the S3 Bucket that we created. Alternatively, I implemented a &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/src/main/java/dev/vkazulkin/handler/GetAuthorContentResult.java" rel="noopener noreferrer"&gt;GetAuthorContentResult&lt;/a&gt; Lambda function with the name &lt;em&gt;GetAuthorContentResult&lt;/em&gt;. This Lambda function takes the author's first and last name and streams the content of the JSON file from the S3 bucket. This JSON file is stored in the subdirectory &lt;em&gt;/lambda&lt;/em&gt; of our created S3 bucket. I also created and put an &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/template.yaml" rel="noopener noreferrer"&gt;API Gateway&lt;/a&gt; in front of this Lambda function. Please use the &lt;em&gt;/result/{firstname}/{lastname}&lt;/em&gt; HTTP GET endpoint for it, for example /result/Vadym/Kazulkin. Be aware that it takes several minutes for the file to appear in the S3 Bucket after being written to S3 Files. If you need the result immediately, you can rewrite this Lambda to retrieve the result from S3 Files instead.&lt;/p&gt;

&lt;p&gt;This was a very simple application to explore the durable steps with the AWS Lambda Durable Execution SDK for Java. Let's improve our application a bit. Until now, we searched for the upcoming talks and YouTube videos sequentially. But we can do it in parallel. For that, we can use the &lt;em&gt;stepAsync&lt;/em&gt; operation instead of &lt;em&gt;step&lt;/em&gt;.  I provided a separate implementation, &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/src/main/java/dev/vkazulkin/handler/AsyncAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AsyncAuthorContentExtractor&lt;/a&gt;, and the Lambda durable function with the name &lt;em&gt;AsyncAuthorContentExtractor&lt;/em&gt; for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;upcomingTalksFuture&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stepAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForUpcomingTalks-async-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchForUpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;youtubeVideosFuture&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stepAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForYouTubeVideos-async-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;YouTubeVideos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchForYouTubeVideos&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;upcomingTalksFuture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;youtubeVideos&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;youtubeVideosFuture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, &lt;em&gt;stepAsync&lt;/em&gt; invocation returns a &lt;em&gt;DurableFuture&lt;/em&gt; of the result type - a future representing the step result. This operation is non-blocking; we only block when invoking the &lt;em&gt;get&lt;/em&gt; method on the result. Everything else remains the same in this asynchronous implementation as it was in the synchronous one.&lt;/p&gt;

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

&lt;p&gt;In this part of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to create and execute durable steps synchronously and asynchronously. In the next part, we'll extend our application by adding and implementing &lt;em&gt;wait&lt;/em&gt; and &lt;em&gt;callback&lt;/em&gt; operations to it. Still, our Lambda durable function itself contains too much business logic. Ideally, it should simply be the orchestrator and contain as little business logic as possible. That's why in the later parts, we'll move the logic associated with each step into a separate Lambda function. We'll explore how to invoke another Lambda function within the durable step. We'll also show how to invoke multiple Lambda functions in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambdadurablefunctions</category>
      <category>s3files</category>
    </item>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 1 Introduction to the series and services</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 10 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1gp3</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1gp3</guid>
      <description>&lt;h2&gt;
  
  
  Introduction to the series
&lt;/h2&gt;

&lt;p&gt;Throughout this article series, we'll develop a simple application to show how to build multi-step applications and orchestrate workflows using AWS Lambda durable functions. For it, we'll explore the &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java" rel="noopener noreferrer"&gt;AWS Lambda Durable Execution SDK for Java&lt;/a&gt;. This simple application provides the functionality to extract the content for the provided user. This content involves searching for YouTube videos of the authors and their upcoming talks. We'll store the result on Amazon S3 Files in JSON format. As this operation may potentially take time, we'll additionally provide access to the author content through the Amazon API Gateway:&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%2F0l2gsqbvyfq2dh2gjpzn.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%2F0l2gsqbvyfq2dh2gjpzn.png" alt=" " width="510" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We'll start with the simple application to search for YouTube videos and upcoming talks of the author synchronously. First, we'll use durable steps and then later add waits and callbacks. We won't implement any persistent layer and store the static content in memory. &lt;/li&gt;
&lt;li&gt;We'll then improve our application by adding asynchronous communication where it makes sense. For example, searching for YouTube videos and upcoming talks can be done in parallel.&lt;/li&gt;
&lt;li&gt;We'll then further improve our application by converting the AWS durable function to a pure orchestrator. This means we'll move the business logic for searching for YouTube videos and upcoming talks into separate Lambda functions. With that, our Lambda durable function will invoke those separate Lambda functions in parallel and combine the results&lt;/li&gt;
&lt;li&gt;Finally, we'll further extend our application to use &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway-target-connector-web-search-tool.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Web Search Tool&lt;/a&gt; to search for the author content on the web. With that, our Lambda durable function becomes an orchestrator of the agentic AI workflows. As we'll use Java in this series, we'll explore how to run this business logic responsible for the Amazon Bedrock AgentCore Web Search Tool. As this tool is exposed as an MCP tool by the &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Gateway&lt;/a&gt;, we need a framework capable of providing AI functionality and supporting MCP. That's why we'll use Spring AI. As 2 operational environments for our agentic AI workflows, we'll explore an AWS Lambda function and &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/agents-tools-runtime.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Runtime&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find the code examples in my &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-java-sdk&lt;/a&gt; repository. We'll go through all the examples in more detail in the subsequent articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction to the services used throughout the series
&lt;/h2&gt;

&lt;p&gt;Throughout this series, we'll use the following AWS services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;AWS Lambda durable functions&lt;/a&gt;. With Lambda durable functions, you can build resilient multi-step applications and AI workflows. These can execute for up to one year while maintaining reliable progress despite interruptions. When a durable function runs, this complete lifecycle is called a durable execution, which uses checkpoints to track progress and automatically recover from failures through replay, re-executing from the beginning while skipping completed work. Within each function, you use durable operations as fundamental building blocks. Steps execute business logic with built-in retries and progress tracking. Waits suspend execution without incurring compute charges, making them ideal for long-running processes. These processes include human-in-the-loop workflows or polling external dependencies. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files.html#s3-files-what-is" rel="noopener noreferrer"&gt;Amazon S3 Files&lt;/a&gt;. S3 Files is a shared file system that connects any AWS compute resource directly with your data in Amazon S3. It provides fast, direct access to all of your S3 data as files with full file system semantics and low-latency performance, without your data ever leaving S3. Every file-based application, agent, and team can access and work with your S3 data as a file system using the tools they already depend on. Built using Amazon EFS, S3 Files gives you the performance and simplicity of a file system with the scalability, durability, and cost-effectiveness of S3. You can read, write, and organize data using file and directory operations. At the same time, S3 Files manages the synchronization of changes between your bucket and file system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We'll also introduce in the later parts of this series the following Amazon Bedrock AgentCore services or features: &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway.html" rel="noopener noreferrer"&gt;Gateway&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway-target-connector-web-search-tool.html" rel="noopener noreferrer"&gt;Web Search Tool&lt;/a&gt;, and &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/agents-tools-runtime.html" rel="noopener noreferrer"&gt;Runtime&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambdadurablefunctions</category>
      <category>s3files</category>
    </item>
    <item>
      <title>Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore - Part 3 Develop Agents as web application with Embabel</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 03 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-agents-as-2d94</link>
      <guid>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-agents-as-2d94</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb"&gt;part 2&lt;/a&gt;, we developed our first AI agents with the Embabel shell. We learned many concepts of this framework, such as agents, actions, and goals. In this article, we'll slightly adjust our application to convert it into a web application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update from September 2026&lt;/strong&gt; Please read my article &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-4-upgrade-sample-e4o"&gt;Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore - Part 4 Upgrade sample application to Embabel 1.5&lt;/a&gt; to find out how to upgrade this sample application to Embabel version 1.5, which supports Spring Boot 4.x and Spring AI 2.x.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement AI agents as a web application with Embabel
&lt;/h2&gt;

&lt;p&gt;You can look at my GitHub &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/embabel-1.x-conference-app-agent-local" rel="noopener noreferrer"&gt;embabel-1.x-conference-app-agent-local&lt;/a&gt; repository for the example. We'll completely reuse the implementation of both agents, the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/CreateTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;CreateTalksAndApplyForConferencesAgent&lt;/a&gt; and the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/SearchForTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/a&gt; that we implemented in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb"&gt;part 2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Those agents are capable of responding to the following prompts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; "Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today. Also, provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference." &lt;em&gt;SearchForTalksAndApplyForConferencesAgent&lt;/em&gt; is responsible for responding to this prompt.&lt;/li&gt;
&lt;li&gt;"Please create a talk with a cool title (max 60 characters long) and description (max 300 characters long) about using Spring AI on the Amazon Bedrock AgentCore service. Then provide me with the list of conferences, including their IDs, with Java topics happening in 2026 and 2027, with the call for papers open today. Finally, for each conference, apply individually for it with the talk just created." &lt;em&gt;CreateTalksAndApplyForConferencesAgent&lt;/em&gt; is responsible for responding to this prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;First, let's check that we declared the following dependencies in &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.embabel.agent&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;embabel-agent-starter-bedrock&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${embabel-agent.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-web&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, make sure not to declare the &lt;em&gt;embabel-agent-starter-shell&lt;/em&gt; dependency, as we don't use Embabel shell in this article.&lt;/p&gt;

&lt;p&gt;All we need to do now is to additionally implement the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/controller/EmbabelAgentController.java" rel="noopener noreferrer"&gt;EmbabelAgentController&lt;/a&gt; controller.&lt;/p&gt;

&lt;p&gt;Let's first implement a generic endpoint that will accept both prompts. Let's first autowire and create some objects coming from Embabel that we'll use for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmbabelAgentController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AgentPlatform&lt;/span&gt; &lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AgentInvocation&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ProcessOptions&lt;/span&gt; &lt;span class="n"&gt;processOptions&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;ProcessOptions&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withVerbosity&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;Verbosity&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withShowPrompts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withShowLlmResponses&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withShowPlanning&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDebug&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;


&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;EmbabelAgentController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AgentPlatform&lt;/span&gt; &lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentPlatform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invocation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentInvocation&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processOptions&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we autowire the Embabel's &lt;em&gt;AgentPlatform&lt;/em&gt; object and use it to create the &lt;em&gt;AgentInvocation&lt;/em&gt; object. We additionally pass the &lt;em&gt;ProcessOption&lt;/em&gt; object that is configured to show planning, LLM responses, and prompts. We also pass the type of the response - the domain object returned after the agents achieve their goals; in our case, the  &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/domain/Domain.java" rel="noopener noreferrer"&gt;Domain.ConferenceApplications&lt;/a&gt; class:&lt;/p&gt;

&lt;p&gt;Now, let's implement the endpoint itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/generic"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;genericPrompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"request"&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;UserInput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; 
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;last&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we wrap the prompt into Embabel's &lt;em&gt;UserInput&lt;/em&gt; object that we put into the map with all inputs required to run the agent. Then we use the &lt;em&gt;AgentInvocation&lt;/em&gt; object to run the agent with the specified input synchronously. We can also invoke &lt;em&gt;runAsync&lt;/em&gt; to run the agent asynchronously. Finally, we retrieve the result of the agent invocation with the specified type.&lt;/p&gt;

&lt;p&gt;Let's build the application with &lt;code&gt;mvn clean package&lt;/code&gt; and run it locally with &lt;code&gt;mvn spring-boot:run&lt;/code&gt;. We'll send the prompt number 1 to this endpoint. What we'll see is that Embabel picked the wrong agent, &lt;em&gt;CreateTalksAndApplyForConferencesAgent&lt;/em&gt;, to run. Why? As we don't use the Embabel shell, Embabel doesn't analyze the prompt and compute the score to determine what exact agent to run. It currently only looks into the return type of all agents and picks the first one that matches the type defined in the &lt;em&gt;AgentInvocation&lt;/em&gt; object. But both our agents have the same return type &lt;em&gt;Domain.ConferenceApplications&lt;/em&gt;. Their goals, in the end, are the same: to apply for the conferences. With that, for our 2 agents, there is a 50% probability that Embabel will pick the wrong agent to run.&lt;/p&gt;

&lt;p&gt;How can we fix that? For example, we can define the endpoint per agent. Let's do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/applyToConferencesWithExistingTalks"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;applyToConferencesWithExistingTalks&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
       &lt;span class="nc"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AGENT_NAME&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/applyToConferencesWithNewTalks"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;applyToConferencesWithNewTalks&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;CreateTalksAndApplyForConferencesAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AGENT_NAME&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; 
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I additionally wrote 2 helper methods, &lt;em&gt;invokeAgent&lt;/em&gt; and &lt;em&gt;getAgentByName&lt;/em&gt;, to simplify the agent search by name and its subsequent invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;invokeAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;        
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"request"&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;UserInput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAgentByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agentProcess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createAgentProcess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;processOptions&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;agentProcess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;last&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="nf"&gt;getAgentByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;optionalAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agents&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findFirst&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

     &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="n"&gt;optionalAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"agent with the name "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" not found"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
     &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;optionalAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's explain what happens in the &lt;em&gt;invokeAgent&lt;/em&gt; method. We first invoke the &lt;em&gt;getAgentByName&lt;/em&gt; method. It searches in the collection of agents that the agent platform knows for one that has a specified name. All classes annotated with Embabel's &lt;em&gt;Agent&lt;/em&gt; annotation will be registered in the agent platform. Then we use the &lt;em&gt;AgentPlatform&lt;/em&gt; object to create the &lt;em&gt;AgentProcess&lt;/em&gt; and pass the found agent, the already described process options, and inputs to it. Then we run the &lt;em&gt;AgentProcess&lt;/em&gt; and return its last output of type &lt;em&gt;Domain.ConferenceApplications.class&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Now we send prompt number 1 (see above) to the &lt;em&gt;applyToConferencesWithExistingTalks&lt;/em&gt; endpoint and prompt number 2 to the &lt;em&gt;applyToConferencesWithNewTalks&lt;/em&gt; endpoint. We'll see something similar as a result, as we saw in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb"&gt;part 2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For example, here I use &lt;a href="https://httpie.io/" rel="noopener noreferrer"&gt;httpie&lt;/a&gt; to send prompt number 1: &lt;br&gt;
&lt;code&gt;http GET http://localhost:8080/applyToConferencesWithExistingTalks?prompt="Please provide me with the list of conferences including their IDs with Java topic happening in 2026 and 2027 with call for papers open today. Also provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference." Content-Type:text/plain&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the response of the agent:&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%2Fxq4w3wz6pde5nonc5twv.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%2Fxq4w3wz6pde5nonc5twv.png" alt=" " width="799" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Having one endpoint per Embabel agent has its pros and cons. The biggest disadvantage is that a service like &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-5-deploy-mcp-client-for-1n11"&gt;AgentCore Runtime&lt;/a&gt; exposes only one HTTP POST &lt;em&gt;/invocations&lt;/em&gt; endpoint to send the prompt to the web application. This means that we'll need to think about how to design, implement, and deploy each Embabel agent on a separate AgentCore Runtime. Each agent belonging to the same web application will have a lot of shared logic with other agents. This is not very convenient. The biggest advantage is that you can scale each Embabel agent individually. AgentCore Runtime will do it for you.&lt;/p&gt;

&lt;p&gt;It's worth checking in the upcoming versions of Embabel whether there are other options available &lt;a href="https://docs.embabel.com/embabel-agent/guide/1.0.0-SNAPSHOT/#reference.invoking" rel="noopener noreferrer"&gt;to invoke the agent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update from September 2026&lt;/strong&gt; Please read my article &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-4-upgrade-sample-e4o"&gt;Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore - Part 4 Upgrade sample application to Embabel 1.5&lt;/a&gt; to find out how to upgrade this sample application to Embabel version 1.5, which supports Spring Boot 4.x and Spring AI 2.x.&lt;/p&gt;

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

&lt;p&gt;In this article, we covered how to develop a web application with Embabel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springai</category>
      <category>embabel</category>
      <category>bedrockagentcore</category>
    </item>
    <item>
      <title>Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore - Part 2 Develop Agents with Embabel shell</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 27 Jul 2026 15:35:30 +0000</pubDate>
      <link>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb</link>
      <guid>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-1-introduction-to-1ebl"&gt;part 1&lt;/a&gt;, we addressed some challenges when developing AI agents with Spring AI frameworks only. We start exploring how Embabel is capable of addressing and solving those challenges. Throughout this series, we'll start improving the agent developed in the &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-4-provide-mcp-tools-for-2odf"&gt;Provide MCP tools for Conference application via AgentCore Gateway&lt;/a&gt; article using the Embabel framework. First, we'll use the Embabel shell, then the web application. You can look at my GitHub &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/embabel-1.x-conference-app-agent-local" rel="noopener noreferrer"&gt;embabel-1.x-conference-app-agent-local&lt;/a&gt; repository for the example. I updated them to use the recently released Embabel version 1.0.0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update from September 2026&lt;/strong&gt; Please read my article &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-4-upgrade-sample-e4o"&gt;Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore - Part 4 Upgrade sample application to Embabel 1.5&lt;/a&gt; to find out how to upgrade this sample application to Embabel version 1.5, which supports Spring Boot 4.x and Spring AI 2.x.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement AI agents with Embabel shell
&lt;/h2&gt;

&lt;p&gt;In this article, we'll implement 2 AI agents capable of responding to the following prompts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; "Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today. Also, provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference."&lt;/li&gt;
&lt;li&gt;"Please create a talk with a cool title (max 60 characters long) and description (max 300 characters long) about using Spring AI on the Amazon Bedrock AgentCore service. Then provide me with the list of conferences, including their IDs, with Java topics happening in 2026 and 2027, with the call for papers open today. Finally, for each conference, apply individually for it with the talk just created."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As our sample application also supports only searching for the conference as an attendee, not as a speaker, you can similarly implement those agents.&lt;/p&gt;

&lt;p&gt;Before we start, we need to understand the following &lt;a href="https://github.com/embabel/embabel-agent#key-concepts" rel="noopener noreferrer"&gt;key concepts&lt;/a&gt; of the Embabel framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Actions: Steps an agent takes&lt;/li&gt;
&lt;li&gt;Goals: What an agent is trying to achieve&lt;/li&gt;
&lt;li&gt;Conditions: Conditions to assess before executing an action or determining that a goal has been achieved. Conditions are reassessed after each action is executed.&lt;/li&gt;
&lt;li&gt;Domain model: Objects underpinning the flow and informing Actions, Goals and Conditions.&lt;/li&gt;
&lt;li&gt;Plan: A sequence of actions to achieve a goal. Plans are dynamically formulated by the system, not the programmer. The system replans after the completion of each action, allowing it to adapt to new information as well as observe the effects of the previous action. This is effectively an OODA loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let's first define the &lt;em&gt;actions&lt;/em&gt; for our agent - steps an agent takes.&lt;br&gt;
We can split our first agent into the following actions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request: "Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today". LLM responds to it.&lt;/li&gt;
&lt;li&gt;Request: "Provide me with the list of my talks with this topic in the title".  LLM responds to it.&lt;/li&gt;
&lt;li&gt;Request: "For each conference and talk retrieved, apply individually for the conference". This request means that the LLM uses its responses from the first 2 requests as the request and responds to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And we can split our second agent into the following actions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request: "Please create a talk with a cool title (max 60 characters long) and description (max 300 characters long) about using Spring AI on the Amazon Bedrock AgentCore service". LLM responds to it.&lt;/li&gt;
&lt;li&gt;Request: "Provide me with the list of my talks with this topic in the title".  LLM responds to it.&lt;/li&gt;
&lt;li&gt;Request: "For each conference and talk, apply individually for the conference". This request means that the LLM uses its responses from the first 2 requests as the request and responds to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this particular case, steps 2 and 3 are mainly the same for our 2 agents. The difference lies in step 1, where we either retrieve an existing talk(s) or create a new one. Your case might be different, so the design of the agents and their actions might also be different.&lt;/p&gt;

&lt;p&gt;First, let's declare some important dependencies in &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt; to use the Embabel framework (I use version 1.0.0) and its shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.embabel.agent&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;embabel-agent-starter-bedrock&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${embabel-agent.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.embabel.agent&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;embabel-agent-starter-shell&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${embabel-agent.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, make sure not to declare the &lt;em&gt;spring-boot-starter-web&lt;/em&gt; dependency, as we'll implement a web application in the next article.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/EmbabelConferenceApplication.java" rel="noopener noreferrer"&gt;EmbabelConferenceApplication&lt;/a&gt; is the main entry point of our application. Here we also load Amazon Bedrock models, which we declared in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/resources/models/additional-bedrock.yaml" rel="noopener noreferrer"&gt;additional-bedrock.yaml&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AdditionalBedrockModels&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
   &lt;span class="nc"&gt;BedrockModelLoader&lt;/span&gt; &lt;span class="nf"&gt;bedrockModels&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BedrockModelLoader&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;DefaultResourceLoader&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"classpath:models/additional-bedrock.yaml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/domain/Domain.java" rel="noopener noreferrer"&gt;Domain&lt;/a&gt; class, we declared a domain object (Java record) for each request and response, which corresponds to the agent actions described above. For example, for the action to search for the talks, those domain objects look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Request to LLM to create the talk&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ConferenceSearchRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;startDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
  &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;endDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;callForPapersOpenOnThistDate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; 
&lt;span class="o"&gt;}&lt;/span&gt;


&lt;span class="c1"&gt;// Response from LLM for the talk creation request&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;Conference&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;conferenceId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;homepage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;startDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;endDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;callForPapersStartDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;callForPapersEndDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;linkToCallforPapers&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's design those 2 agents. We defined the common actions for both of them in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/AbstractConferenceAgent.java" rel="noopener noreferrer"&gt;AbstractConferenceAgent&lt;/a&gt; class. We defined individual agent-specific actions in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/CreateTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;CreateTalksAndApplyForConferencesAgent&lt;/a&gt; and &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/SearchForTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/a&gt; classes, respectively.&lt;/p&gt;

&lt;p&gt;Let's explain the &lt;em&gt;talk search&lt;/em&gt; agent action from the &lt;em&gt;SearchForTalksAndApplyForConferencesAgent&lt;/em&gt; class in more detail. First of all, we annotate each agent with Embabel's &lt;em&gt;Agent&lt;/em&gt; annotation, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Agent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AGENT_NAME&lt;/span&gt; &lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"search for the talk(s), search for the conference(s) by the given criteria (all, by the topic, by the date range, and by call for papers being open on some date), and apply for them with the found talks"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractConferenceAgent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="o"&gt;....&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we give the agent a name and description.&lt;/p&gt;

&lt;p&gt;The first question is how we can break down a big unstructured prompt into the 3 actions that we described above? Let's start with the action to search for the talk first. The answer is: we can ask LLMs to do this job. Even cheap models are already capable of doing this. Here is how this looks for the action: "Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Action&lt;/span&gt;
&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TalkSearchRequest&lt;/span&gt; &lt;span class="nf"&gt;extractTalkSearchRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserInput&lt;/span&gt; &lt;span class="n"&gt;userInput&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OperationContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
   &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withLlm&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LlmOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withModel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"us.amazon.nova-pro-v1:0"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""
Create a talk search request from this user input, extracting the optional parameters like talk title substring, conference start and end date, and whether the call for papers is still open on a certain date. 
Don't include any other information in the request.
Here is the user input:
     %s"""&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;formatted&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userInput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;()),&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TalkSearchRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's explain step by step what's happening here. The &lt;em&gt;extractTalkSearchRequest&lt;/em&gt; method is an action, which the Agent executes.  That's why we annotated it with the Embabel &lt;em&gt;Action&lt;/em&gt; annotation. This method also takes 2 parameters, both coming from the Embabel framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;UserInput&lt;/em&gt; object, which represents the unstructured user prompt&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;OperationContext&lt;/em&gt;, which gives us access to the AI functionality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We first invoke the &lt;em&gt;ai()&lt;/em&gt; method of the &lt;em&gt;context&lt;/em&gt; object and then provide the LLM to do the job. We can either pass the concrete model AI or use other options that are available, like using the default LLM, LLM by role, and others. As a result, we get back the Embabel implementation of the &lt;em&gt;PromptRunnerOperations&lt;/em&gt;. Or, we can declare such LLMs in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/resources/application.properties" rel="noopener noreferrer"&gt;application.properties&lt;/a&gt;, for example :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;embabel.models.default-llm&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.anthropic.claude-sonnet-4-6&lt;/span&gt;
&lt;span class="py"&gt;embabel.models.llms.best&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.anthropic.claude-sonnet-4-6&lt;/span&gt;
&lt;span class="py"&gt;embabel.models.llms.balanced&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.amazon.nova-pro-v1:0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By invoking the &lt;em&gt;createObject()&lt;/em&gt; method on the &lt;em&gt;PromptRunnerOperations&lt;/em&gt; implementation, we pass the user prompt and instruction to the LLM on how to split this prompt into one subtask or action. It should only contain the information about the talk search criteria. Additionally, we pass the domain object, in this case, the object of type &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/domain/Domain.java" rel="noopener noreferrer"&gt;Domain.TalkSearchRequest&lt;/a&gt;. Embabel will make the best effort to map the LLM response to this domain object, which additionally provides us with type safety. &lt;/p&gt;

&lt;p&gt;The next step is to implement the talk search itself. For this, we define another action in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/SearchForTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Action&lt;/span&gt;
&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Talks&lt;/span&gt; &lt;span class="nf"&gt;talkSearch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TalkSearchRequest&lt;/span&gt; &lt;span class="n"&gt;talkSearchRequest&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Ai&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;        
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;speaker&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;     
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;promptRunner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;            
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withPromptContributors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;talkSearchRequest&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withToolGroup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMcpToolGroupByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"get-talks-by-"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Search for the talks with the given criteria"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Talks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please note that the object of the &lt;em&gt;Domain.TalkSearchRequest&lt;/em&gt; type, which is the output of the previous action, is now the input of this action. Also, we inject the &lt;em&gt;Ai&lt;/em&gt; object coming from the Embabel framework this time directly.  Let's first ignore the &lt;em&gt;config.speaker&lt;/em&gt; invocation, which we'll explain later. Here we pass the prompt, which contains the properties required to search for the talk. When with &lt;em&gt;createObject()&lt;/em&gt; method invocation, we once again pass the prompt and the domain object, which should match the outcome, in this case, the object of type &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/domain/Domain.java" rel="noopener noreferrer"&gt;Domain.Talks&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;To respond to this prompt, the agent needs tools. Please review the articles &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-local-mcp-client-560a"&gt;Develop local MCP client for Conference application &lt;/a&gt; and &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-4-provide-mcp-tools-for-2odf"&gt;Provide MCP tools for Conference application via AgentCore Gateway&lt;/a&gt; to understand the whole setup. I implemented very similar code to initialize the MCP client connected to the Bedrock AgentCore Gateway in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/service/McpToolService.java" rel="noopener noreferrer"&gt;McpToolService&lt;/a&gt; class. MCP client is then exposed as a Spring bean in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/EmbabelConferenceApplication.java" rel="noopener noreferrer"&gt;EmbabelConferenceApplication&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt; 
&lt;span class="nd"&gt;@Primary&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;McpSyncClient&lt;/span&gt; &lt;span class="nf"&gt;geMcpClient&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mcpToolService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMcpClient&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By invoking the &lt;em&gt;withToolGroup(this.getMcpToolGroupByName("get-talks-by-"))&lt;/em&gt; method on the &lt;em&gt;PromptRunner&lt;/em&gt; (see above), we achieved another goal that we described in part 1. We not only split the prompt into individual actions, but are now also able to provide exact MCP tool(s) required to execute it.  Embabel provides the &lt;em&gt;ToolGroup&lt;/em&gt; abstraction for this. In this case, we give the LLM only the tools whose names consist of the &lt;em&gt;get-talks-by-&lt;/em&gt; substring. I implemented the MCP tool filtering in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/AbstractConferenceAgent.java" rel="noopener noreferrer"&gt;AbstractConferenceAgent&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;ToolGroup&lt;/span&gt; &lt;span class="nf"&gt;getMcpToolGroupByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt;  &lt;span class="n"&gt;toolGroupDescription&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="nc"&gt;ToolGroupDescription&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Companion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invoke&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
         &lt;span class="s"&gt;"A collection of tools to interact with 
         the MCP conference search service"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
         &lt;span class="s"&gt;"location"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;
   &lt;span class="o"&gt;);&lt;/span&gt;

  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;noOp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ToolCallContextMcpMetaConverter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Companion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;noOp&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;McpToolGroup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;toolGroupDescription&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
          &lt;span class="s"&gt;"Vadym"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
          &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolGroupPermission&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INTERNET_ACCESS&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
          &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcpClient&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filterMcpTool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;noOp&lt;/span&gt;
      &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;filterMcpTool&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolCallback&lt;/span&gt; &lt;span class="n"&gt;toolCallback&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// not passing name means return true -&amp;gt; pass all tools&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;toolCallback&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getToolDefinition&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The downside is that if we were to rename the MCP tools, we'd need to adjust their names here also. The alternative is to use semantic search for the tools. The alternative is to use semantic search for the tools.&lt;/p&gt;

&lt;p&gt;Now let's come back to the &lt;em&gt;config.speaker()&lt;/em&gt; method invocation, which we saw in the &lt;em&gt;talkSearch&lt;/em&gt; action. Embabel gives the possibility to create different roles and attach different LLMs to them. In our case, we have 2 roles: conference attendee and conference speaker. We can define these roles either programmatically or in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/resources/application.properties" rel="noopener noreferrer"&gt;application.properties&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;conference.attendee.llm.role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;best&lt;/span&gt;
&lt;span class="py"&gt;conference.attendee.persona.role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Conference attendee&lt;/span&gt;
&lt;span class="py"&gt;conference.attendee.persona.goal&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Conduct a thorough conference search with the information (topics, conference dates) provided so I decide which to attend.&lt;/span&gt;
&lt;span class="py"&gt;conference.attendee.persona.backstory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;As a Conference Attendee Specialist, your mission is to find a suitable conference that matches the given request&lt;/span&gt;


&lt;span class="py"&gt;conference.speaker.llm.role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;best&lt;/span&gt;
&lt;span class="py"&gt;conference.speaker.persona.role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Conference speaker&lt;/span&gt;
&lt;span class="py"&gt;conference.speaker.persona.goal&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Conduct the search for the talks or create the talk with the given criteria with the conference application for those talks&lt;/span&gt;
&lt;span class="py"&gt;conference.speaker.persona.backstory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;As a Conference Speaker Specialist, your mission is to search for the talk or create the talk with the given criteria with the conference application for those talks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also link each role to the LLM model by referencing the previously defined LLM strategy (in our case, by the name &lt;em&gt;best&lt;/em&gt;). See the LLM definition once again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;embabel.models.default-llm&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.anthropic.claude-sonnet-4-6&lt;/span&gt;
&lt;span class="py"&gt;embabel.models.llms.best&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.anthropic.claude-sonnet-4-6&lt;/span&gt;
&lt;span class="py"&gt;embabel.models.llms.balanced&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.amazon.nova-pro-v1:0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our case, the &lt;em&gt;best&lt;/em&gt; LLM strategy will use the &lt;em&gt;us.anthropic.claude-sonnet-4-6&lt;/em&gt; model.&lt;br&gt;
Next, we defined &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/config/ConferenceConfig.java" rel="noopener noreferrer"&gt;ConferenceConfig&lt;/a&gt; to have access to the roles and actors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Validated&lt;/span&gt;
&lt;span class="nd"&gt;@ConfigurationProperties&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"conference"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ConferenceConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Actor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RoleGoalBackstory&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;attendee&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;   
    &lt;span class="nc"&gt;Actor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RoleGoalBackstory&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;speaker&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;em&gt;conference&lt;/em&gt; prefix, together with &lt;em&gt;Actor&lt;/em&gt; definitions, determines the mapping to the roles. For example, &lt;em&gt;Actor speaker&lt;/em&gt; matched the &lt;em&gt;conference.speaker&lt;/em&gt; role definition in the application.properties.&lt;/p&gt;

&lt;p&gt;Finally, we injected the &lt;em&gt;ConferenceConfig&lt;/em&gt; object (as config) into the constructor of each agent. &lt;/p&gt;

&lt;p&gt;Similarly, we split the initial prompt into other individual steps or actions for searching for conferences. See the &lt;em&gt;extractConferenceSearchRequest&lt;/em&gt; and &lt;em&gt;conferenceSearch&lt;/em&gt; agents in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/AbstractConferenceAgent.java" rel="noopener noreferrer"&gt;AbstractConferenceAgent&lt;/a&gt; class.  Also, the last step (applying for conferences with talks) looks similar. See the &lt;em&gt;applyForConference&lt;/em&gt; action in the same class. There are 2 things to notice here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We don't ask the LLM to split the prompt. This is because the input to this action is the output of 2 actions before: &lt;em&gt;talkSearch&lt;/em&gt; and &lt;em&gt;conferenceSearch&lt;/em&gt;, and 2 domain objects, &lt;em&gt;Domain.Conferences&lt;/em&gt; and &lt;em&gt;Domain.Talks&lt;/em&gt; respectively. Both become the input to the &lt;em&gt;PromptContributors&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;This agent is additionally annotated with the &lt;em&gt;AchievesGoal&lt;/em&gt; annotation to outline that this action execution is the final step that the agent should take in the workflow. See the complete code:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Action&lt;/span&gt;
&lt;span class="nd"&gt;@AchievesGoal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"apply for the conferences with their IDs and talk IDs and provide status of the application"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;applyForConference&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Conferences&lt;/span&gt; &lt;span class="n"&gt;conferences&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Talks&lt;/span&gt; &lt;span class="n"&gt;talks&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Ai&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;speaker&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;promptRunner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withPromptContributors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conferences&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;talks&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withToolGroup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMcpToolGroupByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"apply-to-conference"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Apply for the conference with the given criteria"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're completely done with the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/SearchForTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/a&gt; agent. &lt;/p&gt;

&lt;p&gt;The implementation of the agent &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/CreateTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;CreateTalksAndApplyForConferencesAgent&lt;/a&gt; looks very similar. The biggest difference is the first step: instead of searching for the existing talks, the talk is created. See the &lt;em&gt;extractTalkCreationRequest&lt;/em&gt; and &lt;em&gt;createTalks&lt;/em&gt; actions.&lt;/p&gt;

&lt;p&gt;Now come the biggest questions: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How is Embabel capable of identifying which agent to execute?&lt;/li&gt;
&lt;li&gt;How is Embabel capable of identifying the order of actions to execute for the individual agent? We only defined the last action to finally achieve the goal properly by annotation. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's build the application with &lt;code&gt;mvn clean package&lt;/code&gt; and run it locally with &lt;code&gt;mvn spring-boot:run&lt;/code&gt;. We see that Embabel starts the shell, and we can execute the prompt by running: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;x 'Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today. Also, provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference.'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Let's see what happens next: &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%2F2ld5oyy2lwipllhrapd8.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%2F2ld5oyy2lwipllhrapd8.png" alt=" " width="799" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Embabel identifies 2 agents, takes their description to compute the score, and chooses a concrete agent to execute based on the user input (prompt). I assume Embabel also uses an LLM for it. As a result, it computed: As a result, it computed :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SearchForExistingTalksAndApplyForConferencesAgent: 0,97&lt;/li&gt;
&lt;li&gt;CreateNewTalksAndApplyForConferencesAgent: 0,05&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, the &lt;em&gt;SearchForExistingTalksAndApplyForConferencesAgent&lt;/em&gt; agent is clearly a winner.&lt;/p&gt;

&lt;p&gt;Next, we see that Embabel formulates a plan to achieve the goal:&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%2Fmmd3fkkej5t3t37hz99c.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%2Fmmd3fkkej5t3t37hz99c.png" alt=" " width="796" height="97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sequence of agent actions to execute is a correct one :&lt;/p&gt;

&lt;p&gt;Embabel could also come up with the plan to first search for the conferences, then for the talks, and then to apply. Both plans are valid. How does Embabel create such a plan? The system, not the programmer, dynamically formulates plans. The system replans after the completion of each action, allowing it to adapt to new information as well as observe the effects of the previous action. This is effectively an OODA loop. Emabebl analyzes the input and output types of each agent that we defined. This also helps to formulate the plan. &lt;/p&gt;

&lt;p&gt;We see how Embabel executes those individual actions in the identified order. It logs all structured outputs of each action. The final output 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%2Fo3vwb5zwfdps6vbnkctj.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%2Fo3vwb5zwfdps6vbnkctj.png" alt=" " width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It found 2 talks and 1 conference to apply those talks that matched the criteria that we defined in the prompt. Finally, Embabel provides the statistics of the LLM used, the number of input (prompt) and output (completion) tokens spent. We also see the total cost of the agent execution. Embabel maintains the LLM pricing database. I don't expect this information to be very accurate, but it provides some indication. We also see the MCP tool names that the LLM used during the agent execution, their response times, and the number of failures.&lt;/p&gt;

&lt;p&gt;If we execute the prompt &lt;code&gt;x 'Please create a talk with a cool title (max 60 characters long) and description (max 300 characters long) about using Spring AI on the Amazon Bedrock AgentCore service. Then provide me with the list of conferences, including their IDs, with Java topics happening in 2026 and 2027, with the call for papers open today. Finally, for each conference, apply individually for it with the talk just created.'&lt;/code&gt;, we'll see that Embabel chooses to execute the &lt;em&gt;CreateNewTalksAndApplyForConferencesAgent&lt;/em&gt; agent :&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%2F8q4edk7uyuzhu22cbssy.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%2F8q4edk7uyuzhu22cbssy.png" alt=" " width="800" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It then formulates the plan and executes the actions to achieve the goal:&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%2Fwheywxj1ywnypnqznm66.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%2Fwheywxj1ywnypnqznm66.png" alt=" " width="800" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, it provides the same statistics, which we previously explained:&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%2Flszjugfl3ytuiubz5otd.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%2Flszjugfl3ytuiubz5otd.png" alt=" " width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are many more features that Embabel provides, like retries, which we'll explore in later articles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update from September 2026&lt;/strong&gt; Please read my article &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-4-upgrade-sample-e4o"&gt;Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore - Part 4 Upgrade sample application to Embabel 1.5&lt;/a&gt; to find out how to upgrade this sample application to Embabel version 1.5, which supports Spring Boot 4.x and Spring AI 2.x.&lt;/p&gt;

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

&lt;p&gt;In this article, we developed our first AI agents with the Embabel shell. We learned many concepts of this framework, such as agents, actions, and goals. In the next article, we'll slightly adjust our application to convert it into a web application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springai</category>
      <category>embabel</category>
      <category>bedrockagentcore</category>
    </item>
    <item>
      <title>Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore - Part 1 Introduction to the series and to the Embabel framework</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 20 Jul 2026 14:28:13 +0000</pubDate>
      <link>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-1-introduction-to-1ebl</link>
      <guid>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-1-introduction-to-1ebl</guid>
      <description>&lt;h2&gt;
  
  
  Introduction to the series and to the Embabel framework
&lt;/h2&gt;

&lt;p&gt;In my article series &lt;a href="https://dev.to/vkazulkin/series/37762"&gt;Building AI Agents with Spring AI and Amazon Bedrock AgentCore&lt;/a&gt;, we've already covered how to build AI Agents with Spring AI and Amazon Bedrock AgentCore. You'll need to go through at least its first 4 parts to be able to follow this article series. I'll reuse that sample application for this series.  Why might the abstractions provided by Spring AI frameworks alone not be enough? Let's look at the prompt that we sent to our application in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-4-provide-mcp-tools-for-2odf"&gt;Provide MCP tools for Conference application via AgentCore Gateway&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;"Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today. Also, provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference."&lt;/p&gt;

&lt;p&gt;The prompt is highly unstructured. We set here at least 3 pairs of requests to the LLM and their responses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request: "Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today". LLM responds to it.&lt;/li&gt;
&lt;li&gt;Request: "Provide me with the list of my talks with this topic in the title".  LLM responds to it.&lt;/li&gt;
&lt;li&gt;Request: "For each conference and talk retrieved, apply individually for the conference". This request means that the LLM uses its responses from the first 2 requests as the request and responds to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The thing is that we, as humans, can think logically, but not every LLM can break this prompt down into such individual pieces. So, it might require a strong and more expensive model to be able to do it. If we can automatically break down the problem into subtasks so the prompt will become structured, the LLM will have better chances at doing its job right. Spring AI has a function &lt;em&gt;CallResponseSpec.entity&lt;/em&gt; method for the synchronous LLM call to at least convert the response &lt;a href="https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html" rel="noopener noreferrer"&gt;to structured output&lt;/a&gt;. But again, this is not enough, as we need to break down the initial prompt into a set of structured subtasks first.&lt;/p&gt;

&lt;p&gt;The next challenge is that in the same article, we exposed 7 MCP tools through the Bedrock AgentCore Gateway. And we gave all of them to the LLM all at once. This is because the LLM can't respond to any request without using the correct tool. And there is always only 1 correct tool out of 7 per request. It not only blows up the content window, but there is also a high chance that the LLM will hallucinate. It may not only break down the prompt correctly, but may also select the wrong tool to respond to the request.&lt;/p&gt;

&lt;p&gt;Wouldn't it be better if we could break down the prompt as described above and give the LLM exactly the correct tool per request? See the parts &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-2-deploy-conference-search-2bo8"&gt;2&lt;/a&gt; and &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-4-provide-mcp-tools-for-2odf"&gt;4&lt;/a&gt; to see how we defined and exposed those MCP tools. For the first request, it will be the "conference search tool by topic, date, and call for papers open" tool. Then, for the second tool - "get talks by title substring" tool. For the third - "apply to conferences with conference ID and talk ID" tool.&lt;/p&gt;

&lt;p&gt;With Spring AI, we can filter the tools, as we explained in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-local-mcp-client-560a"&gt;part 3&lt;/a&gt;. But it's not enough as long as we can't break down the big prompt into individual subtasks, which may require suitable tool(s). If we implement prompt handling in a not structured way, it's not trivial to implement the tool filtering. This is because different prompts may require different tools depending on their content. &lt;/p&gt;

&lt;p&gt;The same challenges have another prompt from the same article: "Please create a talk with a cool title (max 60 characters long) and description (max 300 characters long) about using Spring AI on the Amazon Bedrock AgentCore service. Then provide me with the list of conferences, including their IDs, with Java topics happening in 2026 and 2027, with the call for papers open today. Finally, for each conference, apply individually for it with the talk just created."&lt;/p&gt;

&lt;p&gt;The only difference from the previous prompt is that we ask the LLM to create a new talk instead of extracting the existing talk(s). For this, the LLM will need to use the "create new talk" tool.&lt;/p&gt;

&lt;p&gt;Also, our application has 2 different actors. First, these are conference attendees who are interested in searching for and attending the conference. Second, these are the conference speakers who apply to the conference with their talk(s). Maybe those actors should use a different LLM model that suits each role the best.&lt;/p&gt;

&lt;p&gt;Welcome to the &lt;a href="https://github.com/embabel/embabel-agent" rel="noopener noreferrer"&gt;Embabel&lt;/a&gt;. It's a framework for authoring agentic flows on the JVM that seamlessly mixes LLM-prompted interactions with code and domain models. It supports intelligent finding the path towards goals. Embabel builds on the Spring AI framework. &lt;/p&gt;

&lt;p&gt;I give some links to the resources, which will help you to grasp the concepts of this framework quickly. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.embabel.com/embabel-agent/guide/1.0.0/" rel="noopener noreferrer"&gt;Embabel user guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@igor.dayen/embabel-agentic-ai-framework-release-1-0-0-new-features-332f375d0fba" rel="noopener noreferrer"&gt;Embabel Agentic AI Framework Release 1.0.0: New Features&lt;/a&gt; by Igor Dayen&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=_Y-srK-Ad4c" rel="noopener noreferrer"&gt;The AI Generation Is Coming of Age: Enterprise JVM Agents with Embabel &lt;/a&gt; by Rod Johnson&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=G5VDQCZu6t0" rel="noopener noreferrer"&gt;Developing AI Agents in Java with Embabel (Getting Started)&lt;/a&gt; by Dan Vega&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=2mGr7kdstJs" rel="noopener noreferrer"&gt;Embabel Tools &amp;amp; MCP Servers: Optimize Your Java AI Agents&lt;/a&gt; by Dan Vega&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll explore in the next articles that Embabel is capable of addressing and solving those challenges that we described above. Throughout this series, We'll improve the agent developed in the &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-4-provide-mcp-tools-for-2odf"&gt;Provide MCP tools for Conference application via AgentCore Gateway&lt;/a&gt; article using the Embabel framework. First, we'll use the Embabel shell, then the web application. You can already look at my GitHub &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/embabel-1.x-conference-app-agent-local" rel="noopener noreferrer"&gt;embabel-1.x-conference-app-agent-local&lt;/a&gt; repository for the example. &lt;/p&gt;

&lt;p&gt;The reasons I decided to create a separate series for using the Embabel framework are: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embabel recently became generally available (GA), and the current version is 1.0.0. I expect a lot of new functionality to be added soon. I don't want to mix the Embabel and Spring AI content, even though Embabel builds on Spring AI.&lt;/li&gt;
&lt;li&gt;Embabel currently doesn't support Spring Boot 4.x and Spring AI 2.x version. So, also here I'll need to update my examples when that version support is there.&lt;/li&gt;
&lt;li&gt;It's unclear whether it's possible to integrate &lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore" rel="noopener noreferrer"&gt;spring-ai-agentcore&lt;/a&gt; into Embabel. For example, &lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore/tree/main/spring-ai-agentcore-runtime-starter" rel="noopener noreferrer"&gt;spring-ai-agentcore-runtime-starter&lt;/a&gt;, which we demonstrated for Spring AI in the article &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-6-deploy-mcp-client-on-d4d"&gt;Deploy MCP client on AgentCore Runtime using Spring AI AgentCore Starter&lt;/a&gt;. And especially &lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore/tree/main/spring-ai-agentcore-memory" rel="noopener noreferrer"&gt;spring-ai-agentcore-memory&lt;/a&gt;, which we demonstrated in the articles &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-7-use-spring-ai-agentcore-fm9"&gt;Use Spring AI AgentCore short-term Memory for MCP client on AgentCore Runtime&lt;/a&gt; and &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-8-use-spring-ai-agentcore-3457"&gt;Use Spring AI AgentCore long-term Memory for MCP client on AgentCore Runtime&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springai</category>
      <category>embabel</category>
      <category>bedrockagentcore</category>
    </item>
    <item>
      <title>Building AI Agents with Spring AI and Amazon Bedrock AgentCore - Part 9 Configure AgentCore Observability</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 13 Jul 2026 12:03:51 +0000</pubDate>
      <link>https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-9-configure-agentcore-kb5</link>
      <guid>https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-9-configure-agentcore-kb5</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-6-deploy-mcp-client-on-d4d"&gt;part 6&lt;/a&gt;, we introduced our sample application &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime" rel="noopener noreferrer"&gt;spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime&lt;/a&gt; using Spring AI AgentCore. Later, in parts &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-7-use-spring-ai-agentcore-fm9"&gt;7&lt;/a&gt; and &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-8-use-spring-ai-agentcore-3457"&gt;8&lt;/a&gt;, we added AgentCore short- and long-term Memory, respectively, using Spring AI AgentCore Memory.&lt;/p&gt;

&lt;p&gt;In this part of the series, we'll explore how to configure &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability.html" rel="noopener noreferrer"&gt;AgentCore Observability&lt;/a&gt;. I've already covered this topic in my other article series when we used the Strands Agents SDK. I refer to my following articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-heroes/amazon-bedrock-agentcore-runtime-part-3-agentcore-observability-f08"&gt;AgentCore Runtime Observability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-heroes/amazon-bedrock-agentcore-gateway-part-4-agentcore-gateway-observability-2775"&gt;AgentCore Gateway Observability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-heroes/amazon-bedrock-agentcore-runtime-part-8-agentcore-memory-observability-32pc"&gt;AgentCore Memory Observability&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even though the CloudWatch Generative AI Observability service UI now looks different and has more capabilities, CloudWatch Logging and Monitoring configuration looks the same for our application and is provided out of the box. For the CloudWatch metrics, please review the article &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-service-provided.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore generated observability data&lt;/a&gt; for the current state of the exposed metrics. Also, make sure to enable Log delivery for Application and Usage Logs and Tracing for all AgentCore Services involved. In our case, these are: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runtime (2 of them, as we host the MCP server and the Agent there)&lt;/li&gt;
&lt;li&gt;Gateway&lt;/li&gt;
&lt;li&gt;Memory (both short- and long-term). &lt;/li&gt;
&lt;li&gt;Identity (please make sure to do so for the Runtime and Gateway Identity)&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%2Fjb00pz7fm9220p7ner5s.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%2Fjb00pz7fm9220p7ner5s.png" alt=" " width="799" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling AgentCore Tracing
&lt;/h2&gt;

&lt;p&gt;If we follow the steps described above and enable tracing for all AgentCore services in use, we'll only see the basic AgentCore metrics, but completely miss Sessions and Traces. The reason for this is that we provided the examples using the Strands Agents SDK. It works well with AgentCore Observability (baked by CloudWatch Generative AI Observability). We only had to add the dependency to &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-demos/blob/main/amazon-agentcore-runtime-to-gateway-demos/bedrock-agentcore-custom-agent/requirements.txt" rel="noopener noreferrer"&gt;aws-opentelemetry-distro&lt;/a&gt; and instrument our code, as shown below in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-demos/blob/main/amazon-agentcore-runtime-to-gateway-demos/bedrock-agentcore-custom-agent/Dockerfile" rel="noopener noreferrer"&gt;Dockerfile&lt;/a&gt;. Strands Agent has all the information on where to send the metrics and traces to the default OTEL provider, AWS CloudWatch. But how does it work for Java applications based on Spring AI and hosted on AgentCore Runtime?&lt;/p&gt;

&lt;p&gt;To view the metrics in CloudWatch Generative AI observability, we need to add the AWS Distro for OpenTelemetry (ADOT) SDK to our agent code. &lt;a href="https://aws-otel.github.io/" rel="noopener noreferrer"&gt;ADOT&lt;/a&gt; is a secure, production-ready, AWS-supported distribution of the OpenTelemetry project. Part of the Cloud Native Computing Foundation, OpenTelemetry provides open source APIs, libraries, and agents to collect distributed traces and metrics for application monitoring. With ADOT, we can instrument our applications just once to send correlated metrics and traces to multiple AWS and Partner monitoring solutions. In our case, we will send the metrics to the CloudWatch GenAI Observability service. &lt;/p&gt;

&lt;p&gt;AWS offers &lt;a href="https://docs.aws.amazon.com/xray/latest/devguide/xray-java-opentel-sdk.html" rel="noopener noreferrer"&gt;AWS Distro for OpenTelemetry Java&lt;/a&gt; with the AWS Distro for OpenTelemetry (ADOT). To get started, see the &lt;a href="https://aws-otel.github.io/docs/getting-started/java-sdk/auto-instr" rel="noopener noreferrer"&gt;AWS Distro for OpenTelemetry Java documentation&lt;/a&gt;. Let's add it to our sample application. To do so, we need to modify the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/agent/Dockerfile" rel="noopener noreferrer"&gt;Dockerfile&lt;/a&gt; of our sample application. We first have to download the &lt;em&gt;aws-opentelemetry-agent&lt;/em&gt; and then run it as the  Java agent to instrument the code on the fly:&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;ADD&lt;/span&gt;&lt;span class="s"&gt; https://github.com/aws-observability/aws-otel-java-instrumentation/releases/latest/download/aws-opentelemetry-agent.jar /opt/aws-opentelemetry-agent.jar&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; JAVA_TOOL_OPTIONS=-javaagent:/opt/aws-opentelemetry-agent.jar&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The documentation also says that the second component is required to receive the metrics and traces: the AWS Distro for OpenTelemetry Collector. In all the &lt;a href="https://aws-otel.github.io/docs/getting-started/collector" rel="noopener noreferrer"&gt;examples&lt;/a&gt; AWS provides, the collector is a sidecar application deployed with Docker Compose. Unfortunately, it's not possible to use Docker Compose for the AgentCore Runtime. We only provide the reference to the image in the &lt;a href="https://aws.amazon.com/ecr/" rel="noopener noreferrer"&gt;Amazon Elastic Container Registry&lt;/a&gt; (ECR) repository that the AgentCore Runtime pulls and runs for us.&lt;/p&gt;

&lt;p&gt;It took me a while to figure out how to achieve this, and I even created the &lt;a href="https://github.com/awslabs/agentcore-samples/issues/996" rel="noopener noreferrer"&gt;issue&lt;/a&gt; for it. There is a so-called collector-less &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-configure.html" rel="noopener noreferrer"&gt;Observability for the Amazon Bedrock AgentCore resources&lt;/a&gt;. As of now, unfortunately, not all parameters to be configured are described in this article. But I combined this information with the article &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLP-UsingADOT.html" rel="noopener noreferrer"&gt;Exporting collector-less telemetry using AWS Distro for OpenTelemetry (ADOT) SDK&lt;/a&gt; to achieve the goal:&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;ADD&lt;/span&gt;&lt;span class="s"&gt; https://github.com/aws-observability/aws-otel-java-instrumentation/releases/latest/download/aws-opentelemetry-agent.jar /opt/aws-opentelemetry-agent.jar&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; JAVA_TOOL_OPTIONS=-javaagent:/opt/aws-opentelemetry-agent.jar \&lt;/span&gt;
AGENT_OBSERVABILITY_ENABLED=true \
OTEL_RESOURCE_ATTRIBUTES=spring_ai_ac_conference_application_runtime,aws.log.group.names=
/aws/bedrock-agentcore/runtimes/spring_ai_ac_conference_application_runtime-a00QWV3i7t \
OTEL_EXPORTER_OTLP_LOGS_HEADERS=x-aws-log-group=/aws/bedrock-agentcore/runtimes/spring_ai_ac_conference_application_runtime-a00QWV3i7t,x-aws-log-stream=runtime-logs,x-aws-metric-namespace=bedrock-agentcore \
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf \
OTEL_TRACES_EXPORTER=otlp \
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://xray.us-east-1.amazonaws.com/v1/traces \
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/protobuf \
OTEL_LOGS_EXPORTER=otlp \
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs.us-east-1.amazonaws.com/v1/logs 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Besides the already described steps to download the &lt;em&gt;aws-opentelemetry-agent&lt;/em&gt; and run it as the Java agent to instrument the code, we configured the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AGENT_OBSERVABILITY_ENABLED=true  to indicate that we use Agent Observability and would like to view the traces in the CloudWatch Generative AI Observability and not in X-Ray.&lt;/li&gt;
&lt;li&gt;OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, and OTEL_EXPORTER_OTLP_LOGS_PROTOCOL to all be &lt;em&gt;http/protobuf&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;OTEL_EXPORTER_OTLP_TRACES_ENDPOINT and OTEL_EXPORTER_OTLP_LOGS_ENDPOINT as regional endpoints for traces and logs. If you deploy your application in another region other than us-east-1, you need to adjust the URL.&lt;/li&gt;
&lt;li&gt;OTEL_RESOURCE_ATTRIBUTES to be service.name=spring_ai_ac_conference_application_runtime,aws.log.group.names=/aws/bedrock-agentcore/runtimes/spring_ai_ac_conference_application_runtime-a00QWV3i7t. Please adjust &lt;em&gt;service.name&lt;/em&gt; value to how you named the service in AgentCore Runtime. I called it &lt;em&gt;spring_ai_ac_conference_application_runtime&lt;/em&gt;. For the suffix of the &lt;em&gt;aws.log.group.names&lt;/em&gt; use your AgentCore Runtime ID (in my case &lt;em&gt;spring_ai_ac_conference_application_runtime-a00QWV3i7t&lt;/em&gt;). AWS Log Group Name for AgentCore Runtime always follows the pattern: /aws/bedrock-agentcore/runtimes/{RUNTIME_ID}.&lt;/li&gt;
&lt;li&gt;OTEL_EXPORTER_OTLP_LOGS_HEADERS to be x-aws-log-group=/aws/bedrock-agentcore/runtimes/spring_ai_ac_conference_application_runtime-a00QWV3i7t,x-aws-log-stream=runtime-logs,x-aws-metric-namespace=bedrock-agentcore. The same as above: for the suffix of the &lt;em&gt;x-aws-log-group&lt;/em&gt; use your AgentCore Runtime ID again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, after rebuilding and redeploying the application, we can see similar metrics and traces as provided in the articles using Strands Agents SDK above. There are, of course, some differences in the collected metadata. This is because we use the AWS OpenTelemetry Agent distribution for Java and not for Python, as in the articles above. Here are some selected screenshots taken from the &lt;a href="https://us-east-1.console.aws.amazon.com/cloudwatch/home?region=us-east-1#/gen-ai-observability/agent-core/agents" rel="noopener noreferrer"&gt;CloudWatch GenAI Observability: Bedrock AgentCore Observability&lt;/a&gt; for the prompt "Please provide me with the list of conferences, including their IDs, with the Java topic happening in 2027, with the call for papers open today. Also, provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference.", which I sent :&lt;/p&gt;

&lt;p&gt;All sessions view:&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%2Fgg5g2l900qst89pj39kf.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%2Fgg5g2l900qst89pj39kf.png" alt=" " width="800" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All traces view:&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%2Fgmo9qbj2ksnu3djt54ht.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%2Fgmo9qbj2ksnu3djt54ht.png" alt=" " width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All spans view:&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%2F2wpfn4mvkblgq57dhck7.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%2F2wpfn4mvkblgq57dhck7.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Session ID view:&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%2F71rusfuxak2tq5emdije.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%2F71rusfuxak2tq5emdije.png" alt=" " width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tree view of the Trace ID:&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%2Fygkhq1bp6ct2sixqofea.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%2Fygkhq1bp6ct2sixqofea.png" alt=" " width="800" height="439"&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%2F9oln85fpfw1h2gjlvejo.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%2F9oln85fpfw1h2gjlvejo.png" alt=" " width="683" height="657"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Trajectory view of the Trace ID: &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%2F5o0w2lbddxzgn3ohxzwt.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%2F5o0w2lbddxzgn3ohxzwt.png" alt=" " width="799" height="495"&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%2F2dvbhhwvrktl4aiymwzt.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%2F2dvbhhwvrktl4aiymwzt.png" alt=" " width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Timeline view of the Trace ID: &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%2Fdq51ifxvc9layjwit1lz.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%2Fdq51ifxvc9layjwit1lz.png" alt=" " width="800" height="623"&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%2Fjwlabev7kwf7ivcyf1z8.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%2Fjwlabev7kwf7ivcyf1z8.png" alt=" " width="799" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is how to enable observability for Amazon Bedrock AgentCore Runtime hosted agents. If you don't host your application on AgentCore, you can still use CloudWatch to &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-get-started.html#enabling-observability-non-runtime-hosted" rel="noopener noreferrer"&gt;enable observability for non-Amazon Bedrock AgentCore-hosted agents&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;In this article, we described how to configure AgentCore Observability in a collector-less way. This involves running the Java &lt;em&gt;aws-opentelemetry-agent&lt;/em&gt; agent to instrument the code and set a bunch of environment variables in the Dockerfile. Please also make sure you have activated logging and tracing for both AgentCore Gateway and Runtime. &lt;/p&gt;

&lt;p&gt;In the next article, we'll explore how to configure the same AgentCore observability with the help of &lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore/tree/main/spring-ai-agentcore-otel-extension" rel="noopener noreferrer"&gt;spring-ai-agentcore-otel-extension&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my other series on &lt;a href="https://dev.to/vkazulkin/series/41348"&gt;Building AI Agents with Embabel, Spring AI, and Amazon Bedrock AgentCore&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springai</category>
      <category>bedrockagentcore</category>
      <category>observability</category>
    </item>
    <item>
      <title>Building AI Agents with Spring AI and Amazon Bedrock AgentCore - Part 8 Use Spring AI AgentCore long-term Memory for MCP client on AgentCore Runtime</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 06 Jul 2026 14:07:04 +0000</pubDate>
      <link>https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-8-use-spring-ai-agentcore-3457</link>
      <guid>https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-8-use-spring-ai-agentcore-3457</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-7-use-spring-ai-agentcore-fm9"&gt;part 7&lt;/a&gt;, we explained how to add and use AgentCore short-term Memory to our application with the help of Spring AI AgentCore Memory. In this article, we'll use &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/long-term-memory-long-term.html" rel="noopener noreferrer"&gt;AgentCore long-term Memory&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;We'll once again build on the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime" rel="noopener noreferrer"&gt;spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime&lt;/a&gt; sample application, which we introduced in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-6-deploy-mcp-client-on-d4d"&gt;part 6&lt;/a&gt;. Please review part 7 to better understand the base concept of Spring AI AgentCore Memory and how to add it to our application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create Bedrock AgentCore long-term Memory
&lt;/h2&gt;

&lt;p&gt;I've written the article &lt;a href="https://dev.to/aws-heroes/amazon-bedrock-agentcore-runtime-part-7-using-agentcore-long-term-memory-with-strands-agents-sdk-lb2"&gt;Amazon Bedrock AgentCore Runtime - Part 7 Using AgentCore long-term Memory with Strands Agents SDK&lt;/a&gt; about what AgentCore long-term Memory is and how to create one with the Python SDK. Also provided an example of how to use it with &lt;a href="https://strandsagents.com/" rel="noopener noreferrer"&gt;Strands Agent SDK&lt;/a&gt;. I refer to this article to understand the basics.&lt;/p&gt;

&lt;p&gt;To run examples, we've already deployed our &lt;a href="[spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime](https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime)"&gt;sample application&lt;/a&gt; on AgentCore Runtime by executing the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/cdk/src/main/java/dev/vkazulkin/agentcore/runtime/RuntimeWithMCPStack.java" rel="noopener noreferrer"&gt;RuntimeWithMCPStack&lt;/a&gt; stack in part 6. Next, let's create AgentCore long-term Memory with CDK for Java by executing the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/cdk/src/main/java/dev/vkazulkin/agentcore/memory/LongTermMemoryStack.java" rel="noopener noreferrer"&gt;LongTermMemoryStack&lt;/a&gt; stack. First, let's look at what is happening there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LongTermMemoryStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;LongTermMemoryStack&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Construct&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;appName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="nc"&gt;StackProps&lt;/span&gt; &lt;span class="n"&gt;stackProps&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stackProps&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"long-term-memory"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;memoryName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"long_term_memory_for_conference_application"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Long-Term Memory for Conference Application"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expirationDuration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;memoryStrategies&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MemoryStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;usingBuiltInSummarization&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                      &lt;span class="nc"&gt;MemoryStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;usingBuiltInSemantic&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                 

    &lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"LongTermMemoryIdOutput"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMemoryId&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;           
    &lt;span class="o"&gt;}&lt;/span&gt;  
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use Bedrock AgentCore &lt;em&gt;Memory.Builder&lt;/em&gt; to set the memory name, description, expiration duration, and then create the memory. By defining the memory strategies, we outline that we'll create the AgentCore long-term memory. We used the &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/long-term-configuring-built-in-strategies.html#long-term-semantic-facts-strategy" rel="noopener noreferrer"&gt;semantic&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/long-term-configuring-built-in-strategies.html" rel="noopener noreferrer"&gt;summarization&lt;/a&gt; built-in memory strategies. You can also set the currently supported &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/long-term-configuring-built-in-strategies.html#long-term-user-preferences-strategy" rel="noopener noreferrer"&gt;user preference&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/long-term-configuring-built-in-strategies.html#long-term-session-episodic-strategy" rel="noopener noreferrer"&gt;episodic&lt;/a&gt;  memory strategies instead. The &lt;em&gt;MemoryStrategy&lt;/em&gt; class offers &lt;em&gt;usingBuiltInUserPreference&lt;/em&gt; and &lt;em&gt;usingBuiltInEpisodic&lt;/em&gt; methods for this purpose. You can also configure the &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/long-term-configuring-custom-strategies.html" rel="noopener noreferrer"&gt;Custom Memory Strategy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can deploy the stack with the command: &lt;code&gt;cdk deploy spring-ai-ac-conference-application-lt-memory-stack -c awsAccountId={YOUR_AWS_ACCOUNT_ID}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is how the created long-term memory looks in the AgentCore Memory UI:&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%2Fmeuo39e4qxbu1qwybp02.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%2Fmeuo39e4qxbu1qwybp02.png" alt=" " width="799" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this is how the default namespaces look: &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%2Fwqsw4yqi05mtwu5xs74u.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%2Fwqsw4yqi05mtwu5xs74u.png" alt=" " width="800" height="162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're not satisfied with the built-in configuration, for example, default namespaces, you can set your own. Here is an example of how to create a semantic memory strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt; &lt;span class="nc"&gt;MemoryStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;usingSemantic&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ManagedStrategyProps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"namespace1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"namespace2"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other memory strategies work the same; just use the corresponding &lt;em&gt;using*&lt;/em&gt; method. But I always start with the built-in memory strategies.&lt;/p&gt;

&lt;p&gt;Also, the Memory ID will be printed out, which we will need to configure in our Spring AI application. We can find the same Memory ID in the service UI above.&lt;/p&gt;

&lt;p&gt;Finally, we need to configure the following IAM permissions to allow our application running on AgentCoreRuntime to access this AgentCore Memory:&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BedrockAgentCoreLongTermMemory"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&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="s2"&gt;"bedrock-agentcore:ListEvents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:CreateEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:RetrieveMemoryRecords"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:GetMemory"&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;"Resource"&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="s2"&gt;"arn:aws:bedrock-agentcore:{YOUR_AWS_REGION}:{YOUR_AWS_ACCOUNT_ID}:memory/{YOUR_LONG_TERM_MEMORY_ID}"&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;
  
  
  Configure Bedrock AgentCore long-term Memory in our sample application
&lt;/h2&gt;

&lt;p&gt;To configure Bedrock AgentCore long-term Memory in our sample application, we need to make some changes to it. First, we need to configure some long-term memory-related properties in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/agent/src/main/resources/application.properties" rel="noopener noreferrer"&gt;application.properties&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;agentcore.memory.memory-id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;{YOUR_LONG_TERM_MEMORY_ID}&lt;/span&gt;
&lt;span class="py"&gt;agentcore.memory.long-term.auto-discovery&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first required property is the AgentCore Memory ID we just created. By setting &lt;em&gt;agentcore.memory.long-term.auto-discovery&lt;/em&gt; to true, we use the recommended &lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore/tree/main/spring-ai-agentcore-memory#option-1-autodiscovery-recommended" rel="noopener noreferrer"&gt;AgentCore long-term Memory autodiscovery option&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Autodiscovery behavior is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queries AWS to discover all strategies configured in your memory&lt;/li&gt;
&lt;li&gt;Creates advisors only for supported types: SEMANTIC, SUMMARIZATION, USER_PREFERENCE, EPISODIC&lt;/li&gt;
&lt;li&gt;Skips CUSTOM strategy types (not supported by autodiscovery)&lt;/li&gt;
&lt;li&gt;Uses the first namespace if a strategy has multiple namespaces&lt;/li&gt;
&lt;li&gt;Uses &lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore/tree/main/spring-ai-agentcore-memory#defaults-summary" rel="noopener noreferrer"&gt;default topK values&lt;/a&gt; for each strategy type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can also override the specific settings for discovered strategies by providing explicit configuration. See the link above to find out how to do this. Another option is to use the &lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore/tree/main/spring-ai-agentcore-memory#option-2-explicit-configuration" rel="noopener noreferrer"&gt;AgentCore long-term Memory explicit configuration option&lt;/a&gt;, in which we need to specify each strategy manually.&lt;/p&gt;

&lt;p&gt;Next, we need to ensure that we set &lt;em&gt;ChatMemory&lt;/em&gt; to the &lt;em&gt;ChatClient&lt;/em&gt; in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/agent/src/main/java/dev/vkazulkin/agent/controller/SpringAIAgentController.java" rel="noopener noreferrer"&gt;SpringAIAgentController&lt;/a&gt;. I'll provide the generic constructor, capable of dealing with no AgentCore Memory configured or short-term or long-term memory configured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SpringAIAgentController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
&lt;span class="nc"&gt;ChatMemory&lt;/span&gt; &lt;span class="n"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AgentCoreLongTermMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ltmAdvisors&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ToolCallingChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"us.anthropic.claude-sonnet-4-6"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

   &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;      
           &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultAdvisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAllMemoryAdvisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ltmAdvisors&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;          
&lt;span class="o"&gt;....&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Advisor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getAllMemoryAdvisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatMemory&lt;/span&gt; &lt;span class="n"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AgentCoreLongTermMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ltmAdvisors&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; 
   &lt;span class="nc"&gt;Advisor&lt;/span&gt; &lt;span class="n"&gt;chatMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MessageChatMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cltmAdvisors&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Advisor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;)&lt;/span&gt; &lt;span class="n"&gt;ltmAdvisors&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;allAdvisors&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;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Advisor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;();&lt;/span&gt;
   &lt;span class="n"&gt;allAdvisors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cltmAdvisors&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;allAdvisors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;allAdvisors&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Short-term Spring AI AgentCore Memory implements the Spring AI &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat-memory.html" rel="noopener noreferrer"&gt;ChatMemoryRepository&lt;/a&gt; interface. This doesn't work for long-term Spring AI AgentCore Memory. For this, we inject the list of &lt;em&gt;AgentCoreLongTermMemoryAdvisor&lt;/em&gt;. Then we use the &lt;a href="https://docs.spring.io/spring-ai/reference/api/advisors.html" rel="noopener noreferrer"&gt;Advisors API&lt;/a&gt; to build the complete list of Advisors in the &lt;em&gt;getAllMemoryAdvisors&lt;/em&gt; method. We use &lt;em&gt;MessageChatMemoryAdvisor&lt;/em&gt; with the &lt;em&gt;ChatMemory&lt;/em&gt; to build the short-term memory advisor, and then we add the long-term advisors to the list. We then provide this complete list of memory advisors as an input to the &lt;em&gt;defaultAdvisors&lt;/em&gt; method of the &lt;em&gt;ChatClient.Builder&lt;/em&gt;. Even if we don't configure the AgentCore short-term or long-term Memory (or both), the same code still works without throwing any exceptions. &lt;em&gt;MessageChatMemoryAdvisor&lt;/em&gt;, and &lt;em&gt;List of AgentCoreLongTermMemoryAdvisors&lt;/em&gt; or both will be &lt;em&gt;null&lt;/em&gt; in such a case, which Spring AI treats the same way as not setting any advisors. That's all the changes we need to make to our application to use the AgentCore long-term memory. &lt;/p&gt;

&lt;p&gt;The last step is exactly the same as for the short-term memory: to include a custom &lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore/tree/main/spring-ai-agentcore-memory#conversation-id-format" rel="noopener noreferrer"&gt;ChatMemory Conversation ID&lt;/a&gt; in the &lt;em&gt;ChatClient&lt;/em&gt;.  According to the &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat-memory.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, &lt;em&gt;ChatMemory.CONVERSATION_ID&lt;/em&gt; parameter is required for all memory advisors. Calls that omit this parameter will throw an IllegalArgumentException at runtime, as there is no default conversation ID. &lt;br&gt;
The Spring AI AgentCore long-term Memory supports flexible conversation ID formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple: user123 → actor: user123, session: default-session&lt;/li&gt;
&lt;li&gt;With Session: user123:session456 → actor: user123, session: session456&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how the code looks for it in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/agent/src/main/java/dev/vkazulkin/agent/controller/SpringAIAgentController.java" rel="noopener noreferrer"&gt;SpringAIAgentController&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;CONVERSATION_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"default-actor-id-12345678:default-session-id-12345678"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@AgentCoreInvocation&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Flux&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;invoceAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PromptRequest&lt;/span&gt; &lt;span class="n"&gt;promptRequest&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AgentCoreContext&lt;/span&gt; &lt;span class="n"&gt;agentCoreContext&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&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;getAuthTokenViaHttpClient&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;McpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getMcpClientTransport&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;asyncMcpToolCallbackProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AsyncMcpToolCallbackProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mcpClients&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;


    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promptRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;advisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatMemory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CONVERSATION_ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CONVERSATION_ID&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tools&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;DateTimeTools&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;&lt;span class="n"&gt;asyncMcpToolCallbackProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getToolCallbacks&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As shown above, we defined a static CONVERSATION_ID. However, if you'd like to use individual IDs depending on the actor or user providing the prompt, you can add the login functionality and set the individual user ID. Finally, we set the value of the conversation ID as the parameter of the memory advisor.&lt;/p&gt;

&lt;p&gt;Then we need to rebuild the Docker image of our application and deploy it to the Amazon ECR. After it, we need to configure the correct &lt;em&gt;ecrImageURI&lt;/em&gt; in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/cdk/cdk.json" rel="noopener noreferrer"&gt;cdk.json&lt;/a&gt;. We covered those concepts in parts &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-2-deploy-conference-search-2bo8"&gt;2&lt;/a&gt; and &lt;a href=""&gt;4&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, we need to redeploy the AgentCore Runtime stack with the command: &lt;code&gt;cdk deploy spring-ai-ac-conference-application-agentcore-runtime-stack -c awsAccountId={YOUR_AWS_ACCOUNT_ID}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, we can use the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/agent/src/main/java/dev/vkazulkin/agent/sdk/InvokeRuntimeAgent.java" rel="noopener noreferrer"&gt;InvokeRuntimeAgent&lt;/a&gt; class to send prompts to our agent running on AgentCore Runtime. We described this in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-5-deploy-mcp-client-for-1n11"&gt;part 5&lt;/a&gt; and can reuse these prompts to apply the talks to the conferences. &lt;/p&gt;

&lt;p&gt;But now, similarly to the example of the short-term memory in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-7-use-spring-ai-agentcore-fm9"&gt;part 7&lt;/a&gt;, we can ask such questions as: "You recently applied for some conferences for me. Can you provide me with the details?" The agent will give us a reply, which shows that it provided the answer using the AgentCore long-term Memory:&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%2Fapb93yw9o8i2f5e7u7s8.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%2Fapb93yw9o8i2f5e7u7s8.png" alt=" " width="773" height="888"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reply is very similar to one provided in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-7-use-spring-ai-agentcore-fm9"&gt;part 7&lt;/a&gt; for the long-term memory.&lt;/p&gt;

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

&lt;p&gt;In this article, we explained how to add and use AgentCore long-term Memory to our application with the help of Spring AI AgentCore Memory. In the next article, we'll add &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability.html" rel="noopener noreferrer"&gt;AgentCore Observability&lt;/a&gt; to our application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my other series on &lt;a href="https://dev.to/vkazulkin/series/41348"&gt;Building AI Agents with Embabel, Spring AI, and Amazon Bedrock AgentCore&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>java</category>
      <category>springai</category>
      <category>bedrockagentcore</category>
    </item>
    <item>
      <title>Building AI Agents with Spring AI and Amazon Bedrock AgentCore - Part 7 Use Spring AI AgentCore short-term Memory for MCP client on AgentCore Runtime</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 29 Jun 2026 14:00:35 +0000</pubDate>
      <link>https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-7-use-spring-ai-agentcore-fm9</link>
      <guid>https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-7-use-spring-ai-agentcore-fm9</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-6-deploy-mcp-client-on-d4d"&gt;part 6&lt;/a&gt;, we used Spring AI AgentCore Starter in our sample application to deploy and run our MCP client on AgentCore Runtime. We used several useful features of the Spring AI AgentCore Starter, like annotation-based auto-configuration, smart health checks, and rate limiting.&lt;/p&gt;

&lt;p&gt;Until now, our agent deployed on AgentCore Runtime was stateless. If we apply for conferences with some talks and then ask the agent in the next conversation about which talks we applied for which conferences, the agent won't be able to answer. In this article, we'll add the &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html" rel="noopener noreferrer"&gt;AgentCore Memory&lt;/a&gt; to our application. We'll start with the short-term memory and later add the long-term memory. Then, we'll show how to use &lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore/tree/main/spring-ai-agentcore-memory" rel="noopener noreferrer"&gt;Spring AI AgentCore Memory&lt;/a&gt;, which provides the Spring AI ChatMemory integration with the Amazon Bedrock AgentCore Memory service.&lt;/p&gt;

&lt;p&gt;We'll build on the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime" rel="noopener noreferrer"&gt;spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime&lt;/a&gt; sample application, which we introduced in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-6-deploy-mcp-client-on-d4d"&gt;part 6&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring AI AgentCore Memory
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore/tree/main/spring-ai-agentcore-memory" rel="noopener noreferrer"&gt;Spring AI AgentCore Memory&lt;/a&gt; provides &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat-memory.html" rel="noopener noreferrer"&gt;Spring AI ChatMemory&lt;/a&gt; integration with the &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Memory&lt;/a&gt; service.&lt;br&gt;
Its features currently include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring AI Integration: Implements ChatMemoryRepository interface
&lt;/li&gt;
&lt;li&gt;Auto-configuration: Zero-configuration setup with Spring Boot&lt;/li&gt;
&lt;li&gt;Short-Term Memory: Conversation history with MessageWindowChatMemory&lt;/li&gt;
&lt;li&gt;Long-Term Memory: 4 consolidation strategies (Semantic, User Preference, Summary, Episodic)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To use Spring AI AgentCore Memory in our application, we need to add one more dependency to the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/agent/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springaicommunity&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-ai-agentcore-memory&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As stated above, in this article, we'll cover short-term memory, leaving the long-term memory to the next article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create Bedrock AgentCore short-term Memory
&lt;/h2&gt;

&lt;p&gt;I've written the article &lt;a href="https://dev.to/aws-heroes/amazon-bedrock-agentcore-runtime-part-6-using-agentcore-short-term-memory-with-strands-agents-sdk-55d4"&gt;Amazon Bedrock AgentCore Runtime - Part 6 Using AgentCore short-term Memory with Strands Agents SDK&lt;/a&gt; about what AgentCore short-term Memory is and how to create one with the Python SDK. Also provided an example of how to use it with the &lt;a href="https://strandsagents.com/" rel="noopener noreferrer"&gt;Strands Agent SDK&lt;/a&gt;. I refer to this article to understand the basics.&lt;/p&gt;

&lt;p&gt;To run examples, we've already deployed our &lt;a href="[spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime](https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime)"&gt;sample application&lt;/a&gt; on AgentCore Runtime by executing the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/cdk/src/main/java/dev/vkazulkin/agentcore/runtime/RuntimeWithMCPStack.java" rel="noopener noreferrer"&gt;RuntimeWithMCPStack&lt;/a&gt; stack in part 6. Next, let's create AgentCore short-term Memory with CDK for Java by executing the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/cdk/src/main/java/dev/vkazulkin/agentcore/memory/ShortTermMemoryStack.java" rel="noopener noreferrer"&gt;ShortTermMemoryStack&lt;/a&gt; stack. First, let's look at what is happening there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShortTermMemoryStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

 &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ShortTermMemoryStack&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Construct&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;appName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="nc"&gt;StackProps&lt;/span&gt; &lt;span class="n"&gt;stackProps&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stackProps&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"short-term-memory-1"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;memoryName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"short_term_memory_for_conference_application"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Short-Term Memory for Conference Application"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expirationDuration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   

    &lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ShortTermMemoryIdOutput"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMemoryId&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;           
    &lt;span class="o"&gt;}&lt;/span&gt;  
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use Bedrock AgentCore &lt;em&gt;Memory.Builder&lt;/em&gt; to set the memory name, description and expiration duration, and then create the memory. &lt;/p&gt;

&lt;p&gt;You can deploy the stack with the command: &lt;code&gt;cdk deploy spring-ai-ac-conference-application-st-memory-stack -c awsAccountId={YOUR_AWS_ACCOUNT_ID}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As we don't set any memory strategy, the short-term memory will be created 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.amazonaws.com%2Fuploads%2Farticles%2Fvuoxh5g9ibejsygmjv66.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%2Fvuoxh5g9ibejsygmjv66.png" alt=" " width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, the Memory ID will be printed out, which we will need to configure in our Spring AI application. We can find the same Memory ID in the service UI above.&lt;/p&gt;

&lt;p&gt;Finally, we need to configure the following IAM permissions to allow our application running on AgentCoreRuntime to access this AgentCore Memory:&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BedrockAgentCoreShortTermMemory"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&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="s2"&gt;"bedrock-agentcore:ListEvents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:CreateEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:RetrieveMemoryRecords"&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;"Resource"&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="s2"&gt;"arn:aws:bedrock-agentcore:{YOUR_AWS_REGION}:{YOUR_AWS_ACCOUNT_ID}:memory/{YOUR_SHORT_TERM_MEMORY_ID}"&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;
  
  
  Configure Bedrock AgentCore short-term Memory in our sample application
&lt;/h2&gt;

&lt;p&gt;To configure Bedrock AgentCore short-term Memory in our sample application, we need to make some changes to it. First, we need to configure some short-term memory-related properties in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/agent/src/main/resources/application.properties" rel="noopener noreferrer"&gt;application.properties&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;agentcore.memory.memory-id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;{YOUR_SHORT_TERM_MEMORY_ID}&lt;/span&gt;
&lt;span class="py"&gt;agentcore.memory.total-events-limit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10 #Max events to retrieve (context window), default 100&lt;/span&gt;
&lt;span class="py"&gt;agentcore.memory.default-session&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default-session-id-12345678 #Default session name, default name is default-session&lt;/span&gt;
&lt;span class="py"&gt;agentcore.memory.page-size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;50  #API pagination size, default value is 50&lt;/span&gt;
&lt;span class="py"&gt;agentcore.memory.ignore-unknown-roles&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false #Handle unknown message roles, default value is false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only required property is the AgentCore Memory ID we just created. All others are optional, with default values described in the &lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore/tree/main/spring-ai-agentcore-memory#configuration-reference" rel="noopener noreferrer"&gt;Spring AI AgentCore Memory Configuration Reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, we need to ensure that we set &lt;em&gt;ChatMemory&lt;/em&gt; to the &lt;em&gt;ChatClient&lt;/em&gt; in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/agent/src/main/java/dev/vkazulkin/agent/controller/SpringAIAgentController.java" rel="noopener noreferrer"&gt;SpringAIAgentController&lt;/a&gt;. We've already set it up like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SpringAIAgentController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ChatMemory&lt;/span&gt; &lt;span class="n"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ToolCallingChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
   &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"us.anthropic.claude-sonnet-4-6"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
   &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
          &lt;span class="c1"&gt;//short term memory         &lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultAdvisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MessageChatMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;          
&lt;span class="o"&gt;....&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Short-term Spring AI AgentCore Memory implements the Spring AI &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat-memory.html" rel="noopener noreferrer"&gt;ChatMemoryRepository&lt;/a&gt; interface. That's why, when we provide the memory configuration as above, Spring AI injects the correct implementation of the &lt;em&gt;ChatMemory&lt;/em&gt; into the constructor. Then we use the &lt;a href="https://docs.spring.io/spring-ai/reference/api/advisors.html" rel="noopener noreferrer"&gt;Advisors API&lt;/a&gt; to build &lt;em&gt;MessageChatMemoryAdvisor&lt;/em&gt; with the &lt;em&gt;ChatMemory&lt;/em&gt; and to provide it as an input to the &lt;em&gt;defaultAdvisors&lt;/em&gt; method of the &lt;em&gt;ChatClient.Builder&lt;/em&gt;. Even if we don't configure the AgentCore short-term Memory, the same code still works without throwing any exceptions. &lt;em&gt;MessageChatMemoryAdvisor&lt;/em&gt; will be &lt;em&gt;null&lt;/em&gt;, which Spring AI treats the same way as not setting any advisors. That's all the changes we need to make to our application to use the AgentCore short-term memory. &lt;/p&gt;

&lt;p&gt;In addition, we need to include a custom &lt;a href="https://github.com/spring-ai-community/spring-ai-agentcore/tree/main/spring-ai-agentcore-memory#conversation-id-format" rel="noopener noreferrer"&gt;ChatMemory Conversation ID&lt;/a&gt; in the &lt;em&gt;ChatClient&lt;/em&gt;.  According to the &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat-memory.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, the &lt;em&gt;ChatMemory.CONVERSATION_ID&lt;/em&gt; parameter is required for all memory advisors. Calls that omit this parameter will throw an IllegalArgumentException at runtime, as there is no default conversation ID. &lt;br&gt;
The Spring AI AgentCore long-term Memory supports flexible conversation ID formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple: user123 → actor: user123, session: default-session&lt;/li&gt;
&lt;li&gt;With Session: user123:session456 → actor: user123, session: session456&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how the code looks for it in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/agent/src/main/java/dev/vkazulkin/agent/controller/SpringAIAgentController.java" rel="noopener noreferrer"&gt;SpringAIAgentController&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;CONVERSATION_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"default-actor-id-12345678:default-session-id-12345678"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@AgentCoreInvocation&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Flux&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;invoceAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PromptRequest&lt;/span&gt; &lt;span class="n"&gt;promptRequest&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AgentCoreContext&lt;/span&gt; &lt;span class="n"&gt;agentCoreContext&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&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;getAuthTokenViaHttpClient&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;McpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getMcpClientTransport&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;asyncMcpToolCallbackProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AsyncMcpToolCallbackProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mcpClients&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promptRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;advisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatMemory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CONVERSATION_ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CONVERSATION_ID&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tools&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;DateTimeTools&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;&lt;span class="n"&gt;asyncMcpToolCallbackProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getToolCallbacks&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As shown above, we defined a static CONVERSATION_ID. However, if you'd like to use individual IDs depending on the actor or user providing the prompt, you can add the login functionality and set the individual user ID. Finally, we set the value of the conversation ID as the parameter of the memory advisor.&lt;/p&gt;

&lt;p&gt;Then we need to rebuild the Docker image of our application and deploy it to the Amazon ECR. After it, we need to configure the correct &lt;em&gt;ecrImageURI&lt;/em&gt; in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/cdk/cdk.json" rel="noopener noreferrer"&gt;cdk.json&lt;/a&gt;. We covered those concepts in parts &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-2-deploy-conference-search-2bo8"&gt;2&lt;/a&gt; and &lt;a href=""&gt;4&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, we need to redeploy the AgentCore Runtime stack with the command: &lt;code&gt;cdk deploy spring-ai-ac-conference-application-agentcore-runtime-stack -c awsAccountId={YOUR_AWS_ACCOUNT_ID}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, we can use the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/spring-ai-2.0-ac-conference-app-agent-bedrock-agentcore-runtime/agent/src/main/java/dev/vkazulkin/agent/sdk/InvokeRuntimeAgent.java" rel="noopener noreferrer"&gt;InvokeRuntimeAgent&lt;/a&gt; class to send prompts to our agent running on AgentCore Runtime. We described this in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-5-deploy-mcp-client-for-1n11"&gt;part 5&lt;/a&gt; and can reuse these prompts to apply the talks to the conferences. &lt;/p&gt;

&lt;p&gt;But now, additionally, we can ask such questions as: "You recently applied for some conferences for me. Can you provide me with the details?" The agent will give us a reply similar to the reply shown below, which shows that it provided the answer using the AgentCore short-term Memory:&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%2Frhfo2i7f2m96h0ibhbza.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%2Frhfo2i7f2m96h0ibhbza.png" alt=" " width="789" height="730"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;In this article, we explained how to add and use AgentCore short-term Memory to our application with the help of Spring AI AgentCore Memory. In the next article, we'll use AgentCore long-term Memory instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my other series on &lt;a href="https://dev.to/vkazulkin/series/41348"&gt;Building AI Agents with Embabel, Spring AI, and Amazon Bedrock AgentCore&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>java</category>
      <category>springai</category>
      <category>bedrockagentcore</category>
    </item>
  </channel>
</rss>
