<?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: Fazalul Haque</title>
    <description>The latest articles on DEV Community by Fazalul Haque (@vfazal).</description>
    <link>https://dev.to/vfazal</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%2F3940071%2F6f71ed19-adcf-456b-87d2-facddcae01e2.png</url>
      <title>DEV Community: Fazalul Haque</title>
      <link>https://dev.to/vfazal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vfazal"/>
    <language>en</language>
    <item>
      <title>Why Senior Data Engineers Write SQL Differently</title>
      <dc:creator>Fazalul Haque</dc:creator>
      <pubDate>Mon, 10 Aug 2026 05:49:19 +0000</pubDate>
      <link>https://dev.to/vfazal/why-senior-data-engineers-write-sql-differently-ocl</link>
      <guid>https://dev.to/vfazal/why-senior-data-engineers-write-sql-differently-ocl</guid>
      <description>&lt;p&gt;Every experienced data engineer has opened a SQL file, stared at a wall of nested subqueries, and wondered, "Who wrote this?"&lt;/p&gt;

&lt;p&gt;Then they check the Git history.&lt;/p&gt;

&lt;p&gt;It was them. Six months ago.&lt;/p&gt;

&lt;p&gt;Two engineers are asked to pull a list of high-value repeat customers for the marketing team. Both write a query. Both get the right answer. One query gets merged in a single review pass. The other comes back with three comments, two of which are just "what does this do?"&lt;/p&gt;

&lt;p&gt;Same result set. Same database. Completely different outcomes. The difference has nothing to do with whether the SQL is correct, because it is, in both cases. It has to do with whether the query is easy for someone else to read, understand, and change when business requirements inevitably evolve, or whether even the original author can make sense of it six months later.&lt;/p&gt;

&lt;p&gt;That is the gap this article is about. &lt;/p&gt;

&lt;p&gt;We will use one running example throughout: a PostgreSQL e-commerce schema with &lt;code&gt;customers&lt;/code&gt;, &lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;order_items&lt;/code&gt;, and &lt;code&gt;products&lt;/code&gt; tables, and a single business question: find repeat customers who have spent over $500 in the last 90 days, excluding refunded orders. We'll start with a query that works, then refactor it into one that's easier to understand, review, debug, and maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working SQL vs. Maintainable SQL
&lt;/h2&gt;

&lt;p&gt;Here is a version that works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'90 days'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'refunded'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
  &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
     &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="n"&gt;oi&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it, and it returns the correct customers. Put it into production, and three things will eventually go wrong. Someone adds a column to &lt;code&gt;customers&lt;/code&gt; and the &lt;code&gt;SELECT *&lt;/code&gt; silently pulls in data nobody asked for. Someone needs to know why the threshold is 90 days and $500, and there is no comment to tell them. Someone needs to debug why a known repeat customer is missing from the results, and the only way to check is to mentally execute a query with a subquery nested inside a &lt;code&gt;HAVING&lt;/code&gt; clause nested inside an &lt;code&gt;IN&lt;/code&gt; clause.&lt;/p&gt;

&lt;p&gt;This query works today. It may even work tomorrow. What it doesn't do is make tomorrow's change easy.&lt;/p&gt;

&lt;p&gt;Maintainable SQL is written with future readers in mind. It assumes schemas evolve, business rules change, teammates rotate, and production issues eventually happen. Senior engineers optimize for that reality. Their goal isn't simply to write SQL that returns the right rows—they write SQL that someone else can confidently modify without introducing new bugs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Good SQL doesn't minimize the number of lines. It minimizes the amount of information the reader has to keep in their head.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Structure SQL as a Sequence of Transformations
&lt;/h2&gt;

&lt;p&gt;The query optimizer cares about execution plans; your teammates care about readability. Modern database query optimizers can often transform different SQL formulations into similar execution plans by flattening subqueries, reordering joins, and applying other optimizations. When performance is comparable, the version that's easier for humans to understand is usually the better engineering choice.&lt;/p&gt;

&lt;p&gt;Here is the same logic, restructured.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Repeat customer definition: more than one qualifying order&lt;/span&gt;
&lt;span class="c1"&gt;-- in the trailing 90 days, excluding refunds.&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;recent_orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'90 days'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'refunded'&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;order_totals&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_total&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;recent_orders&lt;/span&gt; &lt;span class="n"&gt;ro&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="n"&gt;oi&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;customer_summary&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_spend&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;order_totals&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;high_value_repeat_customers&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_spend&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer_summary&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;total_spend&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hvrc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hvrc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_spend&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;high_value_repeat_customers&lt;/span&gt; &lt;span class="n"&gt;hvrc&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hvrc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hvrc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_spend&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing about the output changed. Everything about the experience of reading it did. A few things are doing the work here, and they generalize well beyond this one query.&lt;/p&gt;

&lt;p&gt;The nested subquery is gone. The original query asked the reader to hold three levels of logic in their head at once: which orders qualify, what each order is worth, and which customers clear the bar. Splitting that into sequential CTEs means each block answers exactly one question, and the next block builds on a clean answer instead of a buried calculation.&lt;/p&gt;

&lt;p&gt;Each CTE has a name that describes what it produces, not what it technically is. &lt;code&gt;recent_orders&lt;/code&gt;, &lt;code&gt;order_totals&lt;/code&gt;, &lt;code&gt;customer_summary&lt;/code&gt;, and &lt;code&gt;high_value_repeat_customers&lt;/code&gt; tell you the shape of the data at each stage without opening the block. Compare that to &lt;code&gt;cte1&lt;/code&gt;, &lt;code&gt;t&lt;/code&gt;, or &lt;code&gt;temp_data&lt;/code&gt;, which tell you nothing and force a re-read every time they appear later in the query.&lt;/p&gt;

&lt;p&gt;The transformation is broken into logical stages that mirror how a person would actually reason through the problem by hand: filter the orders, total the orders, summarize by customer, then apply the business threshold. That sequence is also the order someone would naturally debug in.&lt;/p&gt;

&lt;p&gt;Which leads to the real payoff: easier debugging. If a customer who should qualify is missing, you do not have to mentally execute the whole query. You run &lt;code&gt;SELECT * FROM recent_orders WHERE customer_id = 1042&lt;/code&gt; and check whether their orders show up. Then &lt;code&gt;order_totals&lt;/code&gt;. Then &lt;code&gt;customer_summary&lt;/code&gt;. Each CTE is a checkpoint you can inspect in isolation, which is exactly why this structure is easier to maintain than a single dense expression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Select Only the Columns You Need
&lt;/h2&gt;

&lt;p&gt;I once reviewed a query that produced the correct report for over a year. Then a new column was added to the source table. Because the query used SELECT *, a downstream export silently changed shape and broke another team's pipeline. Nothing was "wrong" with the SQL. It simply wasn't written with future change in mind. Nobody intentionally changed the business logic, yet downstream systems still break because the query's contract was never explicit.&lt;/p&gt;

&lt;p&gt;The same is happening here. Look back at the original query and notice what &lt;code&gt;SELECT * FROM customers&lt;/code&gt; actually returns. Every column on that table, including ones that have nothing to do with the question being asked, and ones that might not exist yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Before: pulls every column on customers, including ones&lt;/span&gt;
&lt;span class="c1"&gt;-- nobody asked for and some that may not even be safe to expose.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- After: explicit about exactly what's needed downstream.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signup_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The explicit version costs three extra lines and buys real protection. If someone adds an &lt;code&gt;marketing_preferences&lt;/code&gt; or &lt;code&gt;gdpr_deletion_requested&lt;/code&gt; column to &lt;code&gt;customers&lt;/code&gt; next quarter, the &lt;code&gt;SELECT *&lt;/code&gt; version starts leaking it into every downstream dashboard and export that consumes this query, without anyone deciding that should happen. The explicit version is unaffected, because it only ever returns what it was written to return.&lt;/p&gt;

&lt;p&gt;There is also a performance and cost angle. Most modern cloud data warehouses, such as Snowflake, BigQuery, Redshift, and Databricks, store data in a columnar format, allowing the query engine to read only the columns required by a query. When you use SELECT *, the execution engine has to read every selected column, including ones you don't actually need. On wide tables, that can mean scanning large JSON documents, audit fields, or other bulky columns that downstream consumers never use. Selecting only the columns you need not only makes the intent of the query clearer, but can also reduce I/O, lower query costs, and improve performance in column-oriented systems.&lt;/p&gt;

&lt;p&gt;There is a reviewability cost too. A reviewer looking at &lt;code&gt;SELECT *&lt;/code&gt; has to go check the table definition to know what they are actually approving. A reviewer looking at an explicit column list can see the entire contract of the query in the query itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate Results Before Shipping
&lt;/h2&gt;

&lt;p&gt;A query that runs without an error is not the same thing as a query that is correct, and senior engineers tend to have been burned by that distinction at least once. Before this query is delivered to the marketing team, it is worth a few minutes of sanity checking.&lt;/p&gt;

&lt;p&gt;Check that the row count is plausible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;high_value_repeat_customers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the customer base is 40,000 people and this query returns 38,000 of them as "high-value repeat customers," the threshold logic is almost certainly wrong somewhere, even though the query executed cleanly.&lt;/p&gt;

&lt;p&gt;Check for nulls in fields the downstream team will rely on, like &lt;code&gt;email&lt;/code&gt;. A join that silently drops or nulls out a field is invisible until someone tries to use the data.&lt;/p&gt;

&lt;p&gt;Check the aggregate against something independently known. If finance reports total revenue for the last 90 days as roughly &lt;code&gt;$1.2M&lt;/code&gt;, and summing &lt;code&gt;total_spend&lt;/code&gt; across all customers in this result set comes out to &lt;code&gt;$4M&lt;/code&gt;, something is double-counting, most likely a join fanning out rows somewhere upstream of &lt;code&gt;order_totals&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is a quick checklist to mentally walk through before sharing a query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the row count look reasonable?&lt;/li&gt;
&lt;li&gt;Did any joins accidentally multiply rows?&lt;/li&gt;
&lt;li&gt;Are unexpected &lt;code&gt;NULL&lt;/code&gt; values appearing?&lt;/li&gt;
&lt;li&gt;Do aggregates roughly match a trusted source?&lt;/li&gt;
&lt;li&gt;Have obvious edge cases been tested?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks may not guarantee correctness, but they dramatically reduce the chance of discovering problems after someone has already acted on the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make Intent Obvious
&lt;/h2&gt;

&lt;p&gt;A query that only the author can parse is not really finished, because someone else is going to have to read it during code review, during an incident, or during onboarding. Formatting consistency does a lot of the work here: consistent capitalization for keywords, consistent indentation for joins and conditions, and CTEs that are visually separated so the eye can find the boundaries between stages at a glance. None of these changes what the query does. It changes how quickly a reviewer can understand the query. That is the real cost being paid every time SQL is reviewed.&lt;/p&gt;

&lt;p&gt;Instead of compressing multiple joins, filters, and conditions into dense blocks of SQL, keep related logic together, align joins consistently, and format predicates predictably. Reviewers should be able to scan the query and immediately identify filtering, aggregation, and business rules without mentally reformatting the code first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good formatting doesn't make SQL execute faster. It makes people understand it faster.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Comments matter, but only in the right place. A comment explaining what a &lt;code&gt;GROUP BY&lt;/code&gt; does is noise, because the SQL already says that. A comment explaining why orders are filtered to the last 90 days, or why refunded orders are excluded, is signal, because that logic comes from a business decision the code itself cannot express. The CTE names in the example above already do a lot of that explanatory work for free. Comments should fill in what names cannot carry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make Assumptions Explicit
&lt;/h2&gt;

&lt;p&gt;Every business query is full of decisions that are invisible in the result set. Why 90 days and not 60. Why a refunded order does not count toward spend even though it briefly existed as a completed order. Why "repeat" means more than one order rather than two or more, which is the same threshold expressed differently but easy to get wrong if someone has to guess at it later.&lt;/p&gt;

&lt;p&gt;Senior engineers do not just write queries, they encode these assumptions so the next reader does not have to reverse-engineer them from the logic. That can be a comment block at the top of the query, like the one above the &lt;code&gt;recent_orders&lt;/code&gt; CTE. It can be a CTE name that states the assumption directly, the way &lt;code&gt;high_value_repeat_customers&lt;/code&gt; tells you the threshold logic lives in that block before you even read it. What it cannot be is silence, because silence forces the next person to either guess or go find the original author and ask.&lt;/p&gt;

&lt;p&gt;This matters more as a query gets reused. The first time someone copies this logic into a new dashboard, they will copy the assumptions along with it, whether those assumptions are documented or not. Making them explicit is the only way to make sure they get copied correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write SQL for the Next Engineer
&lt;/h2&gt;

&lt;p&gt;None of the techniques in this article are particularly advanced. Naming CTEs well, avoiding &lt;code&gt;SELECT *&lt;/code&gt;, validating results, or breaking complex logic into smaller steps won't make you the smartest SQL developer in the room. &lt;/p&gt;

&lt;p&gt;What they do is make your SQL easier for the next engineer to understand, review, debug, and safely change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior engineers often optimize for making the query run. Senior engineers optimize for making the next change easy.&lt;/strong&gt; They write SQL that explains itself, surfaces assumptions, and reduces the amount of information another engineer has to keep in their head.&lt;/p&gt;

&lt;p&gt;SQL is read far more often than it is written. Optimize for the reader first, and maintainability naturally follows.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>dataengineering</category>
      <category>data</category>
    </item>
    <item>
      <title>Airflow 3.x Architecture Explained: A Practical Guide for Data Engineers</title>
      <dc:creator>Fazalul Haque</dc:creator>
      <pubDate>Mon, 22 Jun 2026 12:53:58 +0000</pubDate>
      <link>https://dev.to/vfazal/airflow-3x-architecture-explained-a-practical-guide-for-data-engineers-3261</link>
      <guid>https://dev.to/vfazal/airflow-3x-architecture-explained-a-practical-guide-for-data-engineers-3261</guid>
      <description>&lt;h2&gt;
  
  
  What Is Airflow?
&lt;/h2&gt;

&lt;p&gt;Let me be honest about how I first encountered Airflow. My team had a bunch of Python and SQL scripts running on cron jobs. One script would download data, another would load it into a staging database, a third one would transform it, and a fourth one would ingest it into our data warehouse. The scripts were chained together with some fragile shell glue. When things worked, nobody noticed. When things broke — and they broke often — figuring out &lt;em&gt;where&lt;/em&gt; it broke and &lt;em&gt;why&lt;/em&gt; was a nightmare.&lt;/p&gt;

&lt;p&gt;Someone on the team said "we should use Airflow." I nodded like I understood, then spent the next three evenings reading documentation.&lt;/p&gt;

&lt;p&gt;Here's what I wish someone had told me then: &lt;strong&gt;Airflow is a platform for authoring, scheduling, and monitoring workflows — workflows that you define as Python code.&lt;/strong&gt; That's it. No magic. You write a Python file describing what tasks to run and in what order, and Airflow takes care of the scheduling, retries, logging, and visibility.&lt;/p&gt;

&lt;p&gt;The thing that took me a while to internalize is that Airflow is not a data processing engine. It doesn't move your data around. It &lt;em&gt;orchestrates&lt;/em&gt; the things that do. It's the conductor, not the orchestra.&lt;/p&gt;




&lt;h2&gt;
  
  
  The DAG — Everything Starts Here
&lt;/h2&gt;

&lt;p&gt;Before diving into the architecture, you need to understand what a DAG is, because the whole system revolves around it.&lt;/p&gt;

&lt;p&gt;DAG stands for &lt;strong&gt;Directed Acyclic Graph&lt;/strong&gt;. Sounds fancy. In practice it just means: a set of tasks with dependencies between them, where you can't have circular dependencies (task A depends on task B depends on task A — that would be a cycle, and Airflow won't allow it).&lt;/p&gt;

&lt;p&gt;A simple DAG might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;extract_data → transform_data → load_to_warehouse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You define this in a Python file. Each step is a &lt;strong&gt;Task&lt;/strong&gt; – a single unit of work, like running a Python function, executing a SQL query, or calling an external API. The arrows between them are &lt;strong&gt;dependencies&lt;/strong&gt;. Airflow reads your Python file, figures out the task ordering, and then knows how and when to execute things.&lt;/p&gt;

&lt;p&gt;In Airflow 3.x, DAG files still live in a &lt;code&gt;dags/&lt;/code&gt; folder, but there have been some meaningful changes to how they're parsed and distributed — we'll get to that when we talk about the DAG Processor.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: A Bird's Eye View
&lt;/h2&gt;

&lt;p&gt;Before we look at each component individually, here's a high-level view of how they fit together:&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%2Fili4w3mlog6gpsoyc77a.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%2Fili4w3mlog6gpsoyc77a.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every component reads from or writes to the &lt;strong&gt;Metadata Database&lt;/strong&gt; in some way. That database is the single source of truth for the entire system. Let's now go through each piece.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Understanding the Architecture Matters
&lt;/h2&gt;

&lt;p&gt;If you're running Airflow locally, many of these components may live on the same machine and feel invisible.&lt;/p&gt;

&lt;p&gt;So why spend time learning the architecture?&lt;/p&gt;

&lt;p&gt;Because most real-world Airflow problems come down to understanding which component is responsible for what.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DAG not showing up in the UI? Look at DAG parsing.&lt;/li&gt;
&lt;li&gt;Tasks stuck in queued state? Check the executor and workers.&lt;/li&gt;
&lt;li&gt;No new tasks are starting? Investigate the scheduler.&lt;/li&gt;
&lt;li&gt;UI errors or failed API requests? Check the API Server.&lt;/li&gt;
&lt;li&gt;Everything seems broken? The metadata database is often the first place to look.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to memorize every component. But having a mental model of how they fit together makes troubleshooting much easier as you move beyond simple local deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Metadata Database
&lt;/h2&gt;

&lt;p&gt;I'm starting here because once you understand this, everything else makes sense.&lt;/p&gt;

&lt;p&gt;The metadata database is a relational database — Postgres in production (please use Postgres, not SQLite outside of local dev). It stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All your DAG definitions and their current state&lt;/li&gt;
&lt;li&gt;Every DAG Run — every time a DAG has executed or is scheduled to execute&lt;/li&gt;
&lt;li&gt;Every Task Instance — every individual task run, its state (queued, running, success, failed), pointers to logs, and timing&lt;/li&gt;
&lt;li&gt;User information, connections, variables, and more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every other component in Airflow talks to this database. The Scheduler reads from it to decide what to run. Workers write to it when tasks finish. The API Server and UI read from it to show you that nice UI. The database is the glue that holds the distributed system together.&lt;/p&gt;

&lt;p&gt;One practical implication: if your database is slow or down, your entire Airflow cluster is effectively crippled. Invest in your database setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Scheduler
&lt;/h2&gt;

&lt;p&gt;The Scheduler is the brain of the operation. It's a long-running process that continuously checks the metadata database, figures out which tasks are ready to run, and sends them off to be executed.&lt;/p&gt;

&lt;p&gt;Here's a rough version of what it does in a loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check all DAG schedules — is there a DAG that's supposed to have run in the last interval and hasn't started yet?&lt;/li&gt;
&lt;li&gt;Create &lt;strong&gt;DAG Runs&lt;/strong&gt; and &lt;strong&gt;Task Instances&lt;/strong&gt; in the metadata database for anything that needs to run&lt;/li&gt;
&lt;li&gt;Look at all Task Instances that are in a "scheduled" state and evaluate their dependencies — are the upstream tasks done?&lt;/li&gt;
&lt;li&gt;Move eligible tasks to "queued" state and submit them to the executor&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Scheduler doesn't run tasks itself. It &lt;em&gt;decides&lt;/em&gt; that tasks should run and tells the executor to handle the actual execution. That's an important distinction.&lt;/p&gt;

&lt;p&gt;In Airflow 3.x, the Scheduler has become more reliable and scalable. You can run multiple schedulers (active-active setup) for high availability. If one scheduler process dies, another picks up the work.&lt;/p&gt;

&lt;p&gt;One gotcha with the Scheduler: it needs up-to-date DAG metadata in order to schedule work. In Airflow 3.x, DAG parsing responsibilities can be delegated to a dedicated DAG Processor component. More on that below.&lt;/p&gt;




&lt;h2&gt;
  
  
  The DAG Processor (New in Airflow 3.x)
&lt;/h2&gt;

&lt;p&gt;This is one of the more significant architectural changes in Airflow 3.x, and one that beginners often don't realize exists.&lt;/p&gt;

&lt;p&gt;In older versions of Airflow, the Scheduler also handled parsing your DAG files. This was problematic. If a DAG file had a slow import or a bug that caused it to hang, it could slow down the Scheduler itself. Not great.&lt;/p&gt;

&lt;p&gt;In Airflow 3.x, DAG parsing has been extracted into its own &lt;strong&gt;DAG Processor&lt;/strong&gt; component. It runs as a separate process, continuously scanning your &lt;code&gt;dags/&lt;/code&gt; directory, importing the Python files, and extracting DAG metadata (schedule, tasks, dependencies) into the metadata database.&lt;/p&gt;

&lt;p&gt;The Scheduler then just reads from the database — it doesn't need to parse Python files directly anymore. Cleaner separation of concerns.&lt;/p&gt;

&lt;p&gt;To be precise, in a basic single‑machine setup the scheduler still spawns subprocesses to parse DAGs for you; the dedicated DAG Processor is mainly a production pattern. The important idea is that, in 3.x, you can keep user DAG code out of the core scheduler process and scale DAG parsing independently when you need to.&lt;/p&gt;




&lt;h2&gt;
  
  
  The API Server
&lt;/h2&gt;

&lt;p&gt;Airflow 3.x introduced a proper standalone API Server. In earlier versions, the REST API was bundled into the Webserver. &lt;/p&gt;

&lt;p&gt;The API Server exposes Airflow's REST API — it's what the UI uses to fetch DAG information, trigger runs, clear tasks, etc. It's also what you'd use if you want to trigger DAG runs programmatically from another system. CI/CD pipelines kicking off DAGs, external services triggering workflows — all of that goes through the API Server.&lt;/p&gt;

&lt;p&gt;What changed from 2.x is that the REST API is now a first-class interface rather than a secondary feature attached to the Webserver. This separation gives Airflow a cleaner architecture and makes it easier to evolve the UI and API independently over time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Webserver (Airflow UI)
&lt;/h2&gt;

&lt;p&gt;In Airflow 3.x, the traditional standalone webserver role has largely been replaced by a UI that communicates with the API Server.&lt;/p&gt;

&lt;p&gt;You use Airflow UI to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;See all your DAGs and their last run status&lt;/li&gt;
&lt;li&gt;View the graph of a DAG — which tasks ran, which failed, which are running right now&lt;/li&gt;
&lt;li&gt;Look at logs for individual task runs&lt;/li&gt;
&lt;li&gt;Manually trigger DAG runs or clear failed tasks to retry them&lt;/li&gt;
&lt;li&gt;Manage connections and variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Airflow 3.x, the UI has been significantly revamped. It's faster, cleaner, and the new &lt;strong&gt;Grid View&lt;/strong&gt; is much more useful than the old tree view for understanding the history of a DAG across many runs.&lt;/p&gt;

&lt;p&gt;The Airflow UI is stateless with respect to actual scheduling. It doesn't schedule anything. It's purely a read/write interface to the metadata database (via the API Server). If the API Server or the UI crashes, your DAGs keep running. Your visibility goes away, but the work continues.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Executor
&lt;/h2&gt;

&lt;p&gt;The Executor is one of the most misunderstood Airflow concepts.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Executor&lt;/strong&gt; is not a separate process you run — it's a component &lt;em&gt;within&lt;/em&gt; the Scheduler that determines how tasks actually get executed. Think of it as the strategy the Scheduler uses for dispatching work, not something you run independently.&lt;/p&gt;

&lt;p&gt;There are a few executor types:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LocalExecutor&lt;/strong&gt; — Tasks run as subprocesses on the same machine as the Scheduler. Simple, works well for small setups. Not suitable for real scale because everything shares one machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CeleryExecutor&lt;/strong&gt; — Tasks are sent to a Celery task queue (backed by Redis or RabbitMQ), and picked up by Worker processes that can run on different machines. This is the classic horizontally scalable setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KubernetesExecutor&lt;/strong&gt; — Each task spins up a new Kubernetes Pod to run in, and the pod is destroyed when the task finishes. Very clean isolation, great for containerized environments. More overhead per task but excellent for bursty workloads.&lt;/p&gt;

&lt;p&gt;Some deployments use hybrid execution models that combine Celery workers with Kubernetes-based execution.&lt;/p&gt;

&lt;p&gt;The choice of executor has massive implications for how you deploy and scale Airflow. Start with LocalExecutor if you're just learning. Move to CeleryExecutor or KubernetesExecutor as you grow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Workers
&lt;/h2&gt;

&lt;p&gt;Workers are the things that actually &lt;em&gt;execute&lt;/em&gt; your task code. They're separate processes (or pods, in the Kubernetes case) that pull tasks from the queue and run them. They only exist as separate processes if you're using CeleryExecutor or KubernetesExecutor. With LocalExecutor, there isn’t a separate worker service: tasks run as subprocesses inside the scheduler machine. &lt;/p&gt;

&lt;p&gt;When a task runs on a Worker:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Worker picks up the task from the queue&lt;/li&gt;
&lt;li&gt;It executes your Python function (or Bash command, or Spark job, or whatever the operator does)&lt;/li&gt;
&lt;li&gt;It writes the result back to the metadata database — success or failure&lt;/li&gt;
&lt;li&gt;It ships logs to wherever logs are configured to go (local filesystem, S3, GCS, etc.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Workers don't need to know about your full DAG structure. They just need to know "run this task." All the DAG context they need is passed along with the task message.&lt;/p&gt;

&lt;p&gt;One important thing: in a multi-worker setup, every worker machine needs access to the same DAG files. Otherwise, the worker won't be able to find the code it's supposed to run. This is commonly solved by mounting a shared filesystem or using Git Sync to pull DAGs onto every worker.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Triggerer
&lt;/h2&gt;

&lt;p&gt;One component I haven't mentioned yet: the Triggerer. It's an optional process that handles deferred tasks — tasks that are waiting on some external event (like a file landing in S3, or a sensor waiting for a condition) without occupying a worker slot while they wait. It runs deferred tasks in an asyncio event loop, which is far more efficient than a worker sitting idle. If you're not using deferrable operators, you don't strictly need it — but most production setups run it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connections and Variables
&lt;/h2&gt;

&lt;p&gt;These aren't really "architecture" in the strict sense, but they're part of the runtime infrastructure and worth understanding early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connections&lt;/strong&gt; are how Airflow stores credentials and connection info for external systems — your database host/port/user/password, your AWS credentials, your Snowflake account info. When you use an operator (like &lt;code&gt;PostgresOperator&lt;/code&gt; or &lt;code&gt;S3Hook&lt;/code&gt;), it looks up a named connection from the metadata database rather than you hardcoding credentials in your DAG.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variables&lt;/strong&gt; are just key-value pairs stored in the metadata database. Useful for config values you want to change without editing DAG code.&lt;/p&gt;

&lt;p&gt;Both connections and variables can be managed through the UI, the API, or environment variables (the latter being preferred in production from a secrets management perspective).&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Let's see what actually happens when a scheduled DAG runs, so all these pieces connect:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Your DAG file lives in the &lt;code&gt;dags/&lt;/code&gt; folder. The &lt;strong&gt;DAG Processor&lt;/strong&gt; picks it up, parses it, and writes the DAG structure to the &lt;strong&gt;Metadata Database&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;Scheduler&lt;/strong&gt; wakes up (it's running in a loop, typically every few seconds). It checks the database and sees that your DAG is scheduled to run at 2:00 AM. It's now 2:00 AM. The Scheduler creates a &lt;strong&gt;DAG Run&lt;/strong&gt; record and individual &lt;strong&gt;Task Instance&lt;/strong&gt; records in the database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Scheduler evaluates task dependencies. Task A has no upstream dependencies, so it becomes eligible. The Scheduler flips its state to "queued" and tells the &lt;strong&gt;Executor&lt;/strong&gt; to execute it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Executor places Task A into the &lt;strong&gt;task queue&lt;/strong&gt; (Redis/RabbitMQ when using CeleryExecutor).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An available &lt;strong&gt;Worker&lt;/strong&gt; picks up Task A from the queue. It executes your Python function. If it succeeds, the Worker marks the Task Instance as "success" in the database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Back to the Scheduler — on the next loop, it sees Task A is done. Task B was waiting on Task A. Now Task B's dependencies are satisfied, so it becomes eligible. The cycle repeats.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Throughout all of this, the &lt;strong&gt;API Server&lt;/strong&gt; is answering requests from your browser, showing you the DAG Run and its task states in real time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Everything is written to the &lt;strong&gt;Metadata Database&lt;/strong&gt; along the way.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What if one of the components is down?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;What breaks if it's down?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler&lt;/td&gt;
&lt;td&gt;No new tasks start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workers&lt;/td&gt;
&lt;td&gt;Tasks stop executing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata DB&lt;/td&gt;
&lt;td&gt;Entire platform affected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API Server&lt;/td&gt;
&lt;td&gt;UI/API operations fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triggerer&lt;/td&gt;
&lt;td&gt;Deferrable tasks stop progressing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  A Note on Airflow 3.x Specifically
&lt;/h2&gt;

&lt;p&gt;Airflow 3.x brought a bunch of changes, and if you're reading older tutorials, some things will look different. The key differences to be aware of:&lt;/p&gt;

&lt;p&gt;The DAG authoring syntax got cleaner — the &lt;code&gt;@dag&lt;/code&gt; and &lt;code&gt;@task&lt;/code&gt; decorators (from the TaskFlow API) are now first-class citizens and the recommended way to write most DAGs. The old Operator-heavy style still works but new code should prefer TaskFlow.&lt;/p&gt;

&lt;p&gt;The UI was rebuilt from scratch — it's a modern React app now, not the old Flask/Jinja UI. Grid view is the default and it's significantly better.&lt;/p&gt;

&lt;p&gt;The Scheduler and DAG Processor are now clearly separated — as described above. Relevant if you're configuring a production deployment.&lt;/p&gt;

&lt;p&gt;The REST API is now the proper interface for programmatic access — no more hacking around with CLI commands for automation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to Go From Here
&lt;/h2&gt;

&lt;p&gt;If you've followed this far, you understand the fundamental architecture. The next things worth digging into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write your first DAG using the TaskFlow API (&lt;code&gt;@dag&lt;/code&gt;, &lt;code&gt;@task&lt;/code&gt; decorators)&lt;/li&gt;
&lt;li&gt;Understand what Operators are — they're the building blocks of tasks (PythonOperator, BashOperator, and so on)&lt;/li&gt;
&lt;li&gt;Set up a local Airflow environment with Docker Compose (the official &lt;code&gt;docker-compose.yaml&lt;/code&gt; from the Airflow docs is a good start)&lt;/li&gt;
&lt;li&gt;Learn about XComs — that's how tasks pass data to each other&lt;/li&gt;
&lt;li&gt;Understand task retries and the &lt;code&gt;on_failure_callback&lt;/code&gt; — essential for production use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture seems complex at first, but once you've run Airflow a few times, you start to develop intuition for which component is causing problems when things go wrong. And things will go wrong. That's part of the fun. Good luck.&lt;/p&gt;




</description>
      <category>airflow</category>
      <category>dataengineering</category>
      <category>etl</category>
      <category>datapipeline</category>
    </item>
    <item>
      <title>How to Deploy a LangGraph Agent on AWS Bedrock AgentCore</title>
      <dc:creator>Fazalul Haque</dc:creator>
      <pubDate>Mon, 25 May 2026 15:54:19 +0000</pubDate>
      <link>https://dev.to/vfazal/how-to-deploy-a-langgraph-agent-on-aws-bedrock-agentcore-2a9p</link>
      <guid>https://dev.to/vfazal/how-to-deploy-a-langgraph-agent-on-aws-bedrock-agentcore-2a9p</guid>
      <description>&lt;p&gt;You’ve built a LangGraph agent that works fine on your laptop. The next challenge is getting it running in a scalable, serverless production infrastructure without having to redesign the whole thing.&lt;/p&gt;

&lt;p&gt;That’s where AWS Bedrock AgentCore comes in. In this guide, I’ll show you how to put a wrapper for your existing agent to make it run on AgentCore, set up an AgentCore project, test it locally, deploy it to AWS, and invoke it after deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is AWS Bedrock AgentCore?
&lt;/h2&gt;

&lt;p&gt;AgentCore is a serverless hosting platform designed by AWS to deploy, scale, and operate your AI agents securely without you managing the infrastructure. It works with any open-source framework like LangGraph, Strands, CrewAI, or LlamaIndex and supports Large Language Models like OpenAI's GPT, Google's Gemini, or Anthropic's Claude. So you don’t have to rewrite the agent logic. It also provides session isolation, persistent memory, observability and identity management.&lt;/p&gt;

&lt;p&gt;The deployment is managed through the &lt;strong&gt;AgentCore CLI&lt;/strong&gt;, a Node.js tool that scaffolds projects, runs a local dev server, and deploys to AWS using CDK under the hood.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you start, make sure you have the following in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.12+&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;uv&lt;/strong&gt; or &lt;strong&gt;pip&lt;/strong&gt; for Python dependency management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 20+&lt;/strong&gt; — the AgentCore CLI is an npm package&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS CDK&lt;/strong&gt; installed globally (&lt;code&gt;npm install -g aws-cdk&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;AWS account&lt;/strong&gt; with credentials configured locally (&lt;code&gt;aws configure&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Your existing LangGraph agent code that creates a compiled StateGraph object&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1 — Install the AgentCore CLI
&lt;/h2&gt;

&lt;p&gt;The AgentCore workflow starts with a single command-line tool. You’ll use it to create the project, run the app locally, and deploy it to AWS cloud.&lt;/p&gt;

&lt;p&gt;Install the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @aws/agentcore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it after the installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2 — Add Additional Dependencies
&lt;/h2&gt;

&lt;p&gt;Add the following packages to your agent's dependencies:&lt;br&gt;
    - &lt;code&gt;bedrock-agentcore&lt;/code&gt; - the Python SDK that provides the &lt;code&gt;BedrockAgentCoreApp&lt;/code&gt; wrapper class.&lt;br&gt;
    - &lt;code&gt;aws-opentelemetry-distro&lt;/code&gt; - AWS-supported distribution of the OpenTelemetry Python Instrumentation package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# pyproject.toml&lt;/span&gt;
&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-agent"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.12&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"aws-opentelemetry-distro=&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.17&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"bedrock-agentcore&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"boto3&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.42&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"langgraph&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"langchain-core&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="c"&gt;# ... your other existing dependencies&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install all dependencies with your usual workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;span class="c"&gt;# or: pip install -e .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3 — Write the AgentCore Entrypoint (&lt;code&gt;main.py&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;AgentCore expects a single Python file as the entrypoint with a &lt;code&gt;BedrockAgentCoreApp&lt;/code&gt; instance and a function decorated with &lt;code&gt;@app.entrypoint&lt;/code&gt;. The important part is that your LangGraph logic does not need to change much; you’re just wrapping it in a small runtime entrypoint. &lt;/p&gt;

&lt;p&gt;Here is the basic structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.messages&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HumanMessage&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bedrock_agentcore.runtime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BedrockAgentCoreApp&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;graphs.my_agent_graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;build_my_agent&lt;/span&gt;  &lt;span class="c1"&gt;# your existing graph builder
&lt;/span&gt;
&lt;span class="c1"&gt;# 1. Instantiate the AgentCore app and logger
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Build your graph at module load time (startup)
#    AgentCore initialises the module once, then handles concurrent invocations.
#    Any failure here will prevent a broken agent from going live.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Initialising agent...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_my_agent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# returns your compiled LangGraph StateGraph
&lt;/span&gt;    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Critical failure during agent initialisation: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt;  &lt;span class="c1"&gt;# fail fast — don't let a broken agent start serving requests
&lt;/span&gt;
&lt;span class="c1"&gt;# 3. Async helper that drives the LangGraph streaming loop
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;responses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;configurable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;HumanMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;)]},&lt;/span&gt;
        &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stream_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;values&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 4. The entrypoint — this is what AgentCore calls on every invocation
&lt;/span&gt;&lt;span class="nd"&gt;@app.entrypoint&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invoke received&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;user_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prompt cannot be empty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="c1"&gt;# 5. Run the app (only executed when running locally using agentcore dev)
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A few things that matter
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;BedrockAgentCoreApp()&lt;/code&gt;&lt;/strong&gt; gives you the AgentCore runtime wrapper. It sets up the HTTP server, health check endpoints, and structured logging for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Module-level graph initialisation&lt;/code&gt;&lt;/strong&gt; — the graph is created once when the module loads, not on every request. That catches startup failures early, which is good because it prevents a broken app from going live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@app.entrypoint&lt;/code&gt;&lt;/strong&gt; — this decorator registers the function as the handler for incoming invocations. It receives &lt;code&gt;payload&lt;/code&gt; (the parsed JSON body) and &lt;code&gt;context&lt;/code&gt; (AgentCore request context). It should return a plain JSON-serializable dictionary, not a raw string or a custom object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;session_id&lt;/code&gt; → &lt;code&gt;thread_id&lt;/code&gt;&lt;/strong&gt; — The session_id is passed into LangGraph as thread_id, which enables per-session memory with &lt;code&gt;MemorySaver&lt;/code&gt;. &lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — Create an AgentCore Project
&lt;/h2&gt;

&lt;p&gt;Next, we need to generate our project layout. Run the initialization wizard inside a clean root folder and let it create the basic project layout for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The setup wizard will ask a few simple questions, like the project name, language, Python version, entrypoint file, etc. The important part is that the entrypoint and the Python version matches your app.&lt;/p&gt;

&lt;p&gt;After the project is created, you’ll get an agentcore.json file (in agentcore folder) that tells AgentCore where your code lives and how to run it.&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyAgentOnAgentcore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"runtimes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CodeZip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"entrypoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"codeLocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app/my-agent/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"runtimeVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PYTHON_3_12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"networkMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PUBLIC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTTP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"envVars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;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;
  
  
  Step 5 — Put Your Source Code to the Code Location
&lt;/h2&gt;

&lt;p&gt;Copy or move your agent source into the directory specified by &lt;code&gt;codeLocation&lt;/code&gt; in &lt;code&gt;agentcore.json&lt;/code&gt;.  This part is easy to get wrong, and when it’s wrong, deployment becomes unnecessarily frustrating. I struggled for a while after accidentally putting it in the agentcore folder.&lt;/p&gt;

&lt;p&gt;A typical layout should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MyAgentOnAgentcore/
├──agentcore
    └── agentcore.json
├──app/
    └──my-agent/                       ← codeLocation 
        ├── main.py                  ← entrypoint (matches agentcore.json)
        ├── pyproject.toml           ← or requirements.txt
        ├── src/
        │   └── my_agent_graph.py    ← your LangGraph graph builder
        │   └── tools.py             ← your tools, utilities, etc.
        └── .env                   ← your configuration variables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure &lt;code&gt;main.py&lt;/code&gt; sits at the top level of &lt;code&gt;codeLocation&lt;/code&gt; and matches the entrypoint in &lt;code&gt;agentcore.json&lt;/code&gt; exactly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment variables
&lt;/h3&gt;

&lt;p&gt;AgentCore can pass environment variables into the runtime container. For local testing, a &lt;code&gt;.env&lt;/code&gt; file is usually the easiest option.&lt;/p&gt;

&lt;p&gt;For production, you can put values in &lt;code&gt;agentcore.json&lt;/code&gt; under &lt;code&gt;envVars&lt;/code&gt; (&lt;code&gt;.env&lt;/code&gt; file also works). But secrets like passwords and API keys are better stored in AWS Secrets Manager and loaded at runtime. If you do that, the AgentCore execution role needs permission to read those secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;envVars&lt;/code&gt; should be an array of JSON objects with &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;value&lt;/code&gt; fields.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6 — Validate the Project
&lt;/h2&gt;

&lt;p&gt;Before you try to run anything, use AgentCore CLI to validate the project configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore validate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches the common mistakes early, like a missing entrypoint file or a bad path in the config. &lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7 — Run and Test Locally
&lt;/h2&gt;

&lt;p&gt;Start the local runtime to test the agent locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This spins up a local HTTP server that closely mirrors the production environment for testing. If everything is wired up properly, you should see that the app is ready and listening on a local port.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test on Local Server
&lt;/h3&gt;

&lt;p&gt;You can test an invocation using either the AgentCore CLI or curl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="s2"&gt;"Summarise last month sales"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/invocations &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"prompt": "Summarise last month sales", "session_id": "test-001"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful response should come back as JSON with the agent’s answer in it.&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="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Last month, total sales were $1.2M across 3 regions..."&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;
  
  
  Step 8 — Deploy to AWS
&lt;/h2&gt;

&lt;p&gt;Once local testing looks solid, deploy to AWS Bedrock AgentCore Runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, the CLI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Packages your code&lt;/li&gt;
&lt;li&gt;Provisions an S3 bucket for direct code deploy&lt;/li&gt;
&lt;li&gt;Creates an IAM execution role&lt;/li&gt;
&lt;li&gt;Deploys the AgentCore Runtime via CDK&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first deploy takes a few minutes. Subsequent deploys tend to be faster.&lt;/p&gt;

&lt;p&gt;When it’s successfully completed, check the runtime status. It will display the runtime ARN and HTTP URL to invoke the deployed agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 9 — Invoke the Deployed Agent
&lt;/h2&gt;

&lt;p&gt;The easiest way to test the deployed version is with the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"Who are the top 5 customers by revenue?"&lt;/span&gt; &lt;span class="nt"&gt;--session-id&lt;/span&gt; &lt;span class="s2"&gt;"session-id-with-length-greater-than-or-equal-33"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to call it using HTTP, you’ll need to sign the request with AWS SigV4.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn-of-MyAgentOnAgentcore-xxxx/invocations"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--aws-sigv4&lt;/span&gt; &lt;span class="s2"&gt;"aws:amz:us-east-1:bedrock-agentcore"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-amzn-bedrock-agentcore-runtime-session-id: session-id-with-length-greater-than-or-equal-33"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"prompt": "Who are the top 5 customers by revenue?"}'&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Use the HTTP URL displayed by &lt;code&gt;agentcore status&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When integrating your agent with other Python applications, using the SDK is usually the cleanest approach. You can keep the runtime ARN in an environment variable, send the prompt, and pass the same session ID back on follow-up requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# invoke_agent.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock-agentcore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;runtime_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENTCORE_RUNTIME_ARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke_agent_runtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;agentRuntimeArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;runtime_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;contentType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ready. Type &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; to quit.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your question: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agentRuntimeArn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;runtime_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contentType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runtimeSessionId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke_agent_runtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runtimeSessionId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;AGENTCORE_RUNTIME_ARN&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AGENTCORE_RUNTIME_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arn:aws:bedrock-agentcore:us-east-1:123456789012:agent-runtime/MyAgentOnAgentcore-xxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Maintaining session state across invocations
&lt;/h3&gt;

&lt;p&gt;Notice &lt;code&gt;runtimeSessionId&lt;/code&gt; in the boto3 example. AgentCore returns a &lt;code&gt;runtimeSessionId&lt;/code&gt; in every response. Passing it back in the next request tells AgentCore to route subsequent calls to the same session context — which, combined with LangGraph's &lt;code&gt;MemorySaver&lt;/code&gt;, gives the agent full conversation memory across multiple HTTP calls.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Gotchas
&lt;/h2&gt;

&lt;p&gt;Startup failures are intentional. If &lt;code&gt;create_agent()&lt;/code&gt; raises at module load, AgentCore will refuse to start the runtime. This is a feature, not a bug — it prevents an incorrectly configured agent (missing credentials, wrong DB URL) from going live silently.&lt;/p&gt;

&lt;p&gt;The execution role AgentCore creates needs explicit permissions for any AWS service your agent touches, including Secrets Manager and S3. Add those permissions after the first deploy.&lt;/p&gt;

&lt;p&gt;The Python version in &lt;code&gt;pyproject.toml&lt;/code&gt; should match the runtime version in &lt;code&gt;agentcore.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The entrypoint should return a JSON-serialisable dictionary. If it returns a plain string or a custom object, the runtime boundary will reject it.&lt;/p&gt;




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

&lt;p&gt;The transition from a local LangGraph agent to an AgentCore deployment really comes down to a few practical changes: add the right dependencies, wrap the graph in BedrockAgentCoreApp, scaffold the project, test locally, then deploy and invoke it.&lt;/p&gt;

&lt;p&gt;Everything else — the graph, the tools, the model calls, and the memory setup — stays mostly the same. AgentCore handles the runtime side, and LangGraph handles the agent logic.&lt;/p&gt;

&lt;p&gt;A full working example is available at &lt;a href="https://github.com/thedataengr/data-agent-on-aws-agentcore" rel="noopener noreferrer"&gt;github.com/thedataengr/data-agent-on-aws-agentcore&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>aws</category>
      <category>serverless</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
