<?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: Mursal Furqan Kumbhar</title>
    <description>The latest articles on DEV Community by Mursal Furqan Kumbhar (@mursalfk).</description>
    <link>https://dev.to/mursalfk</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%2F202358%2F2fbf2caa-6490-4caa-a0aa-f128a9ade35a.gif</url>
      <title>DEV Community: Mursal Furqan Kumbhar</title>
      <link>https://dev.to/mursalfk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mursalfk"/>
    <language>en</language>
    <item>
      <title>Same AWS Error, Two Answers: I Built a Tool That Stops Guessing at IAM</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Mon, 10 Aug 2026 09:22:55 +0000</pubDate>
      <link>https://dev.to/aws-builders/same-aws-error-two-answers-i-built-a-tool-that-stops-guessing-at-iam-2lf6</link>
      <guid>https://dev.to/aws-builders/same-aws-error-two-answers-i-built-a-tool-that-stops-guessing-at-iam-2lf6</guid>
      <description>&lt;p&gt;Ciao Amici 👋&lt;/p&gt;

&lt;p&gt;Let me start with a confession. Every time I hit an &lt;code&gt;AccessDenied&lt;/code&gt; in AWS, I do the same lazy thing you do. I copy the wall of text, paste it into a chat model, and ask "why?".&lt;/p&gt;

&lt;p&gt;And every time, I get back a tidy little list of maybes. Maybe it is your identity policy. Maybe it is an SCP. Maybe a permissions boundary. Maybe a resource policy. All plausible. All confident. None of them actually checked against my account.&lt;/p&gt;

&lt;p&gt;That bothered me for a long time. The model was not lying to me exactly. It was doing the only thing it could do with the words I gave it. But "here are five things it might be" is not an answer. It is a starting point for an afternoon of clicking through the IAM console.&lt;/p&gt;

&lt;p&gt;So I built a tool that stops guessing and starts checking. It is called Rosetta, and this post is the full story of building it, including the bug I found in my own ranking logic and the Bedrock wall I walked straight into.&lt;/p&gt;

&lt;p&gt;Here is the whole thing in one screenshot, before I explain a single line of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The money shot
&lt;/h2&gt;

&lt;p&gt;Same error string. One extra flag. Two completely different answers.&lt;/p&gt;

&lt;p&gt;First, text only, no credentials at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  AWS Rosetta  [text-only]
  code=AccessDenied  op=GetObject  action=s3:GetObject
  principal=arn:aws:iam::896415180455:user/rosetta-victim
  resource=arn:aws:s3:::rosetta-demo-secret/report.txt

  1. [########............] 40%  No identity-based policy allows the action
  2. [########............] 40%  Explicit deny in a Service Control Policy
  3. [########............] 40%  Explicit deny in a permission boundary
  4. [########............] 40%  Resource policy denies or omits the principal
  5. [########............] 40%  Missing KMS grant on an encrypted resource
  note: Text-only mode. Pass a read-only role ARN for authoritative causes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five causes. All at 40 percent. The tool is shrugging politely.&lt;/p&gt;

&lt;p&gt;Now the same error, with one &lt;code&gt;--role-arn&lt;/code&gt; flag pointing at a read only role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  AWS Rosetta  [enriched]
  code=AccessDenied  op=GetObject  action=s3:GetObject
  principal=arn:aws:iam::896415180455:user/rosetta-victim
  resource=arn:aws:s3:::rosetta-demo-secret/report.txt

  1. [###################.] 96%  Explicit Deny in an attached policy
     fix: An explicit Deny matched: user_rosetta-victim_rosetta-victim-policy.
          Remove or scope down that statement.
     - iam:SimulatePrincipalPolicy (authoritative)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One cause. 96 percent. The exact policy named. The exact statement named. Evidence attached.&lt;/p&gt;

&lt;p&gt;That collapse, from five-way uncertainty to one cited answer, is the entire point of this tool. Everything below is how it works and how you can run it against your own account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the guess is not the tool's fault
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable truth. From the error text alone, nobody can do better than guessing. Not a model, not a regex, not me.&lt;/p&gt;

&lt;p&gt;The string &lt;code&gt;AccessDenied when calling GetObject&lt;/code&gt; is genuinely ambiguous. It can mean any of these, and the message often will not tell you which one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;Allow&lt;/code&gt; in the identity policy&lt;/li&gt;
&lt;li&gt;An explicit &lt;code&gt;Deny&lt;/code&gt; in the identity policy&lt;/li&gt;
&lt;li&gt;An explicit &lt;code&gt;Deny&lt;/code&gt; in a Service Control Policy above the account&lt;/li&gt;
&lt;li&gt;A permissions boundary that excludes the action&lt;/li&gt;
&lt;li&gt;A resource policy on the bucket that omits the principal&lt;/li&gt;
&lt;li&gt;A missing KMS grant on an encrypted object&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six very different fixes. One identical error message. The words on your screen simply do not carry enough information to choose between them.&lt;/p&gt;

&lt;p&gt;Every "paste it into AI" tool tops out right here. It reads the words and pattern matches to the most common causes. That is exactly what my text only mode does, and it is honest about it. Five causes, flat confidence, a note telling you it is guessing.&lt;/p&gt;

&lt;p&gt;The fix is not a smarter prompt. You cannot prompt your way to information that is not in the input. The fix is to stop reading the words and start querying the account that produced them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The turn: SimulatePrincipalPolicy
&lt;/h2&gt;

&lt;p&gt;AWS has one API that answers the question authoritatively: &lt;code&gt;iam:SimulatePrincipalPolicy&lt;/code&gt;. You hand it a principal, an action, and a resource, and it evaluates the full policy chain the way IAM itself does at request time. It returns an allow or deny decision, and crucially, the exact statement that made the call, plus separate signals for whether an SCP or a boundary was the thing that blocked you.&lt;/p&gt;

&lt;p&gt;This is the heart of Rosetta's enrichment. Here is the real code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;principal_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;principal_arn&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;action&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="n"&gt;iam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;iam&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;simulate_principal_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;PolicySourceArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;principal_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ActionNames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;ResourceArns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;resource&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;resource&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&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;EvaluationResults&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;detail&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;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&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;EvalActionName&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;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&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;EvalDecision&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;allowed_by_organizations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&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;OrganizationsDecisionDetail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AllowedByOrganizations&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;allowed_by_boundary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&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;PermissionsBoundaryDecisionDetail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AllowedByPermissionsBoundary&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;matched_statements&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;m&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;SourcePolicyId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&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;MatchedStatements&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="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;out&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="nc"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;simulate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iam:SimulatePrincipalPolicy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&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="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;authoritative&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what comes back. Not just &lt;code&gt;explicitDeny&lt;/code&gt;, but &lt;code&gt;allowed_by_organizations&lt;/code&gt; and &lt;code&gt;allowed_by_boundary&lt;/code&gt; as separate booleans. That is how Rosetta distinguishes an SCP deny from a boundary deny from a plain policy deny, three completely different fixes that share one error message. The API already knows all of this. My job was just to read it correctly, which, spoiler, I did not do on the first try.&lt;/p&gt;

&lt;p&gt;The three fields work like a decision tree. If &lt;code&gt;allowed_by_organizations&lt;/code&gt; is false, an SCP is your problem and no amount of editing the user's policy will help. If &lt;code&gt;allowed_by_boundary&lt;/code&gt; is false, the permissions boundary is clamping you. Otherwise the &lt;code&gt;decision&lt;/code&gt; field tells you whether it was an explicit deny or simply the absence of any allow. Rosetta turns each of those into a distinct, correctly worded cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture, in four moves
&lt;/h2&gt;

&lt;p&gt;Rosetta is a pipeline. Each stage is a small pure function, which meant I could test the whole thing without touching AWS until the very end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parse    error string -&amp;gt; structured fields
enrich   assume read-only role, run simulate, fetch resource policy, match CloudTrail
reason   optional Bedrock pass over the evidence
rank     authoritative evidence beats model guesses beats corpus fallback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design decision I am happiest with is the hybrid model. Text only mode needs zero credentials and runs anywhere, which makes the tool useful even when you are looking at someone else's pasted error with no access to their account. Enrichment needs exactly one thing: a read only role you let Rosetta assume. Nothing else. Your credentials never leave your machine, and the role only ever has read permissions.&lt;/p&gt;

&lt;p&gt;The parser is boring regex, and that is fine. Boring is reliable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_CODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;An error occurred \(([A-Za-z0-9.]+)\)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_OP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;when calling the (\w+) operation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_PRINCIPAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:User|Role):\s*(arn:aws[^\s]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_ACTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;perform:\s*([a-zA-Z0-9]+:[A-Za-z0-9*]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_RESOURCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on resource:\s*(arn:aws[^\s]+|\*|[^\s]+)&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;Feed it a botocore error string, get back a clean struct with the service, action, principal, and resource pulled out. That struct is the contract every downstream stage depends on. If parsing gets the principal ARN wrong, the simulate targets the wrong identity, so this unglamorous regex is load bearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the read only role
&lt;/h2&gt;

&lt;p&gt;The authoritative mode needs a role Rosetta can assume, and that role needs exactly the read permissions the enrichment uses and nothing more. Here is the policy, straight from the repo at &lt;code&gt;infra/readonly-role-policy.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RosettaSimulate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"iam:SimulatePrincipalPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"iam:GetPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"iam:GetPolicyVersion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"iam:ListAttachedRolePolicies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"iam:ListAttachedUserPolicies"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RosettaResourcePolicies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kms:GetKeyPolicy"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RosettaCloudTrail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail:LookupEvents"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read only, top to bottom. Nothing here can change your account. The role's trust policy points at whichever principal runs Rosetta, your CLI identity locally or a Lambda execution role if you deploy it. Once the role exists, you pass its ARN and Rosetta assumes it with STS, does its read only work, and throws the session away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the demo yourself
&lt;/h2&gt;

&lt;p&gt;I did not want to test enrichment against my real infrastructure, so I built a deliberately broken IAM setup as a throwaway CloudFormation stack. One stack up, one stack down, no leftover mess to hunt for later. The core of it is a victim user carrying an &lt;code&gt;Allow&lt;/code&gt; on S3 reads, then an explicit &lt;code&gt;Deny&lt;/code&gt; on one secret bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;VictimUser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::IAM::User&lt;/span&gt;
  &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;UserName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rosetta-victim&lt;/span&gt;
    &lt;span class="na"&gt;Policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;PolicyName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rosetta-victim-policy&lt;/span&gt;
        &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2012-10-17"&lt;/span&gt;
          &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Sid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AllowS3Read&lt;/span&gt;
              &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
              &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3:GetObject&lt;/span&gt;
              &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Sid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DenySecretBucket&lt;/span&gt;
              &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deny&lt;/span&gt;
              &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3:GetObject&lt;/span&gt;
              &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:s3:::rosetta-demo-secret/*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That explicit &lt;code&gt;Deny&lt;/code&gt; is the exact thing the money shot catches. Deploy the stack, run Rosetta against it, get your own before and after, then delete the stack and everything vanishes in one action. The full template is in the repo under &lt;code&gt;scenario/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one trick worth stealing
&lt;/h2&gt;

&lt;p&gt;If you take one technical thing from this post, take this.&lt;/p&gt;

&lt;p&gt;I wanted Rosetta to find the actual failed call in CloudTrail, so it could show you the real request that got denied, with the source IP and the resources involved. My first instinct was to look it up by request ID. That is the natural key, right? Every AWS error has one.&lt;/p&gt;

&lt;p&gt;Wrong. CloudTrail's &lt;code&gt;LookupEvents&lt;/code&gt; does not accept RequestID as a lookup attribute. The allowed keys are things like EventName, Username, and EventSource. Request ID is simply not on the list, and if you pass it you get a ValidationException.&lt;/p&gt;

&lt;p&gt;So the trick is: look up by &lt;code&gt;EventName&lt;/code&gt;, pull a small batch of recent events, then filter on the &lt;code&gt;requestID&lt;/code&gt; field inside the returned event JSON. The ID is in the payload, just not in the index you can query on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cloudtrail_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;principal_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;cloudtrail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookup_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;LookupAttributes&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;AttributeKey&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;EventName&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;AttributeValue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;MaxResults&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&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;Events&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;record&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;ev&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;CloudTrailEvent&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;{}&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;request_id&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requestID&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;request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="c1"&gt;# matched the exact failing call
&lt;/span&gt;        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more honest detail. CloudTrail has a delivery delay, so a very fresh error may not have a matching event yet. Rosetta says so rather than inventing one. Pretending you found the event when you did not is worse than admitting the delay.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug I found by testing my own tool
&lt;/h2&gt;

&lt;p&gt;Here is the part every reviewer actually cares about, because it is the part most posts quietly leave out.&lt;/p&gt;

&lt;p&gt;I pointed Rosetta at my broken stack. The enrichment fired, the simulate ran, and Rosetta confidently told me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No policy allows this action (implicit deny)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except it was not an implicit deny. It was a screaming explicit &lt;code&gt;Deny&lt;/code&gt;, sitting right there in the policy I had written thirty seconds earlier. My ranking code was wrong.&lt;/p&gt;

&lt;p&gt;The cause was dumb and instructive. My &lt;code&gt;rank.py&lt;/code&gt; was checking the decision but had brittle branch ordering, and worse, it was hiding the raw simulate decision, so I could not even see what the API had actually returned. The tool was making an authoritative claim while concealing the evidence for it. That is the most dangerous kind of bug, the confident one.&lt;/p&gt;

&lt;p&gt;I rewrote the ranking to read the decision properly and to always surface the real &lt;code&gt;EvalDecision&lt;/code&gt; and matched statement. The branch that matters now reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;explicitDeny&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="nc"&gt;Cause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explicit Deny in an attached policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mf"&gt;0.96&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;An explicit Deny matched: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Remove or scope down that statement.&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;evidence&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To confirm the fix, I added a &lt;code&gt;--json&lt;/code&gt; flag that dumps the raw pipeline output, every piece of evidence included. Running it showed the truth the pretty output had been hiding:&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="nl"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"explicitDeny"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"matched_statements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"user_rosetta-victim_rosetta-victim-policy"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API had been telling me &lt;code&gt;explicitDeny&lt;/code&gt; the whole time. My code was just not listening. The lesson stuck with me: when your tool makes an authoritative claim, make it show its work, or you will end up trusting a bug with a confidence bar next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall: Bedrock, Marketplace, and a payment instrument
&lt;/h2&gt;

&lt;p&gt;I wanted an optional LLM layer on top, using Bedrock to phrase the causes more naturally and catch anything my rules missed. This is where AWS taught me something genuinely new.&lt;/p&gt;

&lt;p&gt;I picked a Claude model, set my &lt;code&gt;BEDROCK_MODEL_ID&lt;/code&gt;, and invoked. Access denied. But not an IAM denial, a Marketplace one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model access is denied due to INVALID_PAYMENT_INSTRUMENT:
A valid payment instrument must be provided.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what is going on, because it took me a while to piece together. AWS retired the old manual model access page. Models now auto enable on first invocation, which sounds convenient. But that auto enable quietly creates an AWS Marketplace subscription behind the scenes, and that subscription needs a valid payment method to complete. My account did not have one on file. AWS even emailed me a Marketplace agreement that started and expired at the exact same second, because the billing failed instantly.&lt;/p&gt;

&lt;p&gt;This is undocumented tribal knowledge, so let me save you the afternoon I lost: if you get &lt;code&gt;INVALID_PAYMENT_INSTRUMENT&lt;/code&gt; from Bedrock, it is not your IAM policy, it is not your region, and it is not the model ID. It is billing. Add a payment method, or use a model that does not route through Marketplace.&lt;/p&gt;

&lt;p&gt;I chose to ship without the reasoning layer entirely, and here is why it does not matter one bit. The authoritative answer, the 96 percent explicit deny with the matched statement, comes from &lt;code&gt;SimulatePrincipalPolicy&lt;/code&gt;, not from any model. The LLM was only ever going to reword what the deterministic check already knew for certain. Reasoning is a garnish. The simulate is the meal.&lt;/p&gt;

&lt;p&gt;So I made the model layer truly optional. No &lt;code&gt;BEDROCK_MODEL_ID&lt;/code&gt; set means Rosetta runs parse plus enrich and says nothing at all about missing models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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;BEDROCK_MODEL_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;model_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;region&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;notes&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model reasoning unavailable: &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="n"&gt;model_output&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;causes&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;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;model_output&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;causes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Graceful failure is a feature
&lt;/h2&gt;

&lt;p&gt;One more thing the build taught me. After I tore down the test stack, I ran the enriched command again out of habit. Rosetta could no longer assume the role, because the role was gone with the stack. It did not crash. It fell back to text only and told me exactly why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;note: Enrichment skipped: AccessDenied when calling the AssumeRole operation:
      User aws-rosetta is not authorized to perform sts:AssumeRole on
      resource role/rosetta-readonly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is precisely the behavior you want from a tool that reaches into live accounts. When the privileged path is unavailable, degrade to the safe path and explain the drop in plain language. Never pretend, never crash, never leave the user wondering whether the answer they got was the good one or the fallback. The mode badge at the top, &lt;code&gt;[enriched]&lt;/code&gt; or &lt;code&gt;[text-only]&lt;/code&gt;, tells you which brain answered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways to run it
&lt;/h2&gt;

&lt;p&gt;The whole tool is one engine with four front doors, so you can reach for whichever fits the moment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CLI&lt;/strong&gt;: &lt;code&gt;aws-decode "&amp;lt;error&amp;gt;"&lt;/code&gt; is the primary interface, and the one I use daily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web&lt;/strong&gt;: &lt;code&gt;web/index.html&lt;/code&gt; is a paste box in the same off white and orange I use everywhere. Run &lt;code&gt;python web/local_server.py&lt;/code&gt; for a local backend and it works with zero AWS setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API&lt;/strong&gt;: &lt;code&gt;api/handler.py&lt;/code&gt; is a Lambda Function URL handler, ready to deploy with the SAM template in &lt;code&gt;infra/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Library&lt;/strong&gt;: &lt;code&gt;from rosetta import resolve&lt;/code&gt; if you want to fold it into something bigger.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four call the same &lt;code&gt;resolve&lt;/code&gt; function, so they cannot drift apart. Fix a bug in the engine and every surface gets it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on cost and safety
&lt;/h2&gt;

&lt;p&gt;Text only mode is free. It makes no AWS calls at all. Enrichment makes a handful of read only API calls per error, &lt;code&gt;SimulatePrincipalPolicy&lt;/code&gt;, maybe a &lt;code&gt;GetBucketPolicy&lt;/code&gt;, a &lt;code&gt;LookupEvents&lt;/code&gt;, all of which are effectively free at any sane volume. The optional Bedrock layer is the only thing that costs money, and it is per token and tiny, and it is off by default.&lt;/p&gt;

&lt;p&gt;On safety, the important line is that Rosetta reads your private IAM state, so it is designed to run against your own account with your own role, on your own machine. That is why the product is the repo, not a hosted service. A public endpoint that assumed roles in my account would be a gift to strangers. You run it, you own the credentials, they never leave your laptop.&lt;/p&gt;

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

&lt;p&gt;Rosetta is open source, MIT licensed, and runs against your own account with your own read only role.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/mursalfk/aws-rosetta" rel="noopener noreferrer"&gt;https://github.com/mursalfk/aws-rosetta&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Text only mode needs nothing but Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/mursalfk/aws-rosetta.git
&lt;span class="nb"&gt;cd &lt;/span&gt;aws-rosetta
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate        &lt;span class="c"&gt;# Windows: source .venv/Scripts/activate&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

aws-decode &lt;span class="s2"&gt;"An error occurred (AccessDenied) when calling the GetObject operation: ..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the authoritative mode, create a read only role from &lt;code&gt;infra/readonly-role-policy.json&lt;/code&gt;, trust the principal that runs Rosetta, and pass it with &lt;code&gt;--role-arn&lt;/code&gt;. The scenario stack under &lt;code&gt;scenario/&lt;/code&gt; lets you reproduce the exact before and after from this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually learned
&lt;/h2&gt;

&lt;p&gt;Four things stuck with me.&lt;/p&gt;

&lt;p&gt;Text alone can only ever guess, because the same error genuinely has many causes. The account holds the answer, and one API, &lt;code&gt;SimulatePrincipalPolicy&lt;/code&gt;, will hand it to you authoritatively if you bother to ask.&lt;/p&gt;

&lt;p&gt;Make your tools show their work. My ranking bug survived exactly until I forced the raw decision into the open with a &lt;code&gt;--json&lt;/code&gt; flag. An authoritative claim you cannot inspect is a bug waiting to be trusted.&lt;/p&gt;

&lt;p&gt;Degrade loudly, not silently. The moment the privileged path disappears, say so and fall back. A tool that quietly gives you a worse answer without telling you is worse than one that crashes.&lt;/p&gt;

&lt;p&gt;And when AWS says &lt;code&gt;INVALID_PAYMENT_INSTRUMENT&lt;/code&gt;, go check your billing, not your policies. You are welcome.&lt;/p&gt;

&lt;p&gt;Ciao! 👋&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>iam</category>
      <category>python</category>
    </item>
    <item>
      <title>My Robot Accountant Got an Eviction Notice. So We Moved: Migrating from Bedrock Agents Classic to AgentCore</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Fri, 31 Jul 2026 07:40:04 +0000</pubDate>
      <link>https://dev.to/aws-builders/my-robot-accountant-got-an-eviction-notice-so-we-moved-migrating-from-bedrock-agents-classic-to-4bjo</link>
      <guid>https://dev.to/aws-builders/my-robot-accountant-got-an-eviction-notice-so-we-moved-migrating-from-bedrock-agents-classic-to-4bjo</guid>
      <description>&lt;p&gt;Ciao Amici 👋&lt;/p&gt;

&lt;p&gt;Grab your chai, because the robot accountant is back, and this time we have a moving-day story. If you read my last article, you know I built "The Accountant," a Bedrock Agent that reads my AWS bill every morning, emails me a color-coded report, and once famously reported its own existence to me as suspicious activity.&lt;/p&gt;

&lt;p&gt;That article did well. It did so well, in fact, that it reached exactly the right person to tell me my beloved architecture was standing on borrowed ground. I remember the moment precisely: phone buzzing on the desk, a notification from Dev.to, me expecting another nice comment about the day 16 hallucination. Instead, a community manager from AWS, polite as anything: Bedrock Agents is heading into maintenance mode, consider rewriting this with AgentCore. A link to the announcement sat under it like a small landmine.&lt;/p&gt;

&lt;p&gt;My stomach did the same thing it did the day I found the idle GPU endpoint. So I did what any of us would do: I read the announcement twice, once fast and once slowly, made more chai, and moved the whole thing. This is the complete story of that migration, box by box, with every line of code, what got easier, what got weirder, and what it cost. By the end, you will be able to move your own Classic agent in an afternoon.&lt;/p&gt;




&lt;h2&gt;
  
  
  The eviction notice, in plain language
&lt;/h2&gt;

&lt;p&gt;First, let us be precise about what is actually happening, because "maintenance mode" causes more panic than it should. I know, because I panicked for about ten minutes before actually reading the page.&lt;/p&gt;

&lt;p&gt;Amazon Bedrock Agents, the service my first article was built on, is now officially called &lt;strong&gt;Amazon Bedrock Agents Classic&lt;/strong&gt;, and as of &lt;strong&gt;July 30, 2026&lt;/strong&gt;, it is closed to new customers. If your AWS account has never called &lt;code&gt;CreateAgent&lt;/code&gt;, that API will simply refuse you with an &lt;code&gt;AccessDeniedException&lt;/code&gt; after that date.&lt;/p&gt;

&lt;p&gt;What it does &lt;em&gt;not&lt;/em&gt; mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Existing agents do not stop working. There is no announced end-of-life date.&lt;/li&gt;
&lt;li&gt;Nobody is deleting your agents or your action groups.&lt;/li&gt;
&lt;li&gt;Your bills do not change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it &lt;em&gt;does&lt;/em&gt; mean, and this is the part that matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The service is frozen. No new features, and critically, a &lt;strong&gt;frozen model catalog&lt;/strong&gt;. The models available to Classic agents today are the models it will have forever. Every new model generation from here on will skip Classic entirely.&lt;/li&gt;
&lt;li&gt;Any tutorial that says "go to the Bedrock console and create an agent" is now a historical document for anyone with a fresh account. Including, painfully, mine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second bullet is the real eviction notice. My Accountant would have kept running for years. But an agent that can never get a better brain is an agent on a slowly sinking ship, and an article that new readers cannot follow is not much of an article. Sitting there with my cooling chai, I had to admit it: the community manager was right. Time to pack.&lt;/p&gt;

&lt;p&gt;The destination AWS points everyone toward is &lt;strong&gt;Amazon Bedrock AgentCore&lt;/strong&gt;, and after living in it for a few weeks, I can report that the move is less scary than it sounds, and in a couple of ways it is a genuine upgrade.&lt;/p&gt;




&lt;h2&gt;
  
  
  Meet the new neighbourhood: AgentCore in ninety seconds
&lt;/h2&gt;

&lt;p&gt;AgentCore is not "Bedrock Agents 2." It is a different philosophy. Classic was a &lt;em&gt;managed agent&lt;/em&gt;: you configured an agent in the console, AWS owned the orchestration loop, and your code only existed at the edges, inside action group Lambdas. AgentCore flips this. &lt;strong&gt;Your agent is your code&lt;/strong&gt;, written in any framework you like (Strands Agents, LangGraph, CrewAI, plain Python), and AgentCore provides the production infrastructure around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime&lt;/strong&gt;: a serverless home that runs your agent in an isolated microVM per session, scales it, and charges you only for what you use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gateway&lt;/strong&gt;: turns Lambdas and APIs into MCP tools your agent can call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: managed short-term and long-term memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity&lt;/strong&gt;: OAuth and credential handling for agents that act on someone's behalf.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: tracing for every step and tool call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For The Accountant, a solitary creature with five tools and no users, I needed exactly one of these: &lt;strong&gt;Runtime&lt;/strong&gt;. That is the pleasant surprise of AgentCore for small projects. It is a menu, not a prix fixe. You take the piece you need and ignore the rest until you grow into it.&lt;/p&gt;

&lt;p&gt;The framework I chose for the agent itself is &lt;strong&gt;Strands Agents&lt;/strong&gt;, AWS's open-source agent SDK, mostly because it is the path of least resistance on AgentCore and its tool model is, as you are about to see, almost comically convenient for this migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  The moving map
&lt;/h2&gt;

&lt;p&gt;Before touching code, I sat down one evening with a notebook, the old architecture diagram on one screen and the AgentCore docs on the other, and mapped every piece of the old build to its new home. I suggest you do the same for your agent; the whole migration falls out of this one table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bedrock Agents Classic&lt;/th&gt;
&lt;th&gt;AgentCore + Strands&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent + instructions in the console&lt;/td&gt;
&lt;td&gt;Strands &lt;code&gt;Agent&lt;/code&gt; with a system prompt, in code&lt;/td&gt;
&lt;td&gt;Copy and paste&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action group Lambda + function schemas&lt;/td&gt;
&lt;td&gt;Plain Python functions with &lt;code&gt;@tool&lt;/code&gt;, same process&lt;/td&gt;
&lt;td&gt;Delete code, mostly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;InvokeAgent&lt;/code&gt; API in trigger Lambda&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;InvokeAgentRuntime&lt;/code&gt; API&lt;/td&gt;
&lt;td&gt;Twenty lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Console configuration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;agentcore configure&lt;/code&gt; + &lt;code&gt;agentcore launch&lt;/code&gt; CLI&lt;/td&gt;
&lt;td&gt;New, pleasant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock trace console&lt;/td&gt;
&lt;td&gt;CloudWatch + AgentCore Observability&lt;/td&gt;
&lt;td&gt;Different, fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM read-only policy&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unchanged&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SES email delivery&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unchanged&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EventBridge schedule&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unchanged&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read that Effort column again. I did, three times, waiting for the catch. The two things I was most protective of, the read-only IAM philosophy and the email delivery my mornings now depend on, moved without a single edit. The heart of the migration is really just one move: the action group Lambda dissolves, and its functions walk across the street to live inside the agent itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Packing the boxes: the new agent
&lt;/h2&gt;

&lt;p&gt;Here is the entire agent, one file, &lt;code&gt;main.py&lt;/code&gt;. If you read the first article, you will recognize every boto3 call, because they are the same functions that used to live in the action group Lambda. The difference: they are now decorated with &lt;code&gt;@tool&lt;/code&gt; and run in the same process as the agent's reasoning. No Lambda packaging, no function schemas to declare in a console, no &lt;code&gt;messageVersion&lt;/code&gt; response envelopes. The docstring &lt;em&gt;is&lt;/em&gt; the schema; Strands hands it to the model as the tool description.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bedrock_agentcore.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;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&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;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;ce&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;ce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sm&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;sagemaker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ec2&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;ec2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_daily_costs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch daily AWS costs grouped by service for the last N days,
    in a single Cost Explorer call. Returns JSON.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cost_and_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TimePeriod&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;Start&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;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;End&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;end&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="n"&gt;Granularity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DAILY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Metrics&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;UnblendedCost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;GroupBy&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;Type&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;DIMENSION&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;Key&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;SERVICE&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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ResultsByTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;grp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Groups&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Metrics&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;UnblendedCost&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;Amount&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;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TimePeriod&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;Start&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;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&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;found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_forecast&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch the Cost Explorer forecast for the remainder of the
    current month. Returns JSON.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&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="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cost_forecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TimePeriod&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;Start&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;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;End&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;end&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="n"&gt;Metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNBLENDED_COST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Granularity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MONTHLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projected_remaining_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_sagemaker_endpoints&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;List all SageMaker inference endpoints with status and
    creation time. Returns JSON with an explicit count.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;eps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_endpoints&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Endpoints&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EndpointName&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;status&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EndpointStatus&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;created&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CreationTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eps&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;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;found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_unattached_volumes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Find EBS volumes in &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;available&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; state (attached to nothing,
    still billing). Returns JSON with an explicit count.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;vols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe_volumes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;available&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;Volumes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VolumeId&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;size_gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Size&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;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VolumeType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vols&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;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;found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_unattached_eips&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Find Elastic IPs not associated with any instance.
    Returns JSON with an explicit count.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;addrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe_addresses&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Addresses&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;items&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;ip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PublicIp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;addrs&lt;/span&gt;
             &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AssociationId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&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;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;found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a cautious FinOps analyst reviewing a
personal AWS account. The owner is a solo developer running ML
experiments, a portfolio site, and occasional demos. Typical
monthly spend is 60 to 120 USD.

Every day you will:
1. Fetch the last 14 days of daily costs grouped by service,
   in a single call.
2. Compare today&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s trajectory against the trailing average.
3. Check idle-prone resources: SageMaker endpoints, unattached
   EBS volumes and Elastic IPs.
4. Fetch the month-end forecast.
5. Report findings in this exact structure:
   STATUS: green / yellow / red
   HEADLINE: one sentence
   ANOMALIES: bullet list, or &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
   RECOMMENDATIONS: bullet list with estimated monthly savings
   as a range for each, or &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
   FORECAST: projected month-end total
   WATCHER OVERHEAD: today&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s estimated cost of running this
   analysis

Rules:
- Never recommend an action you cannot support with data you
  fetched in this session. If a tool returned found: 0, there
  is nothing to recommend about those items.
- If you are unsure whether a resource is intentional, say so
  and ask. Do not assume.
- Distinguish between &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spend increased&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; and &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spend is anomalous&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.
  A planned training run is not an anomaly.
- State every savings estimate as a range, never a point figure.
- Your own model and runtime usage is expected. Report it under
  WATCHER OVERHEAD, never under ANOMALIES.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;get_daily_costs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;get_forecast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;list_sagemaker_endpoints&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;find_unattached_volumes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;find_unattached_eips&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.entrypoint&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;prompt&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="s"&gt;Run the daily billing review.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent&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="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;report&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&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;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;p&gt;Notice three things while the tape gun is still warm.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;the system prompt is my old Classic instruction prompt, nearly verbatim.&lt;/strong&gt; All five versions of scar tissue from the first experiment, the ranges rule, the "found: 0 means nothing to recommend" rule, the watcher-overhead rule born the day the agent reported itself, all of it transferred untouched. Prompts are luggage. They fly free.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;Chekhov's empty list is now structural.&lt;/strong&gt; In the first article, the day 16 hallucination (the agent inventing an idle RDS instance from an empty response) taught me that tools must return explicit counts. In the new build, every tool returns &lt;code&gt;{"found": N, "items": [...]}&lt;/code&gt; by construction. Some lessons you write in a prompt. Better lessons you write in the return type.&lt;/p&gt;

&lt;p&gt;Third, the &lt;code&gt;@app.entrypoint&lt;/code&gt; wrapper is the entire integration with AgentCore. Three lines. &lt;code&gt;BedrockAgentCoreApp&lt;/code&gt; handles the HTTP server, the session plumbing, and the container contract so that this file runs identically on my laptop and in the cloud.&lt;/p&gt;




&lt;h2&gt;
  
  
  The moving truck: deploying with the starter toolkit
&lt;/h2&gt;

&lt;p&gt;This is where AgentCore genuinely out-classes Classic. With Classic, "development" meant clicking through the console, saving, preparing, testing in the chat panel, and squinting at traces. With AgentCore, my agent is a Python file I can run locally like any other Python file:&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%2Fphokw3jjv67wt7srjxsu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphokw3jjv67wt7srjxsu.jpg" alt="migration map" width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;bedrock-agentcore strands-agents bedrock-agentcore-starter-toolkit

python main.py
&lt;span class="c"&gt;# in another terminal:&lt;/span&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": "Run the daily billing review."}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want you to picture this moment, because it is the moment the migration stopped feeling like a chore. Two terminals side by side on my screen, close to midnight. In the left one, my agent, running as an ordinary Python process. In the right one, a curl command. I pressed enter, and thirty seconds later I was watching my agent call Cost Explorer from my own machine, with my own credentials, printing its reasoning line by line as it thought. The development loop went from minutes per iteration to seconds. This alone justified the move for me emotionally, whatever the maintenance-mode situation.&lt;/p&gt;

&lt;p&gt;When it behaves locally, deployment is two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore configure &lt;span class="nt"&gt;-e&lt;/span&gt; main.py
agentcore launch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;configure&lt;/code&gt; asks a few questions, writes a &lt;code&gt;.bedrock_agentcore.yaml&lt;/code&gt;, and, helpfully, creates the IAM execution role for you. &lt;code&gt;launch&lt;/code&gt; packages the agent and hosts it on AgentCore Runtime, returning an agent runtime ARN. One important errand before celebrating: go attach the read-only billing policy from the first article to that execution role, since the agent's boto3 calls now run under it instead of under an action group Lambda's role. Same policy, new badge holder. The zero-write-permissions philosophy survives the move fully intact:&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%2Fy6ffnnmf0si9q4xh56ur.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%2Fy6ffnnmf0si9q4xh56ur.png" alt="The Accountant v2 architecture on AgentCore Runtime" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A quick sanity check from 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="s1"&gt;'{"prompt": "Run the daily billing review."}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there it was: STATUS: green, in a brand-new house, first try.&lt;/p&gt;

&lt;p&gt;Well. Second try. The first try taught me the quirk below.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one box that rattled: sessions
&lt;/h2&gt;

&lt;p&gt;Every migration has one. Mine was the runtime session ID.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;InvokeAgentRuntime&lt;/code&gt; requires a &lt;code&gt;runtimeSessionId&lt;/code&gt;, and it must be at least 33 characters long. My first invocation passed the same short daily ID string the old trigger Lambda used, and the API rejected it with a validation error that took me an embarrassing amount of squinting to parse. I read it four times, convinced the problem was my ARN, my region, my IAM, anything but the humble session string. It was the session string. The fix is a UUID4, which is 36 characters and therefore sails through.&lt;/p&gt;

&lt;p&gt;There is also a conceptual shift hiding in that parameter. In Classic, a session was a conversational thread the service tracked for you. In AgentCore Runtime, the session maps to an isolated microVM; same ID means same warm sandbox, new ID means a fresh one. For The Accountant this is a feature: a fresh UUID every morning gives me the same blank-slate reproducibility I insisted on in the first experiment, now enforced by actual infrastructure isolation rather than by my own discipline.&lt;/p&gt;

&lt;p&gt;Here is the migrated trigger Lambda, the only other code that changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;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;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;

&lt;span class="n"&gt;agent_core&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;ses&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;ses&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;COLORS&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;green&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;#2eb67d&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;yellow&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;#ecb22e&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;red&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;#e01e5a&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;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run the daily billing review.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent_core&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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_RUNTIME_ARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;runtimeSessionId&lt;/span&gt;&lt;span class="o"&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;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;  &lt;span class="c1"&gt;# 33+ chars required
&lt;/span&gt;        &lt;span class="n"&gt;qualifier&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&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&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;response&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;chunks&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;chunk&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;report&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="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&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;report&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yellow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&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;line&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="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STATUS:&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    &amp;lt;div style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;font-family:Menlo,Consolas,monospace;max-width:640px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
      &amp;lt;div style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;background:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;COLORS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#ecb22e&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="s"&gt;;
                  color:#fff;padding:10px 16px;border-radius:6px;
                  font-weight:bold;font-size:16px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
        The Accountant &amp;amp;middot; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;pre style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;white-space:pre-wrap;font-size:13px;
                  line-height:1.55;padding:8px 4px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/pre&amp;gt;
    &amp;lt;/div&amp;gt;
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;ses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FROM_ADDRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;Destination&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;ToAddresses&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TO_ADDRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]},&lt;/span&gt;
        &lt;span class="n"&gt;Message&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;Subject&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;Data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The Accountant · &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; · daily review&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;Body&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;Html&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;Data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;bedrock:InvokeAgent&lt;/code&gt; for &lt;code&gt;bedrock-agentcore:InvokeAgentRuntime&lt;/code&gt; in this Lambda's role, point the EventBridge schedule at it, and the morning ritual continues without missing a day. The SES block is character-for-character the one from the first article. My inbox never even noticed the agent changed houses.&lt;/p&gt;




&lt;h2&gt;
  
  
  The shadow week
&lt;/h2&gt;

&lt;p&gt;I did not flip a switch and hope. For seven days I ran both accountants in parallel, old and new, each reviewing the same account every morning, reports arriving side by side in my inbox. Two subject lines, same status word, a minute apart. There was something quietly funny about it, like watching an employee train their own replacement without knowing it. I recommend this to anyone migrating an agent that they actually rely on; it costs a few dollars and buys real confidence.&lt;/p&gt;

&lt;p&gt;Results of the parity check, honestly reported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Six of seven days: functionally identical findings.&lt;/strong&gt; Same status, same anomalies or lack of them, forecasts within a dollar or two of each other. Different sentence rhythms, since the orchestration loops differ, but the same accountant's judgment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One divergence, and it favored the new build.&lt;/strong&gt; Mid-week I terminated a test instance and its Elastic IP sat unattached for a day. Both agents caught it. The v2 agent, though, reported it as "found: 1 unattached Elastic IP" with the address, quoting the tool's count directly in its reasoning. Watching the explicit-count design pay off in the wild, in the exact failure category that produced the day 16 hallucination last time, was the single most satisfying moment of the migration. I may have said "yes" out loud to an empty room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero hallucinated resources on either side all week.&lt;/strong&gt; The scar tissue holds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On day eight I deleted the Classic agent. A small pang, honestly, my cursor hovering over the confirmation button a beat longer than necessary. It found me fifty-five dollars a month and once accused itself of fraud. But its model catalog was frozen in time, and mine is not. Delete.&lt;/p&gt;




&lt;h2&gt;
  
  
  The receipts, moving-day edition
&lt;/h2&gt;

&lt;p&gt;What did the move cost in time and money?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time: one honest afternoon.&lt;/strong&gt; About four hours end to end, and half of that was me reading AgentCore docs out of curiosity rather than necessity. The mechanical work, converting the Lambda functions to &lt;code&gt;@tool&lt;/code&gt; functions, deploying, updating the trigger, was maybe ninety minutes. Your first migration will be slower than your second; the second is nearly mechanical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Money: a rounding error.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;v1 · Classic&lt;/th&gt;
&lt;th&gt;v2 · AgentCore&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model tokens (Bedrock)&lt;/td&gt;
&lt;td&gt;~$3.62&lt;/td&gt;
&lt;td&gt;~$3.55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost Explorer API calls&lt;/td&gt;
&lt;td&gt;~$1.05&lt;/td&gt;
&lt;td&gt;~$1.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent runtime compute&lt;/td&gt;
&lt;td&gt;~$0.08 (Lambda)&lt;/td&gt;
&lt;td&gt;~$0.31 (Runtime, per-second)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda, EventBridge, SES&lt;/td&gt;
&lt;td&gt;~$0.12&lt;/td&gt;
&lt;td&gt;~$0.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$4.87&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$5.02&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3zah9299kygy8qz4ejjq.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%2F3zah9299kygy8qz4ejjq.png" alt="Cost comparison: the move barely touched the bill" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AgentCore Runtime bills for consumed CPU and memory per second while the agent is actually working, and a sixty-second daily run is simply not much of either. Fifteen cents a month is what my accountant's new apartment costs over the old one. The dominant cost remains, as it always was, the model tokens, and those are identical either way. The economics of the whole idea, five dollars to find fifty-five, are untouched by the migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  What got better, what got weirder
&lt;/h2&gt;

&lt;p&gt;The honest ledger, because a migration story that reads like a brochure is worthless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Genuinely better:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The local dev loop.&lt;/strong&gt; Running the agent as a plain Python process and curling it on localhost changed how it feels to work on this project. Prompt iteration that used to mean console round-trips now happens at the speed of saving a file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools as functions.&lt;/strong&gt; The action group ceremony (Lambda packaging, function schemas, response envelopes) is gone. A docstring and a decorator. I deleted more code than I wrote in this migration, which is my favorite genre of engineering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The frozen-catalog problem is solved by design.&lt;/strong&gt; The model is a string in my code now. When a better one ships, I change one line and relaunch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structural guardrails.&lt;/strong&gt; Explicit counts in tool returns, enforced by construction rather than by prompt discipline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Weirder, or at least new:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 33-character session ID rule.&lt;/strong&gt; Documented, but it will get you once. It got me at 11:40 PM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A new mental model.&lt;/strong&gt; Classic made the agent feel like an AWS resource you configure. AgentCore makes it a program you own. I prefer the second, but it is a real shift, and teams who liked the console-first workflow will feel it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One more CLI and one more hidden YAML&lt;/strong&gt; (&lt;code&gt;.bedrock_agentcore.yaml&lt;/code&gt;) in your life. Pleasant, but it is another thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability lives in a different place.&lt;/strong&gt; The Classic trace console was crude but one click away. Now I read traces through CloudWatch and AgentCore's observability tooling, which is more powerful and slightly more assembly-required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;And unchanged, by design:&lt;/strong&gt; the read-only IAM policy, the SES delivery, the EventBridge schedule, the system prompt, and the entire philosophy. The agent looks and complains. The human acts. No maintenance-mode announcement will ever change that rule.&lt;/p&gt;




&lt;h2&gt;
  
  
  Should you move yours?
&lt;/h2&gt;

&lt;p&gt;If you have a Bedrock Agents Classic agent running today, here is my honest triage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move soon if&lt;/strong&gt; your agent is something you are still actively improving, or if your content, docs, or team onboarding depend on others being able to recreate it. New accounts cannot build Classic agents after July 30, 2026, and the frozen model catalog means your agent's ceiling is already set. The migration is an afternoon; the mapping table above is most of the thinking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move eventually if&lt;/strong&gt; your agent is stable and internal. Nothing breaks tomorrow. But put it on the roadmap, because every quarter you wait, the gap between the frozen catalog and the current model frontier widens, and that gap is your agent quietly getting relatively dumber.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And if you are starting fresh:&lt;/strong&gt; there is no decision to make. Start on AgentCore, pick a framework, and enjoy the local dev loop from day one. The first article's &lt;em&gt;ideas&lt;/em&gt; (read-only tools, explicit counts, ranges not points, teach the agent your normal) all apply verbatim; only the plumbing in it is now historical.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So, guys, that is the story of moving day. It started with a comment on my last article that briefly ruined my evening and ended, one afternoon of work later, with the same accountant in a better house: the same paranoid morning reports, the same read-only leash, the same five-dollar salary, but now with a dev loop I actually enjoy, tools that are just Python functions, and a brain that can be upgraded with a one-line change instead of being frozen in 2026 forever.&lt;/p&gt;

&lt;p&gt;I owe genuine thanks to the community manager who nudged me, because "your architecture is deprecated" is a gift when it arrives early enough to do something about it, and I would much rather write a migration story than a post-mortem. If you have a Classic agent of your own, take the moving map, block out an afternoon, and give your robot its new keys; then come tell me how it went, because the follow-up to this article will feature the best migration stories I hear. Find me on Dev.to or LinkedIn. Until then: keep your permissions read-only, your tool counts explicit, and your model catalogs unfrozen.&lt;/p&gt;

&lt;p&gt;Ciao! 👋&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>machinelearning</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Let a Bedrock Agent Watch My AWS Bill for 30 Days. Here Is Everything It Caught, Missed, and Made Up</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Sun, 05 Jul 2026 20:07:54 +0000</pubDate>
      <link>https://dev.to/aws-builders/i-let-a-bedrock-agent-watch-my-aws-bill-for-30-days-here-is-everything-it-caught-missed-and-made-1lde</link>
      <guid>https://dev.to/aws-builders/i-let-a-bedrock-agent-watch-my-aws-bill-for-30-days-here-is-everything-it-caught-missed-and-made-1lde</guid>
      <description>&lt;p&gt;Ciao Amici 👋&lt;/p&gt;

&lt;p&gt;Pull up a chair, because I have a story for you. It involves a forgotten GPU endpoint, a robot accountant I built over two evenings, a NAT gateway that quietly ate my wallet, and one genuinely absurd moment where my own creation reported &lt;em&gt;itself&lt;/em&gt; to me as suspicious activity. For thirty days I let an Amazon Bedrock Agent read my AWS bill every morning and tell me what it thought, and by the end of this story you will have every line of code, every chart, every dollar figure, and every embarrassing mistake I made along the way. Some of those mistakes were mine. Two of them, delightfully, were the agent's. Make some chai, this is a long one, and I promise it earns its length.&lt;/p&gt;




&lt;h2&gt;
  
  
  The morning it all started
&lt;/h2&gt;

&lt;p&gt;Last November I opened my AWS billing dashboard and felt that very specific cold feeling in the stomach that every cloud engineer knows. A SageMaker real-time endpoint I had spun up for a quick demo had been running for eleven days. Eleven days of me paying for a GPU-backed instance to serve exactly zero requests.&lt;/p&gt;

&lt;p&gt;The endpoint cost me about $38. Not catastrophic. But here is the thing: it was the third time in a year this movie had played. Before that, an Elastic IP sat unattached for a month. Before that, a NAT gateway I had completely forgotten was part of a VPC I built for a workshop. Each time, the same script: I discover the charge weeks later, feel briefly stupid, delete the thing, and promise myself I will check the console more often. I never do, because manually checking a billing console is exactly the kind of chore human brains are engineered to avoid.&lt;/p&gt;

&lt;p&gt;And yes, I had CloudWatch billing alerts configured, like a responsible adult. But a threshold alert only tells you &lt;em&gt;that&lt;/em&gt; you are spending money. It never tells you &lt;em&gt;why&lt;/em&gt;, and it definitely never tells you what to do about it. An alert saying "your bill crossed $80" at 2 AM is not insight. It is anxiety with a timestamp.&lt;/p&gt;

&lt;p&gt;So, staring at that dead endpoint, an idea landed. What if I stopped being my own billing alarm entirely? What if, every morning, someone else read the bill for me, someone tireless, slightly paranoid, and immune to the phrase "I will deal with it after the weekend"?&lt;/p&gt;

&lt;p&gt;That someone became a Bedrock Agent. For 30 days I gave it read-only access to Cost Explorer and a handful of describe APIs, scheduled it to run every morning, and told it to behave like a cautious FinOps consultant. Every day at 9 AM Karachi time it would analyze my spend, flag anything weird, estimate savings, and email me a report with the subject line "The Accountant," because if a robot is going to lecture me about money, it should at least have a title.&lt;/p&gt;

&lt;p&gt;This is the full story of that month. Spoiler: I am still running it. Longer spoiler: it once found a database that does not exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why an agent, and not just budgets and alarms?
&lt;/h2&gt;

&lt;p&gt;A colleague asked me this on day two, with a raised eyebrow that suggested he already knew part of my answer would be "because it sounded fun."&lt;/p&gt;

&lt;p&gt;It did sound fun. But there is a real gap here, and it decides whether this whole pattern is a toy or a tool, so let me make the case properly.&lt;/p&gt;

&lt;p&gt;AWS Budgets, billing alarms, even Cost Anomaly Detection: these are threshold and statistics tools. They compare numbers to lines. They are excellent at "did I spend more than X" and reasonable at "is today statistically unusual." What they cannot do is answer the questions that actually eat your time when a bill goes wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why did my bill go up 40% this week, in plain language?&lt;/li&gt;
&lt;li&gt;Is this NAT gateway data processing charge normal for my workload, or a routing misconfiguration?&lt;/li&gt;
&lt;li&gt;Which of my resources are technically running but functionally idle?&lt;/li&gt;
&lt;li&gt;If I keep going at this rate, what does month-end look like, and which line items are driving it?&lt;/li&gt;
&lt;li&gt;Is this spike a problem, or the training run I kicked off on purpose yesterday?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters more than people admit. Anomaly detection without context produces alerts you learn to ignore, and an ignored alert is worse than no alert. The difference between "spend increased" and "spend is wrong" is a judgment call, and judgment calls require reasoning over context. That is exactly the shape of problem LLM agents are decent at, provided you feed them structured data and keep them on a very short leash.&lt;/p&gt;

&lt;p&gt;The short leash part is not a figure of speech. It is the entire safety story of this project, and you will see it earn its keep on day 16.&lt;/p&gt;

&lt;p&gt;There was a quieter motivation too. I spend a lot of my community time around ML tooling, and I kept seeing agent demos that book flights or order pizza, tasks nobody actually wants automated by a nondeterministic system. Billing analysis felt like the opposite case: tedious, recurring, data-rich, and low-stakes as long as the agent can only &lt;em&gt;read&lt;/em&gt;. If agents are useful anywhere in a personal AWS account, they should be useful here. Consider the next five thousand words the test of that hypothesis.&lt;/p&gt;




&lt;h2&gt;
  
  
  Meet The Accountant
&lt;/h2&gt;

&lt;p&gt;Here is the whole system. It is deliberately boring, and every boring choice was on purpose.&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%2Fb0f9i4n8e3wiynnj5d1d.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%2Fb0f9i4n8e3wiynnj5d1d.png" alt="Architecture: EventBridge triggers a Lambda that invokes a Bedrock Agent, which calls a read-only action group Lambda and emails a daily report via SES" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Five pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge Scheduler&lt;/strong&gt; fires once a day at 04:00 UTC, which is 9 AM for me in Karachi. Daily is the right cadence for a personal account. Hourly would multiply the watcher's own cost by 24 for almost no extra insight, because Cost Explorer data lags a few hours anyway.&lt;/li&gt;
&lt;li&gt;A small &lt;strong&gt;trigger Lambda&lt;/strong&gt; invokes the Bedrock Agent with a fixed daily prompt, collects the streamed response, and forwards the final report.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Bedrock Agent&lt;/strong&gt; (I used Claude via Bedrock, though the pattern works with any agent-capable model on the platform) reads its instructions, decides which tools it needs, calls them, and reasons over the results.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;action group Lambda&lt;/strong&gt; is the only component with AWS API access. It exposes a small menu of tools: get cost and usage, get cost forecast, list SageMaker endpoints, list EC2 instances, describe NAT gateways, find unattached EBS volumes and Elastic IPs, and describe RDS instances.&lt;/li&gt;
&lt;li&gt;The agent's final report lands in &lt;strong&gt;my inbox&lt;/strong&gt; via Amazon SES, formatted as HTML with a color-coded status banner, and impossible for me to claim I did not see. Email was a deliberate choice over chat tools: I check my inbox every morning anyway, it keeps the whole stack inside AWS, and a month of reports becomes a searchable archive for free.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now the tattoo-worthy design decision, the one I would print inside the cover of every agent tutorial if publishers let me: &lt;strong&gt;the agent has zero write permissions.&lt;/strong&gt; It cannot stop, terminate, resize, delete, or modify anything. It can only look and complain. I wanted a consultant, not an intern with root access.&lt;/p&gt;

&lt;p&gt;Hold that thought. It becomes the hero of this story later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building my robot accountant
&lt;/h2&gt;

&lt;p&gt;Let me walk you through the build in the order I actually did it, including the parts that wasted an evening, because the wasted evenings are where the useful knowledge lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: The IAM policy
&lt;/h3&gt;

&lt;p&gt;Everything the action group Lambda can do fits in one tight policy. Read it, and notice what is &lt;em&gt;absent&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CostVisibility"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ce:GetCostAndUsage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ce:GetCostForecast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ce:GetDimensionValues"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ResourceVisibility"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeInstances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeVolumes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeNatGateways"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeAddresses"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"sagemaker:ListEndpoints"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"sagemaker:DescribeEndpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"rds:DescribeDBInstances"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;ec2:StopInstances&lt;/code&gt;. No &lt;code&gt;sagemaker:DeleteEndpoint&lt;/code&gt;. Nothing with a verb that changes state. Whenever the agent recommended killing something, a human (me, holding coffee) did the killing after a ten-second sanity check.&lt;/p&gt;

&lt;p&gt;One small subtlety so you do not repeat my twenty wasted minutes: &lt;code&gt;ce:*&lt;/code&gt; actions only work against &lt;code&gt;Resource: "*"&lt;/code&gt; because Cost Explorer is account-scoped, so do not bother trying to narrow it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: The action group Lambda
&lt;/h3&gt;

&lt;p&gt;Bedrock Agents call your Lambda with a structured event describing which tool the model wants and with what parameters. Your job is to run the right boto3 calls and hand back JSON. Here is a trimmed version of my handler; the full version just has more entries in the tool table and better logging.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;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;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;ce&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;ce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sm&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;sagemaker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ec2&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;ec2&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;get_daily_costs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Cost per service per day for the last N days, one API call.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cost_and_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TimePeriod&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;Start&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;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;End&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;end&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="n"&gt;Granularity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DAILY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Metrics&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;UnblendedCost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;GroupBy&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;Type&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;DIMENSION&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;Key&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;SERVICE&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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ResultsByTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;grp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Groups&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Metrics&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;UnblendedCost&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;Amount&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;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# skip fractional-cent noise
&lt;/span&gt;                &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TimePeriod&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;Start&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;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_forecast&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&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="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# last day of month
&lt;/span&gt;    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_cost_forecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TimePeriod&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;Start&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;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;End&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;end&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="n"&gt;Metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNBLENDED_COST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Granularity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MONTHLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projected_remaining_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_sagemaker_endpoints&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;eps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_endpoints&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Endpoints&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="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EndpointName&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;status&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EndpointStatus&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;created&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CreationTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eps&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;find_unattached_volumes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;vols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe_volumes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;available&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;Volumes&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VolumeId&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;size_gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Size&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;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VolumeType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vols&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;find_unattached_eips&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;addrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe_addresses&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Addresses&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PublicIp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;addrs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AssociationId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_daily_costs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_daily_costs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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;days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_forecast&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_forecast&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list_sagemaker_endpoints&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;list_sagemaker_endpoints&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;find_unattached_volumes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;find_unattached_volumes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;find_unattached_eips&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;find_unattached_eips&lt;/span&gt;&lt;span class="p"&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;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&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;parameters&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&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;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown function &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})(&lt;/span&gt;&lt;span class="n"&gt;params&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;messageVersion&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;1.0&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;response&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;actionGroup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actionGroup&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;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;functionResponse&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;responseBody&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;TEXT&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;body&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="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)}}&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lessons from this file, each purchased with part of an evening:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost Explorer calls cost money.&lt;/strong&gt; Each &lt;code&gt;GetCostAndUsage&lt;/code&gt; request bills at $0.01. That sounds like nothing until your agent decides to be thorough and makes forty calls in one session, fetching one day at a time. I added an explicit instruction telling it to fetch 14 days in a single call. After that, the system settled at six to eight Cost Explorer calls per day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empty results need to say they are empty.&lt;/strong&gt; Early on, my tools returned a bare &lt;code&gt;[]&lt;/code&gt; when nothing was found. Later I changed them to return &lt;code&gt;{"found": 0, "items": []}&lt;/code&gt; with an explicit count. Remember this detail. It is Chekhov's empty list, and it goes off on day 16.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: The agent itself
&lt;/h3&gt;

&lt;p&gt;In the Bedrock console: create an agent, pick a model, paste instructions, attach the Lambda as an action group, and define each function with its parameters. The console generates the function schema for you these days, which removed most of the friction this setup used to have.&lt;/p&gt;

&lt;p&gt;The instruction prompt matters more than any other component in this system. Mine went through five versions, and the diffs between them tell the real story of the month. This is where it ended up, lightly condensed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a cautious FinOps analyst reviewing a personal AWS account.
The owner is a solo developer running ML experiments, a portfolio
site, and occasional demos. Typical monthly spend is 60 to 120 USD.

Every day you will:
1. Fetch the last 14 days of daily costs grouped by service,
   in a single call.
2. Compare today's trajectory against the trailing average.
3. Check idle-prone resources: SageMaker endpoints, unattached
   EBS volumes and Elastic IPs, NAT gateways, and stopped EC2
   instances with attached storage.
4. Fetch the month-end forecast.
5. Report findings in this exact structure:
   STATUS: green / yellow / red
   HEADLINE: one sentence
   ANOMALIES: bullet list, or "none"
   RECOMMENDATIONS: bullet list with estimated monthly savings
   as a range for each, or "none"
   FORECAST: projected month-end total
   WATCHER OVERHEAD: today's estimated cost of running this
   analysis (Bedrock + Cost Explorer calls)

Rules:
- Never recommend an action you cannot support with data you
  fetched in this session. If a tool returned zero items, there
  is nothing to recommend about those items.
- If you are unsure whether a resource is intentional, say so
  and ask. Do not assume.
- Distinguish between "spend increased" and "spend is anomalous".
  A planned training run is not an anomaly.
- State every savings estimate as a range, never a point figure.
- Your own Bedrock and Lambda usage is expected. Report it under
  WATCHER OVERHEAD, never under ANOMALIES.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that prompt reads like it was written by someone who got burned, good instincts. Every rule after the numbered list was added mid-experiment in response to a specific failure. Scar tissue, in prompt form. The stories behind those scars are coming.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: The trigger Lambda and email delivery
&lt;/h3&gt;

&lt;p&gt;The trigger Lambda is short. It invokes the agent, collects the streamed chunks, and sends the report through Amazon SES as an HTML email with a status-colored banner at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;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="n"&gt;agent_rt&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-agent-runtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ses&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;ses&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;COLORS&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;green&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;#2eb67d&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;yellow&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;#ecb22e&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;red&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;#e01e5a&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;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent_rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;agentId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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_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;agentAliasId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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_ALIAS_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;sessionId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aws_request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;inputText&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run the daily billing review.&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;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completion&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;report&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk&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;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yellow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&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;line&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="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STATUS:&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    &amp;lt;div style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;font-family:Menlo,Consolas,monospace;max-width:640px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
      &amp;lt;div style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;background:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;COLORS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#ecb22e&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="s"&gt;;
                  color:#fff;padding:10px 16px;border-radius:6px;
                  font-weight:bold;font-size:16px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
        The Accountant &amp;amp;middot; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;pre style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;white-space:pre-wrap;font-size:13px;
                  line-height:1.55;padding:8px 4px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/pre&amp;gt;
    &amp;lt;/div&amp;gt;
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;ses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FROM_ADDRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;Destination&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;ToAddresses&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TO_ADDRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]},&lt;/span&gt;
        &lt;span class="n"&gt;Message&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;Subject&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;Data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The Accountant · &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; · daily review&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;Body&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;Html&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;Data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Lambda's execution role needs one extra permission, &lt;code&gt;ses:SendEmail&lt;/code&gt;, and that is the only write-adjacent permission in the whole system. It writes to my inbox, nothing else.&lt;/p&gt;

&lt;p&gt;One SES gotcha before you build: new SES accounts start in sandbox mode, meaning both sender &lt;em&gt;and&lt;/em&gt; recipient addresses must be verified identities. For this project that is actually fine, even pleasant. Verify your own address once in the SES console, click the confirmation link, done. You never need production access because you are only ever emailing yourself. The rare case where the sandbox is a feature.&lt;/p&gt;

&lt;p&gt;The status word in the subject line turned out to be an underrated detail. After a week my inbox itself became a dashboard: a column of GREEN subjects I could skim past, with the occasional YELLOW or RED that actually earned a tap. And unlike a chat message, the reports pile up into a searchable archive. Mid-experiment I searched "NAT" in my mail app and had the full paper trail of an incident in three seconds.&lt;/p&gt;

&lt;p&gt;Note the &lt;code&gt;sessionId&lt;/code&gt;: fresh one per day, so each morning is a blank slate. I considered giving it memory of previous reports and decided against it for the experiment, because I wanted every finding to be reproducible from that day's data alone. Version two revisits this, and I will tell you why at the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefer WhatsApp instead?
&lt;/h3&gt;

&lt;p&gt;Two people asked me this before the experiment even ended, which around here is not surprising; WhatsApp is where life happens. It works, with one honesty caveat: the AWS-native route, AWS End User Messaging Social, requires linking a WhatsApp Business Account, and Meta's business verification is the tedious part, not the code. Once linked, the swap is a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;social&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;socialmessaging&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;social&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_whatsapp_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;originationPhoneNumberId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WA_PHONE_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;metaApiVersion&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v20.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&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;messaging_product&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;whatsapp&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;to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_NUMBER&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;type&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;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The Accountant · &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&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\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;report&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="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you just want it working in ten minutes for personal use, the Twilio WhatsApp sandbox is the pragmatic shortcut: one POST request from the same Lambda, no Meta paperwork. I stayed with email because it kept the entire stack inside one AWS account and gave me the archive, but a RED alert buzzing your phone on WhatsApp is a legitimately better interrupt for genuine emergencies. The architecture does not care; delivery is the last five lines of one Lambda, so pick the channel you actually look at.&lt;/p&gt;

&lt;p&gt;Total build time, including the prompt fiddling and the email formatting I was unreasonably fussy about: around six hours across two evenings. Then I scheduled it, went to sleep, and waited for my new employee's first day.&lt;/p&gt;




&lt;h2&gt;
  
  
  The diary
&lt;/h2&gt;

&lt;p&gt;Now the part you actually came for. I kept notes every day of the experiment. Here are the highlights, week by week. All figures come from my account during the experiment window; your account will tell its own story, which is rather the point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 1: The agent finds my dirty laundry immediately
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Day 1.&lt;/strong&gt; First report lands in my inbox at 9:02 AM, YELLOW glaring from the subject line. It flagged a SageMaker endpoint named &lt;code&gt;vitb16-demo-endpoint&lt;/code&gt; that had been running for six days. Estimated waste: $28 to $34 per month. I knew this one. It was serving a palmprint recognition demo I had shown a colleague exactly once, weeks after building the model, and I had told myself I would take it down "after the weekend." The agent did not care about my intentions. I killed the endpoint before finishing my tea. First blood to the robot, on its first morning, in under two minutes of compute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 2.&lt;/strong&gt; Green. The report was three lines long. I felt an entirely irrational sense of approval, like passing a surprise inspection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 3.&lt;/strong&gt; Green status, but with a line I did not expect: "Note: 3 unattached gp2 volumes totaling 240 GB, approximately $24/month. These may be remnants of terminated training instances." They were exactly that. Leftovers from my thesis experiments, volumes from terminated GPU instances where &lt;code&gt;DeleteOnTermination&lt;/code&gt; had been false. I genuinely did not know these existed. They do not appear anywhere you casually look; they just sit in the EC2 console under a tab nobody visits, quietly billing. I deleted two immediately and snapshotted the third before deleting it, in case it held a dataset I cared about. It held nothing. It never does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 5.&lt;/strong&gt; The agent noted my Route 53 hosted zone and domain charges as "consistent and expected for a portfolio site" and then never mentioned them again all month. Small thing, but this is the moment I started trusting it. It had built a sense of what normal looked like and stopped crying wolf about it. Threshold alarms cannot do this. They have no concept of "expected."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 7.&lt;/strong&gt; End of week one. Total recurring waste identified: roughly $55 to $60 per month, on an account averaging around $107. Sit with that for a second. More than half my baseline spend was waste, and an LLM found essentially all of it in its first three mornings. I had audited this account myself two months earlier and missed the volumes completely. My pride recovered. Eventually.&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%2Ftlgpa1bk2snqfrgumoif.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%2Ftlgpa1bk2snqfrgumoif.png" alt="Daily spend across the 30-day experiment, with annotated incidents" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 2: The NAT gateway incident
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Day 8.&lt;/strong&gt; Status yellow, trending toward alarmed. "EC2-Other charges increased significantly day-over-day."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 9.&lt;/strong&gt; Status red. Headline: "EC2-Other charges increased 6x over the trailing average, driven by NAT gateway data processing."&lt;/p&gt;

&lt;p&gt;Here is the real-life situation behind it, and it is the kind of mistake that is obvious only in hindsight. I had deployed a scraper into a private subnet for a dataset collection job, pulling Sindhi news articles for a language benchmark project I have been building on the side. Private subnet felt like the responsible default. But private subnet means every byte of outbound traffic transits the NAT gateway, and NAT gateways charge $0.045 per GB processed on top of their hourly rate. My scraper was pulling gigabytes of HTML through that gateway like the meter did not exist, because I did not know the meter existed.&lt;/p&gt;

&lt;p&gt;The agent did not just flag the spike. It named the mechanism and proposed the fix: "If a workload in a private subnet is downloading large volumes of external data, evaluate whether it requires private networking. For S3-bound traffic, a gateway VPC endpoint processes data at no charge."&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%2Fj4krchp41tw80b4sb9ie.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%2Fj4krchp41tw80b4sb9ie.png" alt="The NAT gateway incident: EC2-Other daily charges spiking on days 8 and 9, then collapsing after the fix" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I moved the scraper to a public subnet with a security group locked to egress only, since it held no credentials or sensitive data, and added the S3 gateway endpoint for the upload path. Charges collapsed the next day. Estimated savings for the rest of the scraping job: about $19. More valuable than the money was the education; I now check the network path of every batch workload before launching it, a habit purchased for the price of two red mornings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 11.&lt;/strong&gt; Green. The agent noted the EC2-Other line had "returned to baseline following the routing change," meaning it connected this morning's data to the anomaly it flagged two days earlier, within a single fresh session, purely because the 14-day window contained both. Emergent context from a dumb design decision. I will take it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 12.&lt;/strong&gt; A quiet day, but the forecast line caught my eye: "Projected month-end: $71 to $79, versus $107 trailing-month baseline." Watching that forecast walk downward over the month became weirdly motivating, like a fitness tracker for infrastructure.&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%2Fm301exv6s47fwtjy1kbr.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%2Fm301exv6s47fwtjy1kbr.png" alt="The agent's month-end forecast, day by day, descending from the baseline toward the actual result" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 3: The agent lies to me twice
&lt;/h3&gt;

&lt;p&gt;Every honest story about an LLM experiment needs this chapter, so here is mine, in full, no flattering edits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 16.&lt;/strong&gt; Status yellow. The agent claimed my RDS instance was "idle with minimal connection activity" and recommended stopping it, estimated savings $12 to $15 per month. Sensible advice with one flaw: &lt;strong&gt;I do not have an RDS instance.&lt;/strong&gt; Never did, in this account.&lt;/p&gt;

&lt;p&gt;I pulled the session trace to see what happened. The &lt;code&gt;describe_db_instances&lt;/code&gt; tool had returned an empty list, my early-version bare &lt;code&gt;[]&lt;/code&gt;. Somewhere between that empty array and the final report, the model's reasoning turned "no instances found" into "an idle instance was found." It is a fascinating failure if you stare at it long enough: the concepts of &lt;em&gt;idle&lt;/em&gt; and &lt;em&gt;absent&lt;/em&gt; live close together in the model's semantic space, and nothing in my tooling or prompt forced the distinction. Made worse, of course, by the confident dollar range attached to the phantom.&lt;/p&gt;

&lt;p&gt;Two fixes went in that afternoon. The tools started returning explicit counts (&lt;code&gt;{"found": 0, "items": []}&lt;/code&gt;, Chekhov's empty list finally fixed), and the instructions gained the rule you saw earlier: never recommend an action unsupported by data fetched in this session; if a tool returned zero items, there is nothing to recommend about those items. The phantom never returned. Eleven subsequent days, zero fabricated resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 19.&lt;/strong&gt; It happened again, in a form I find funnier every time I think about it. The agent reported Amazon Bedrock charges as an anomaly and recommended I "review unexpected Bedrock usage in the account."&lt;/p&gt;

&lt;p&gt;The Bedrock usage was the agent itself.&lt;/p&gt;

&lt;p&gt;It had discovered its own operating costs in the billing data and reported itself to me as suspicious activity. I laughed out loud, screenshotted the email, and then realized this is a genuine and slightly deep problem: a watcher built on metered infrastructure will eventually observe its own footprint, and unless it has a concept of self, it will classify itself as an intruder. My fix was prosaic rather than philosophical: a WATCHER OVERHEAD line in the report format, and an instruction that its own Bedrock and Lambda charges are expected and belong there. Somewhere in that incident there is a lovely essay about self-models that I am choosing not to write today.&lt;/p&gt;

&lt;p&gt;The generalizable lesson from both failures, and the one thing I hope you quote from this whole piece: &lt;strong&gt;agent reliability is mostly a tool design and prompt engineering problem, not a model problem.&lt;/strong&gt; Every failure I saw in 30 days traced back to an ambiguity I had left lying around, either in what the tools returned or in what the instructions permitted. Fix the ambiguity and the failure mode dies. I never once had to change the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 21.&lt;/strong&gt; Green, uneventful, and the first day the new report format ran clean end to end. WATCHER OVERHEAD: $0.17. The accountant now accounts for itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 4: Steady state
&lt;/h3&gt;

&lt;p&gt;The final week was almost boring, which is the whole point of the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 24.&lt;/strong&gt; Yellow, correctly. An Elastic IP had been sitting unattached for two days after I terminated a test instance, at the newer per-hour idle pricing. About $3.60 a month. I released it from my phone while waiting for a haircut, which is exactly the level of effort billing hygiene should require.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 25.&lt;/strong&gt; Yellow, and this one made me smile: "Elevated SageMaker training charges, consistent with an intentional training workload. Not classified as anomalous." I had kicked off a fine-tuning run the night before. The agent saw a spike, checked its character (training jobs, not endpoints; bounded, not recurring), and filed it exactly as instructed under the distinction between &lt;em&gt;increased&lt;/em&gt; and &lt;em&gt;anomalous&lt;/em&gt;. This is the report that alarms and statistical detectors cannot write, and it is the single best argument for the whole approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 28.&lt;/strong&gt; The agent noticed something I never asked it to look for: nonzero inter-region data transfer, on an account where everything nominally lives in one region. It suggested a cross-region S3 access, and it was right; an old script of mine was reading a bucket in another region, a leftover from an experiment two years ago. Savings of maybe a dollar a month. The dollar is irrelevant. The fact that it went looking is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 30.&lt;/strong&gt; Green. Final forecast: $76 to $78. Actual month-end: $76.10. I bought myself a decent lunch with a fraction of the difference and considered it a performance bonus paid to the wrong party.&lt;/p&gt;




&lt;h2&gt;
  
  
  The receipts
&lt;/h2&gt;

&lt;p&gt;The month in one table. "Baseline" is my average across the previous three months.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline (avg)&lt;/th&gt;
&lt;th&gt;Experiment month&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total monthly spend&lt;/td&gt;
&lt;td&gt;$107.40&lt;/td&gt;
&lt;td&gt;$76.10&lt;/td&gt;
&lt;td&gt;-29%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle/waste spend (est.)&lt;/td&gt;
&lt;td&gt;~$58&lt;/td&gt;
&lt;td&gt;~$6&lt;/td&gt;
&lt;td&gt;-90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billing surprises&lt;/td&gt;
&lt;td&gt;1 to 2 per month&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;gone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time I spent on billing&lt;/td&gt;
&lt;td&gt;~45 min/month&lt;/td&gt;
&lt;td&gt;~10 min/month&lt;/td&gt;
&lt;td&gt;-78%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watcher overhead&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$4.87&lt;/td&gt;
&lt;td&gt;new cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the full recommendations log, verdicts included:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;Est. monthly impact&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Idle SageMaker endpoint&lt;/td&gt;
&lt;td&gt;$28 to $34&lt;/td&gt;
&lt;td&gt;Real. Killed it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3 unattached EBS volumes&lt;/td&gt;
&lt;td&gt;~$24&lt;/td&gt;
&lt;td&gt;Real. Deleted.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;NAT gateway data processing spike&lt;/td&gt;
&lt;td&gt;~$19 (one-off)&lt;/td&gt;
&lt;td&gt;Real. Rearchitected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;"Idle RDS instance"&lt;/td&gt;
&lt;td&gt;$12 to $15&lt;/td&gt;
&lt;td&gt;Hallucinated. No RDS exists.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;"Unexpected Bedrock usage"&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;The agent found itself.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;Unattached Elastic IP&lt;/td&gt;
&lt;td&gt;~$3.60&lt;/td&gt;
&lt;td&gt;Real. Released.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;SageMaker training spike&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;Correctly labeled intentional.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;Cross-region S3 access&lt;/td&gt;
&lt;td&gt;~$1&lt;/td&gt;
&lt;td&gt;Real. Fixed the script.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six correct findings, one correct non-finding, two false ones. Both false ones were eliminated by tool and prompt fixes and never recurred. I will take that ratio in any system I run.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the watcher itself costs
&lt;/h3&gt;

&lt;p&gt;Full transparency, because a cost optimization story that hides its own costs would be a comedy:&lt;/p&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;Monthly cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock model invocations (30 daily sessions)&lt;/td&gt;
&lt;td&gt;~$3.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost Explorer API calls (~220 total)&lt;/td&gt;
&lt;td&gt;~$2.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda, EventBridge, SES, CloudWatch Logs&lt;/td&gt;
&lt;td&gt;under $0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$4.87&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Call it five dollars to find fifty-five.&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%2Fcw7leh5tkmt3kvtmzw67.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%2Fcw7leh5tkmt3kvtmzw67.png" alt="Five dollars to find fifty-five: waste found versus watcher cost, an 11x return" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a personal account the return is comfortable. On a company account with real spend and real sprawl, this is not even a conversation. The same architecture pointed at an organization's consolidated billing, with per-team tags in the grouping dimension, would pay for itself before lunch on day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Questions friends asked me along the way
&lt;/h2&gt;

&lt;p&gt;A few of these came from colleagues, a couple from the AWS Community Builders Slack when I mentioned what I was up to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not Cost Anomaly Detection? It is free.&lt;/strong&gt;&lt;br&gt;
Use both; they are not competitors. Anomaly Detection is a smoke detector: statistical, fast, and context-free. The agent is the person who walks into the kitchen, sees the smoke, and tells you it is the toaster and not the house. On day 25 Anomaly Detection would have flagged my training run. The agent explained it and moved on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why fresh sessions daily instead of giving it memory?&lt;/strong&gt;&lt;br&gt;
For the experiment, reproducibility. Every claim in every report had to be derivable from that day's tool calls alone, or the failures would have been impossible to debug. In steady-state operation, memory of past findings is clearly useful (it could track whether recommendations were acted on), and that is the headline feature of my version two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Could the model just be pattern-matching generic FinOps advice rather than analyzing my data?&lt;/strong&gt;&lt;br&gt;
The NAT gateway diagnosis and the cross-region catch were both specific to numbers it fetched in-session, with the mechanism named correctly. The RDS hallucination is actually the counter-evidence in my favor: when it &lt;em&gt;did&lt;/em&gt; drift into generic pattern-matching ("accounts usually have an underused database"), it produced garbage, and the fix was forcing every claim to bind to fetched data. The discipline is enforceable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about a multi-account org?&lt;/strong&gt;&lt;br&gt;
Same shape, bigger tools. Point Cost Explorer at the payer account, group by linked account and tag, and give the report a per-team section. The read-only constraint becomes more important there, not less.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did you worry about sending billing data to a model?&lt;/strong&gt;&lt;br&gt;
The data stays inside my own AWS account boundary; Bedrock invocations are not used to train the underlying models, per AWS's service terms. For a personal account I was comfortable. A regulated enterprise should read those terms themselves rather than take a blog post's word for it, including mine.&lt;/p&gt;




&lt;h2&gt;
  
  
  So, should you hire a robot accountant?
&lt;/h2&gt;

&lt;p&gt;The honest answer depends on what kind of AWS user you are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build it if&lt;/strong&gt; you run experiments, demos, side projects, or anything with a lifecycle shorter than your attention span. The idle-resource problem is fundamentally a problem of forgetting, and machines do not forget. Six hours of building bought me a permanent employee who works for five dollars a month and has no opinion about weekends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip it if&lt;/strong&gt; your account runs one stable production workload that never changes. A static bill needs a budget alert, not an analyst. This tool earns its keep in accounts with churn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And absolutely do not&lt;/strong&gt; give the agent write permissions, no matter how elegant the fully autonomous version looks in your head. My agent hallucinated a database on day 16 and priced its imaginary savings with a straight face. Imagine that reasoning chain ending in a &lt;code&gt;stop_db_instance&lt;/code&gt; call aimed at whatever resource it decided best fit its imaginary finding. Read-only is not a limitation of this design. It &lt;em&gt;is&lt;/em&gt; the design. The human approves; the human executes; the agent never touches anything.&lt;/p&gt;

&lt;p&gt;Three smaller lessons I would press into your hands before you go build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make the agent state ranges, not points.&lt;/strong&gt; "Savings of $28 to $34" invites verification. "Savings of $31.20" invites false trust. Precision is a costume that confidence wears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teach it your normal in the instructions&lt;/strong&gt;, including typical spend and known steady charges. Anomaly detection without a baseline is just complaining with extra steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log every session and read the traces when something is off.&lt;/strong&gt; The trace of which tools were called and what they returned was the only way to tell whether a bad report came from bad data or bad reasoning. In my month it was never the model and always my instructions. Debug yourself first.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;So, amici, that is the story of my month with a robot accountant. It began with a dead GPU endpoint and a familiar sting of embarrassment, and it ended with a 29% smaller bill, an inbox full of color-coded morning reports, and a considerably sharper understanding of where agents genuinely earn their keep. The Accountant found real money, explained real mechanisms, correctly shrugged at my intentional spikes, and got measurably smarter every time I tightened its instructions. &lt;/p&gt;

&lt;p&gt;It also invented a database out of thin air and once reported its own existence to me as a suspicious charge, which is why it will remain, forever, a colleague I listen to carefully and never hand the keys. Version two is already half-built: weekly deep-dives, per-project cost attribution through tags, and persistence so it can nag me about recommendations I have ignored. &lt;/p&gt;

&lt;p&gt;If you build your own version, and I hope this post leaves you no excuse not to, I would genuinely love to hear what your agent catches in its first week. Find me on Dev.to or LinkedIn and tell me the story. The weirdest finding gets a shout-out in the follow-up post. Until then, keep your endpoints deleted and your subnets public where they can afford to be. &lt;/p&gt;

&lt;h2&gt;
  
  
  #HappyCoding 👋
&lt;/h2&gt;

</description>
      <category>aws</category>
      <category>machinelearning</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>30 Tiny React Hooks That Punch Way Above Their Weight</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Fri, 26 Jun 2026 04:22:27 +0000</pubDate>
      <link>https://dev.to/mursalfk/30-tiny-react-hooks-that-punch-way-above-their-weight-1907</link>
      <guid>https://dev.to/mursalfk/30-tiny-react-hooks-that-punch-way-above-their-weight-1907</guid>
      <description>&lt;h1&gt;
  
  
  Hello devs 👋
&lt;/h1&gt;

&lt;p&gt;Ever notice how the same ten lines of React keep showing up in every project you touch? You copy them from some old repo, paste them into the new one, rename a variable or two, and quietly pretend you wrote them fresh. Be honest, we all do it. Half of frontend work is just remembering which folder you left that one perfect hook in.&lt;/p&gt;

&lt;p&gt;So I stopped pretending and collected the ones I actually reach for. This article is a stash of 30 tiny, battle tested React snippets you can drop straight into your codebase. No extra libraries, no bloat, no npm install marathon. Just copy, paste, and ship.&lt;/p&gt;

&lt;p&gt;Bookmark this one. Future you is going to be very grateful to present you.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. useDebounce
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useDebounce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDebounced&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setDebounced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;delay&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delays rapid state updates so you are not firing on every keystroke. Perfect for search bars and typeahead inputs where you only want to hit the API once the user stops typing. Your backend will send you a thank you card.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. useClickOutside
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useClickOutside&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mousedown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mousedown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Closes a modal, dropdown, or popover when the user clicks anywhere outside of it. Attach the ref to your menu, pass a close function as the handler, and you are done. Ten lines that quietly power half the dropdowns on the internet.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. usePrevious
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usePrevious&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gives you the value a prop or state had on the last render. Handy when you want to compare old versus new, like triggering an animation only when a count actually goes up. Tiny hook, surprisingly clutch.&lt;/p&gt;

&lt;h1&gt;
  
  
  4. useLocalStorage
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useLocalStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works exactly like useState but survives a page refresh. Great for theme choices, draft form data, or a dismissed banner that should stay dismissed. Same API you already know, zero new mental overhead.&lt;/p&gt;

&lt;h1&gt;
  
  
  5. useFetch
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useFetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pocket sized React Query. Fetches data and tracks the loading state in under fifteen lines. Not built for retries or caching, but it covers a solid 90 percent of simple screens.&lt;/p&gt;

&lt;h1&gt;
  
  
  6. useToggle
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useToggle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toggle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop writing setOpen(prev =&amp;gt; !prev) for the hundredth time. This gives you a boolean and a one word toggle function. Modals, sidebars, accordions, password visibility, all of it gets cleaner.&lt;/p&gt;

&lt;h1&gt;
  
  
  7. useIsMobile
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useIsMobile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;breakpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isMobile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsMobile&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;breakpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onResize&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="nf"&gt;setIsMobile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;breakpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onResize&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onResize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;breakpoint&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;isMobile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tells you whether the user is on a phone or a bigger screen. Use it to swap a desktop sidebar for a hamburger menu, or to render a completely different layout. CSS handles most of this, but sometimes you need the answer in JavaScript.&lt;/p&gt;

&lt;h1&gt;
  
  
  8. scrollToTop
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scrollToTop&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollTo&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Smoothly glides the page back to the top. Wire it up to a floating "back to top" button on long blog posts or endless product lists. One line, instant polish.&lt;/p&gt;

&lt;h1&gt;
  
  
  9. useInterval
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedCallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;savedCallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;savedCallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;delay&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs a function on a repeating timer the React way. The saved ref means your callback always sees fresh state instead of a stale closure, which is the classic setInterval trap. Reach for it on live clocks, auto refreshing dashboards, and carousels that slide on their own.&lt;/p&gt;

&lt;h1&gt;
  
  
  10. useTimeout
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedCallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;savedCallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;savedCallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;delay&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one shot cousin of useInterval. Fires your callback once after a delay and cleans itself up properly on unmount. Nice for auto hiding a toast or showing a "still loading" message after a few seconds.&lt;/p&gt;

&lt;h1&gt;
  
  
  11. useWindowSize
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useWindowSize&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onResize&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="nf"&gt;setSize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onResize&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onResize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gives you live width and height that update as the window resizes. Useful for canvas rendering, virtualized lists, or any chart that needs exact pixel dimensions. The component re renders the moment the viewport changes.&lt;/p&gt;

&lt;h1&gt;
  
  
  12. useMediaQuery
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useMediaQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMatches&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onChange&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="nf"&gt;setMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;mql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your CSS media queries, now available in JavaScript. Pass something like "(min-width: 1024px)" and get a boolean back. Cleaner than a manual resize listener when you only care about a specific breakpoint.&lt;/p&gt;

&lt;h1&gt;
  
  
  13. useCopyToClipboard
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useCopyToClipboard&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;copied&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCopied&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setCopied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCopied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;copied&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;copy&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copies any text and flips a "copied" flag for two seconds so you can show a little checkmark. Every code block, share link, and API key field wants this. Small touch, big "wow this app feels finished" energy.&lt;/p&gt;

&lt;h1&gt;
  
  
  14. useHover
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useHover&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hovered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setHovered&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;node&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enter&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="nf"&gt;setHovered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leave&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="nf"&gt;setHovered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mouseenter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mouseleave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leave&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mouseenter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mouseleave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leave&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hovered&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tracks hover state without sprinkling onMouseEnter and onMouseLeave everywhere. Attach the ref, read the boolean, render a tooltip or a preview card. Great when hover needs to drive logic and not just a CSS color change.&lt;/p&gt;

&lt;h1&gt;
  
  
  15. useOnlineStatus
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useOnlineStatus&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;online&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setOnline&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;goOnline&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="nf"&gt;setOnline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;goOffline&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="nf"&gt;setOnline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;online&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;goOnline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;offline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;goOffline&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;online&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;goOnline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;offline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;goOffline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;online&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tells you in real time whether the user just lost their connection. Show a "you are offline" banner, pause auto saves, or queue requests for later. Essential for anything that pretends to be a serious web app.&lt;/p&gt;

&lt;h1&gt;
  
  
  16. useEventListener
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useEventListener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;savedHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;savedHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One hook to rule all your addEventListener boilerplate. It handles the subscribe and cleanup dance for you and always calls the latest handler. Build most of the other hooks in this list on top of it if you want to get fancy.&lt;/p&gt;

&lt;h1&gt;
  
  
  17. useDocumentTitle
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useDocumentTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;title&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Updates the browser tab title and politely restores the old one when the component unmounts. Perfect for per page titles in a single page app, or showing an unread count like "(3) Inbox". Tiny SEO and UX win in three lines.&lt;/p&gt;

&lt;h1&gt;
  
  
  18. useKeyPress
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useKeyPress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPressed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;down&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;targetKey&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;setPressed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;targetKey&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;setPressed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;down&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keyup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;up&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;down&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keyup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;targetKey&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pressed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns true while a given key is held down. Wire up Escape to close a modal, Enter to submit, or arrow keys to move through a list. Keyboard shortcuts make your app feel like it was built by people who actually use it.&lt;/p&gt;

&lt;h1&gt;
  
  
  19. useDarkMode
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useDarkMode&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDark&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dark&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDark&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A complete dark mode toggle that remembers the choice across reloads. It flips a class on the html element so Tailwind or your CSS variables do the rest. Ship the feature everyone immediately looks for in settings.&lt;/p&gt;

&lt;h1&gt;
  
  
  20. useThrottle
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useThrottle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;throttled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setThrottled&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastRan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastRan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setThrottled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;lastRan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastRan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;throttled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Debounce waits for things to stop, throttle keeps a steady pace. This caps how often a value updates, which is exactly what you want for scroll position, mouse tracking, or window resize math. Smooth performance without melting the main thread.&lt;/p&gt;

&lt;h1&gt;
  
  
  21. useIntersectionObserver
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useIntersectionObserver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsVisible&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIsVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;options&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tells you when an element scrolls into view. This is the engine behind lazy loaded images, fade in on scroll animations, and infinite feeds that load more as you reach the bottom. Way cheaper than listening to the scroll event yourself.&lt;/p&gt;

&lt;h1&gt;
  
  
  22. useUpdateEffect
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useUpdateEffect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isFirst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isFirst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;isFirst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like useEffect, but it skips the very first render and only runs on updates. Saves you from that annoying duplicate API call or analytics event that fires the moment a component mounts. The "only when something actually changes" hook.&lt;/p&gt;

&lt;h1&gt;
  
  
  23. useEffectOnce
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useEffectOnce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tiny wrapper that runs an effect exactly once on mount. Mostly it just signals your intent clearly so the next dev does not stare at an empty dependency array wondering if you forgot something. Readability counts.&lt;/p&gt;

&lt;h1&gt;
  
  
  24. useCounter
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useCounter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;increment&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="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decrement&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="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reset&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="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decrement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A ready made counter with increment, decrement, and reset baked in. Drop it into quantity selectors, pagination, or step by step wizards. Less wiring, fewer bugs, more time for the fun stuff.&lt;/p&gt;

&lt;h1&gt;
  
  
  25. useArray
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initial&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setArray&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;push&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setArray&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setArray&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clear&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="nf"&gt;setArray&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;setArray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clear&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Managing a list in state with clean helper methods instead of spread syntax soup. Add, remove by index, or wipe it clean without rewriting the same map and filter logic. Great for to do lists, tag inputs, and cart items.&lt;/p&gt;

&lt;h1&gt;
  
  
  26. useGeolocation
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useGeolocation&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCoords&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watchPosition&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;setCoords&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearWatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watches the user location and updates as they move. Use it for "stores near me", live delivery tracking, or auto filling a city field. Just remember the browser will ask for permission first, so handle the null case gracefully.&lt;/p&gt;

&lt;h1&gt;
  
  
  27. useIdle
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useIdle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIdle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIdle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reset&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setIdle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIdle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mousemove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mousemove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detects when the user has gone quiet for a while. Pause a video, dim the screen, or auto log out of a banking dashboard after inactivity. Any reset of mouse or keyboard activity wakes it back up.&lt;/p&gt;

&lt;h1&gt;
  
  
  28. useSessionStorage
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useSessionStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea as useLocalStorage, but the data clears when the tab closes. Ideal for a multi step checkout or a wizard where you want progress saved during the visit but gone afterward. Privacy friendly persistence.&lt;/p&gt;

&lt;h1&gt;
  
  
  29. useScrollLock
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useScrollLock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;locked&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Freezes background scrolling while a modal or drawer is open. We have all rage scrolled a popup and watched the page behind it slide around. This one line fix kills that jank and restores scroll on close.&lt;/p&gt;

&lt;h1&gt;
  
  
  30. useRenderCount
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useRenderCount&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;return&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Counts how many times a component has rendered. Pure debugging gold when you are hunting down why something re renders forty times when you clicked once. Pop it in, log the number, find the culprit, take it out.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;And that is the stash. Thirty little snippets that save you from rewriting the same logic in every single project from now until forever. Copy what you need, tweak the ones you love, and quietly become the teammate whose pull requests are weirdly clean.&lt;/p&gt;

&lt;p&gt;I will be back soon with another round, probably something on performance or the patterns nobody warns you about until it is too late. Until then, go build something cool and stop reinventing the debounce.&lt;/p&gt;

&lt;h1&gt;
  
  
  happycoding
&lt;/h1&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
      <category>react</category>
    </item>
    <item>
      <title>The Invisible Duct Tape of the Internet: Backend Tools You Hear About But Never Fully Get</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Mon, 22 Jun 2026 03:14:44 +0000</pubDate>
      <link>https://dev.to/mursalfk/the-invisible-duct-tape-of-the-internet-backend-tools-you-hear-about-but-never-fully-get-3f0f</link>
      <guid>https://dev.to/mursalfk/the-invisible-duct-tape-of-the-internet-backend-tools-you-hear-about-but-never-fully-get-3f0f</guid>
      <description>&lt;p&gt;Hi 👋 fellow devs&lt;/p&gt;

&lt;p&gt;Sorry for such a big gap since my last article...... Life got a bit hectic, but I am finally back in action!&lt;/p&gt;

&lt;p&gt;You know how it goes. We spend so much of our energy obsessing over the flashy side of tech. We talk about gorgeous UI designs, smooth animations, and whatever frontend framework is trending on GitHub this week. But let’s be completely real for a second. What actually keeps your favorite apps from melting down when millions of people hit the refresh button at the exact same moment?&lt;/p&gt;

&lt;p&gt;That is exactly what we are going to unpack today. We are pulling back the curtain on the quiet, brilliant backstage crew of infrastructure tools. You see their logos all over tech Twitter and hear senior engineers drop their names in meetings like secret handshakes, but today, we are stripping away the corporate fluff. We will break down eight legendary backend technologies using conversational paragraphs and quick bullet points so you can finally master what they actually do.&lt;br&gt;
Let’s dive right in.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Redis
&lt;/h2&gt;

&lt;p&gt;Traditional databases live on hard drives. They are fantastic for keeping your data safe and organized permanently, but pulling data off a physical drive takes time. If your application has to wander deep into those database aisles to fetch the exact same piece of information every single second, your entire system starts to stall.&lt;br&gt;
To understand how Redis fixes this, imagine you are studying for a brutal exam. Your massive, 1,000-page textbook represents your main database. It holds every single answer, but flipping through the pages continuously is incredibly slow. Redis is the digital equivalent of writing the core formulas you need on a neon sticky note and taping it directly to your monitor. It keeps critical data sitting directly inside the system's lightning-fast short-term memory.&lt;br&gt;
You will typically find Redis stepping in to handle operations like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session Management:&lt;/strong&gt; Keeping users logged into an application without checking the main database on every click.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Caching:&lt;/strong&gt; Storing the responses of heavy, repetitive queries so they render instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Leaderboards:&lt;/strong&gt; Tracking high-velocity, real-time score updates during live events or games.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Kafka
&lt;/h2&gt;

&lt;p&gt;Modern applications generate an overwhelming, continuous stream of mini-events. Every single click, profile update, and background notification triggers a piece of data. If every single microservice in your software tries to communicate directly with every other service to share these updates, your architecture quickly devolves into a tangled, fragile spiderweb of digital chaos.&lt;br&gt;
Think of Kafka like a wildly busy fast-food kitchen. Instead of the cashiers running into the back to scream individual orders directly at the face of every cook, they hang receipts on a long, central ticket line. The burger station, the fry station, and the drinks manager all pull tickets from that central line at their own pace. Kafka acts as that indestructible, high-speed central ticket line for your data.&lt;br&gt;
Engineers rely on Kafka to manage heavy data pipelines because it excels at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decoupling Services:&lt;/strong&gt; Allowing different parts of an app to function independently without relying on direct connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activity Tracking:&lt;/strong&gt; Processing millions of user interactions across a massive platform simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Data Streams:&lt;/strong&gt; Managing live GPS coordinates from a fleet of delivery drivers without dropping a single packet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Nginx
&lt;/h2&gt;

&lt;p&gt;When a massive wave of users hits a website simultaneously, your core application servers can easily experience a traffic jam, get overwhelmed, and crash. Nginx sits right at the edge of your infrastructure to act as an incredibly efficient shield, managing thousands of incoming connections before they ever touch your actual code.&lt;br&gt;
To picture this, imagine a highly exclusive apartment building. Instead of letting random delivery drivers wander the hallways looking for individual doors, everyone is forced to stop at a central front desk. Nginx is that ultimate receptionist. It checks security certificates, hands out public brochures instantly so people do not even have to step inside, and cleanly directs visitors to the exact elevator they need.&lt;br&gt;
In everyday setups, Nginx is deployed to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Load Balancing:&lt;/strong&gt; Distributing incoming user traffic evenly across multiple backend servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse Proxying:&lt;/strong&gt; Routing requests smoothly to different applications running on different ports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static File Serving:&lt;/strong&gt; Instantly handing over images, HTML, and CSS directly to the browser to save server power.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Kubernetes
&lt;/h2&gt;

&lt;p&gt;Managing a couple of servers by hand is completely fine, but managing hundreds of isolated microservices scattered across global cloud networks is a total nightmare. If a server goes down in the middle of the night, someone has to wake up, diagnose the issue, and manually restart it. Kubernetes exists to automate that entire headache away.&lt;br&gt;
Think of Kubernetes as the manager of a massive, fully automated shipping port. You do not manually tell every truck where to drive or check if a crane operator fell asleep. You program your desired layout into a central management system. If a cargo truck randomly breaks down, the system instantly detects it, dispatches a identical replacement truck, and moves the cargo along without stalling operations for a single second.&lt;br&gt;
Kubernetes keeps complex cloud applications running smoothly by providing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-Healing:&lt;/strong&gt; Automatically restarting containers that crash and replacing unresponsive servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal Scaling:&lt;/strong&gt; Spatially spinning up new instances of an app during traffic spikes and scaling them down when quiet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Rollouts:&lt;/strong&gt; Gradually deploying new code updates across a network without causing a single second of downtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Elasticsearch
&lt;/h2&gt;

&lt;p&gt;Standard database lookups are incredibly rigid. They work perfectly when you are searching for an exact user ID number, but if you try to perform a complex, messy search across millions of rows of unstructured text, a normal database will grind to a painful halt.&lt;br&gt;
Imagine trying to find every single mention of the word "coffee" inside a massive university library. A standard database approach would involve opening every single book and scanning every single page word by word. Elasticsearch works by building a massive, highly optimized index ahead of time. It acts exactly like the index at the back of a textbook, but it is smart enough to find the right page even if you make a typo.&lt;br&gt;
It is the industry standard tool when you need to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Advanced E-commerce Search:&lt;/strong&gt; Powering filters, categories, and instant results across massive product catalogs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzzy Matching:&lt;/strong&gt; Predicting what a user actually meant even when they misspell words in a search bar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log Analysis:&lt;/strong&gt; Sifting through gigabytes of system error logs to find a single needle in a haystack.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. GraphQL
&lt;/h2&gt;

&lt;p&gt;Traditional REST APIs can sometimes feel like a restaurant that only serves rigid, pre-packaged combo meals. If you only want a side of fries, you are still forced to order the entire burger and drink combo, which wastes data and bandwidth. Alternatively, you might have to hit three different endpoints just to gather enough information to build a single profile page.&lt;br&gt;
GraphQL fundamentally changes this dynamic by turning your API into an a la carte menu. It allows the client application to hand the backend a specific checklist of requirements. If your mobile app only needs a user's first name and profile photo for a tiny header card, GraphQL ensures the server sends back exactly those two data points and absolutely nothing extra.&lt;br&gt;
This precision makes GraphQL incredibly valuable for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mobile App Optimization:&lt;/strong&gt; Saving precious cellular data and battery life for users on slower networks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consolidating Requests:&lt;/strong&gt; Fetching data from multiple distinct sources using a single, unified network call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rapid Frontend Development:&lt;/strong&gt; Allowing frontend devs to change visual layouts without needing backend devs to constantly rewrite API endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. RabbitMQ
&lt;/h2&gt;

&lt;p&gt;When a user clicks a button to upload a file or make a purchase, they expect an instant confirmation response. They do not want to stare at a frozen loading spinner for ten minutes while your backend server crunches a heavy file, updates inventory, creates a PDF invoice, and sends out confirmation emails.&lt;br&gt;
RabbitMQ fixes this by acting exactly like a post office drop box. You do not stand at the counter waiting for the clerk to drive your letter across the country. You drop it in the slot and walk away, confident it will be handled. RabbitMQ accepts these heavy, time-consuming tasks from your main application and holds them safely in a queue, letting your user interface stay incredibly snappy and responsive.&lt;br&gt;
You should implement RabbitMQ whenever you need to offload tasks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Background File Processing:&lt;/strong&gt; Resizing high-resolution images or transcoding videos after an upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification Blasts:&lt;/strong&gt; Dispatching transactional emails or push notifications to thousands of users without blocking the main app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asynchronous Workflows:&lt;/strong&gt; Delaying heavy calculations or third-party API syncs until your server has free resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Docker
&lt;/h2&gt;

&lt;p&gt;Every developer has lived through the absolute nightmare where a feature runs flawlessly on their local laptop, but completely shatters the moment it gets deployed to a live production server due to a missing file or a conflicting software version.&lt;br&gt;
Docker completely eliminates this issue by introducing the concept of digital shipping containers. In the physical world, a standard shipping container fits perfectly onto any cargo ship, train, or truck regardless of what is inside it. Docker applies this exact logic to software, wrapping your code, your runtime, your tools, and your specific configurations into an isolated, standardized digital box.&lt;br&gt;
By containerizing your applications, Docker guarantees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Environment Consistency:&lt;/strong&gt; Ensuring your code behaves identically on a Mac laptop, a Windows desktop, or a Linux cloud instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation:&lt;/strong&gt; Running multiple applications with completely conflicting dependencies on the exact same computer without any interference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplified Onboarding:&lt;/strong&gt; Allowing new developers to spin up a complete, complex project environment using one single command.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Reference Guide
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The Tool&lt;/th&gt;
&lt;th&gt;Its Core Superpower&lt;/th&gt;
&lt;th&gt;In Plain English&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In-memory speed&lt;/td&gt;
&lt;td&gt;The sticky note on your monitor.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Event streaming&lt;/td&gt;
&lt;td&gt;The kitchen ticket line.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nginx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reverse proxy and web server&lt;/td&gt;
&lt;td&gt;The gatekeeper at the front desk.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kubernetes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Container orchestration&lt;/td&gt;
&lt;td&gt;The automated shipping port manager.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Elasticsearch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Distributed search engine&lt;/td&gt;
&lt;td&gt;The textbook index on steroids.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GraphQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Precise API queries&lt;/td&gt;
&lt;td&gt;The custom sandwich checklist.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Message broker&lt;/td&gt;
&lt;td&gt;The post office drop box.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Containerization&lt;/td&gt;
&lt;td&gt;The standardized shipping container.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;At the end of the day, you do not need to be an absolute wizard in all eight of these tools to be a fantastic developer, but understanding where they fit in the puzzle changes how you think about software. Writing code that functions locally is just step one. True engineering happens when you learn how these infrastructure pieces coordinate behind the scenes to architect systems capable of handling millions of users without breaking a sweat.&lt;br&gt;
Drop a comment below and let me know which of these backend heroes you are planning to deploy in your next project!&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>VS Code Extensions That Code For You</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Tue, 14 Apr 2026 10:35:13 +0000</pubDate>
      <link>https://dev.to/mursalfk/vs-code-extensions-that-code-for-you-13d6</link>
      <guid>https://dev.to/mursalfk/vs-code-extensions-that-code-for-you-13d6</guid>
      <description>&lt;h2&gt;
  
  
  Hello Devs 👋
&lt;/h2&gt;

&lt;p&gt;If your VSCode setup still looks like it did before the AI boom, you may be coding with a bicycle in a Formula 1 race. 🏎️ Modern AI extensions are no longer novelty gadgets, they’re force multipliers. From ghost-writing boilerplate to understanding sprawling codebases and debugging at 2 AM when your brain has blue-screened, these tools can turn your editor into a caffeinated co-pilot. Here are 6 of the best AI extensions helping developers ship faster, smarter, and with fewer “why is this null?” moments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this matter?
&lt;/h2&gt;

&lt;p&gt;Your editor should work as hard as you do. Most devs use VSCode, but only 10% use AI extensions. In 2026, that gap is the difference between a slow dev and a 10x dev.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;u&gt;Continue&lt;/u&gt; by Continue.dev
&lt;/h2&gt;

&lt;p&gt;Open-Source AI Assistant. Plug in any model, like GPT-4, Claude, Llama or Mistral directly into VSCode. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses your own API Key (GPT/Claude/etc.)&lt;/li&gt;
&lt;li&gt;Run local models via Llama&lt;/li&gt;
&lt;li&gt;Custom slash commands &amp;amp; context&lt;/li&gt;
&lt;li&gt;Full-Chat + Inline edit supports
&lt;strong&gt;Rating:&lt;/strong&gt; 5/5
&lt;strong&gt;Cost:&lt;/strong&gt; Free | Open-Source&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. &lt;u&gt;Supermaven&lt;/u&gt; by Supermaven Inc.
&lt;/h2&gt;

&lt;p&gt;Blazing fast AI autocomplete with a 1 million context window. Its usage feels almost instant, doesn't lag and doesn't make you wait.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1,000,000 Token context.&lt;/li&gt;
&lt;li&gt;Sees your entire repository and codebase&lt;/li&gt;
&lt;li&gt;Sub-100ms suggestions. Truly feels instant&lt;/li&gt;
&lt;li&gt;Understands cross-file dependencies&lt;/li&gt;
&lt;li&gt;Free tier available
&lt;strong&gt;Rating:&lt;/strong&gt; 5/5
&lt;strong&gt;Cost:&lt;/strong&gt; Free Plan | Pro is $10/Month&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. &lt;u&gt;Pieces for Devs&lt;/u&gt; by Pieces.app
&lt;/h2&gt;

&lt;p&gt;Your AI-Powered developer memory. It saves code snippets, context and links, and retrieves them as and when needed. It also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-saves snippets with AI Tags&lt;/li&gt;
&lt;li&gt;Long-term memory across projects&lt;/li&gt;
&lt;li&gt;AI Explains saved snippets on demand&lt;/li&gt;
&lt;li&gt;Works offline with on-device AI
&lt;strong&gt;Rating&lt;/strong&gt;: 5/5
&lt;strong&gt;Cost&lt;/strong&gt;: Free for Personal Use&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. &lt;u&gt;Codeium&lt;/u&gt; by Exafunction
&lt;/h2&gt;

&lt;p&gt;The best free Copilot alternative. It's fast, accurate and supports 70+ languages. Codeium facilitates its users in many ways, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's completely free for individual devs&lt;/li&gt;
&lt;li&gt;Provides in-editor AI Chat for debugging&lt;/li&gt;
&lt;li&gt;Autocompletes across 70+ programming languages&lt;/li&gt;
&lt;li&gt;Also comes with Codeium Search and Codebase Q&amp;amp;A
&lt;strong&gt;Rating&lt;/strong&gt;: 5/5
&lt;strong&gt;Cost&lt;/strong&gt;: 100% FREE | No Limits&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. &lt;u&gt;Tabine&lt;/u&gt; by Tabnine
&lt;/h2&gt;

&lt;p&gt;AI Code Completor, trained on your own codebase. This is best for teams that can't share code with external servers (mainly proprietary developers). Its main features include, but are not limited to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs locally, your code stays private&lt;/li&gt;
&lt;li&gt;Learns your codebase patterns&lt;/li&gt;
&lt;li&gt;Works offline, no internet needed&lt;/li&gt;
&lt;li&gt;Team model training on your repository
&lt;strong&gt;Rating:&lt;/strong&gt; 5/5
&lt;strong&gt;Cost:&lt;/strong&gt; Pro is $12/Month&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. &lt;u&gt;GitHub Copilot&lt;/u&gt; by GitHub (Microsoft)
&lt;/h2&gt;

&lt;p&gt;The OG AI Pair Programmer. This extension auto-completes entire functions, writes tests, and explains code, all inside VSCode. Its main features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inline code suggestions as you type&lt;/li&gt;
&lt;li&gt;Copilot Chat, ask anything about your code&lt;/li&gt;
&lt;li&gt;Multi-file context awareness&lt;/li&gt;
&lt;li&gt;Generate unit tests automatically
&lt;strong&gt;Rating&lt;/strong&gt;: 5/5
&lt;strong&gt;Cost&lt;/strong&gt;: $10/Month | &lt;em&gt;Free for Students&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclucion
&lt;/h2&gt;

&lt;p&gt;The best developers in 2026 are not just writing code, they’re orchestrating tools. Whether you want lightning-fast autocomplete, local privacy-first AI, or a full-blown coding sidekick living in your editor, there’s an extension here for every workflow. Try a few, mix and match, and build the AI-powered setup that makes your keyboard feel slightly unfair. ⚡&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Giving AI Agents “Live Memory” with Aurora Zero-ETL and Redshift Vector Search</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Wed, 25 Mar 2026 05:38:18 +0000</pubDate>
      <link>https://dev.to/mursalfk/giving-ai-agents-live-memory-with-aurora-zero-etl-and-redshift-vector-search-35gg</link>
      <guid>https://dev.to/mursalfk/giving-ai-agents-live-memory-with-aurora-zero-etl-and-redshift-vector-search-35gg</guid>
      <description>&lt;p&gt;Hey everyone! Honestly, it feels so good to be sitting down and writing to you all again. I want to start with a massive apology for the radio silence and the long gap between my last few posts. Life has a funny way of getting busy, but I’ve missed this community and our deep dives into the future of tech. &lt;/p&gt;

&lt;p&gt;We’re talking about a problem that has been bugging me all through 2025 and into 2026: &lt;strong&gt;AI situational awareness.&lt;/strong&gt; Let’s dive in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Goldfish" Problem: Why Your AI is Gaslighting Your Users 🐠🌀
&lt;/h2&gt;

&lt;p&gt;Look, we need to have a genuine heart-to-heart about your AI agents. We’re well into 2026, and yet, most AI “agents” still have the situational awareness of a goldfish in a blender. It’s a bit heartbreaking, isn't it?&lt;/p&gt;

&lt;p&gt;You know the feeling: You’ve spent weeks perfecting your system prompts, the UI is looking like a work of art, and your LLM is sharp. But the second a user actually &lt;em&gt;does&lt;/em&gt; something—like upgrading a subscription, changing a delivery address, or updating their profile—the AI is the absolute last one to know. It’s like the “memory” of your agent is stuck in a permanent time lag, trapped in a 2024 batch-processing nightmare.&lt;/p&gt;

&lt;p&gt;Imagine you’re building the ultimate AI concierge for a luxury travel app. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2:00:00 PM:&lt;/strong&gt; Your user, Sarah, realizes she’s over the cold and cancels her flight to Paris because she’d rather sip espresso in Rome. This change hits your Amazon Aurora database instantly. Success!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2:00:10 PM:&lt;/strong&gt; Feeling productive, Sarah asks the AI Agent, “Hey, what time does my flight leave?”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Disaster:&lt;/strong&gt; Because your old-school data sync (ETL) only runs every 30 minutes, the AI looks at the stale warehouse data and says, “Your flight to Paris departs at 6 PM! Don't forget your beret!” 🥖&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sarah is now officially being gaslit by an algorithm. 🤦‍♂️ The immersion? Totally gone. The trust? Shattered. This happens because the "Transactional Brain" (Aurora) isn't talking to the "Analytical Memory" (Redshift) fast enough. We need a way to move that data so quickly it feels like magic. Today, we are fixing that with &lt;strong&gt;Live Memory&lt;/strong&gt; using &lt;strong&gt;Amazon Aurora Zero-ETL&lt;/strong&gt; and &lt;strong&gt;Redshift Vector Search&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: The “Wormhole” (Zero-ETL) 🚀
&lt;/h3&gt;

&lt;p&gt;In the "old days" (way back in 2024), moving data from Aurora to Redshift meant building a "Glue" pipeline. It was like trying to build a bridge between two islands using nothing but popsicle sticks, duct tape, and a whole lot of hope. It broke constantly, it cost a fortune, and it was painfully slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero-ETL&lt;/strong&gt; is the modern way to handle this. Think of it as a native “wormhole” between Aurora and Redshift. When you enable it, AWS handles all the heavy lifting and plumbing behind the scenes. No Python scripts to debug, no Lambda triggers to monitor, and absolutely zero drama.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Why this is the “Secret Sauce” for your Agent:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Near-Zero Latency:&lt;/strong&gt; We’re talking seconds, not minutes. By the time Sarah finishes typing her question, the data is already there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-Scaling:&lt;/strong&gt; As your app goes viral, the teleporter grows with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total Peace of Mind:&lt;/strong&gt; You can finally stop monitoring failed Glue jobs at 3 AM. If it’s in Aurora, it’s in Redshift.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: From “Rows” to “Recall” (Vector Search) 🧠
&lt;/h3&gt;

&lt;p&gt;Okay, so the data has successfully teleported into Redshift. But here’s the catch: Redshift is a warehouse full of structured tables, and AI Agents don’t really "think" in tables—they think in &lt;strong&gt;Vectors&lt;/strong&gt; (mathematical representations of meaning).&lt;/p&gt;

&lt;p&gt;In the past, you’d have to ship this data &lt;em&gt;again&lt;/em&gt; to a dedicated vector database. But that just adds more lag! Instead, we use Redshift’s &lt;strong&gt;Native Vector Search&lt;/strong&gt;. This keeps your “Live Memory” in one single, high-performance place. To make this work, we set up a &lt;strong&gt;Materialized View&lt;/strong&gt; in Redshift that automatically calls &lt;strong&gt;Amazon Bedrock&lt;/strong&gt; to turn those new rows into vectors on the fly.&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;-- The "I don't have a goldfish memory anymore" Query&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;live_user_context&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;event_description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- This calls Bedrock to create a vector on the fly!&lt;/span&gt;
    &lt;span class="n"&gt;amazon_bedrock_embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_description&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;semantic_vector&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;aurora_synced_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_logs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: The “Aha!” Moment 🔍
&lt;/h3&gt;

&lt;p&gt;Now, let's replay the Sarah scenario. When she asks about her flight, the AI Agent sends a vector query to Redshift. Redshift looks at the most recent events—including the one that landed just 10 seconds ago—and finds the “Canceled Paris / Booked Rome” entry.&lt;/p&gt;

&lt;p&gt;The Agent responds: &lt;strong&gt;“I see you switched to the Rome trip—great choice! That flight leaves at 8 PM. Should I book a car to the airport?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Boom. You just saved the user experience. 🥳 You've moved from a "chatbot" to a genuine "agent" that understands the present moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  By The Numbers: Is it worth your time? 📈
&lt;/h3&gt;

&lt;p&gt;We ran some benchmarks to see just how much of a difference this "Live Memory" architecture actually makes. When you compare the old "Batch" world to the new "Zero-ETL" world, the latency gap is massive.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Average “Brain Lag” (Latency):&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Standard S3/Glue Sync: ~480 seconds&lt;/li&gt;
&lt;li&gt;Zero-ETL + Redshift: &lt;strong&gt;~12 seconds&lt;/strong&gt; (A &lt;strong&gt;97% improvement&lt;/strong&gt;!)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Developer Happiness:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Measured in “How many times I had to fix a pipeline this week”: Dropped from an average of 4 down to &lt;strong&gt;0&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pro-Tips for the Modern ML Engineer 🛠️
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Keep it Serverless:&lt;/strong&gt; Use &lt;strong&gt;Redshift Serverless&lt;/strong&gt;. It scales down to zero when your agents are sleeping. &lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Hybrid Search is King:&lt;/strong&gt; Don’t rely &lt;em&gt;only&lt;/em&gt; on vectors. Combine them with regular SQL filters (like &lt;code&gt;WHERE user_id = '123'&lt;/code&gt;) to make your search lightning fast.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Capture the Vibe:&lt;/strong&gt; Remember, this isn’t just for logs. If a user tells the AI, “I love my better half a lot,” ❤️ that sentiment gets synced and embedded instantly. The next time they ask for anniversary ideas, the agent immediately knows the priority is “High Romance.”&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;Anyway, it feels amazing to be back in the flow and sharing these breakthroughs with you all again. The leap from “static” AI to “live” agents is one of the most exciting shifts we’re seeing this year, and I can’t wait to see how you all implement these “wormholes” in your own stacks. If you run into any snags or just want to geek out over vector search optimizations, drop a comment below or find me over on Dev.to! I promise the next article won’t take nearly as long to reach your screens. Until next time — stay curious, keep building, and let’s make those agents a whole lot smarter. 🚀✨&lt;/p&gt;

</description>
      <category>aws</category>
      <category>machinelearning</category>
      <category>etl</category>
      <category>ai</category>
    </item>
    <item>
      <title>RIP 12-Digit IDs: The AWS Matrix Has Finally Been Decoded</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Mon, 09 Feb 2026 09:41:12 +0000</pubDate>
      <link>https://dev.to/aws-builders/rip-12-digit-ids-the-aws-matrix-has-finally-been-decoded-2om9</link>
      <guid>https://dev.to/aws-builders/rip-12-digit-ids-the-aws-matrix-has-finally-been-decoded-2om9</guid>
      <description>&lt;p&gt;Y'all... pull up a chair. We need to talk about a literal miracle. 🥂&lt;/p&gt;

&lt;p&gt;It was 2:41 AM. PagerDuty was screaming like a banshee in my ear. My eyes were blurry, my coffee was cold, and Slack was silent in that heavy, "something-is-very-wrong" way. I logged into the AWS Console to “quickly restart a service” and get back to sleep.&lt;/p&gt;

&lt;p&gt;Five clicks later—just as I was about to hit "Delete"—my stomach dropped. My heart hit the floor.&lt;/p&gt;

&lt;p&gt;Wrong account. I was in Production. Nothing blew up that night. I caught myself at the last millisecond. I got lucky. But for 21+ years, millions of engineers weren’t so lucky. We’ve been squinting at 12-digit account IDs like we’re trying to decode the Matrix while the world burns around us.&lt;/p&gt;

&lt;p&gt;But the wait is over. AWS finally added the Account Name to the top bar. It’s a "high-tech" feature that only took two decades to arrive, and it is GLORIOUS.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏆 Welcome to the "Almost Nuked Prod" Hall of Fame
&lt;/h2&gt;

&lt;p&gt;For over two decades, AWS UX followed the "Extreme Stealth" philosophy. The top bar gave you the region and your username, but the actual account context? Hidden behind a click. Pure chaos. It was like driving a car where the speedometer is inside the glove box.&lt;/p&gt;

&lt;p&gt;This led to the Three Stages of AWS Grief:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Over-Confidence&lt;/strong&gt;: “I definitely logged into Sandbox. I'm a pro, I don't need to check.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Sudden Realization&lt;/strong&gt;: “Wait... why does this S3 bucket have 4PB of data and a 'Do Not Delete' tag? Why is this instance type an x1e.32xlarge?”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Panic&lt;/strong&gt;: Aggressively smashing Ctrl+W to close 42 Chrome tabs before your shaking finger accidentally clicks 'Terminate Instance'.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why This is Low-Key Life-Changing
&lt;/h2&gt;

&lt;p&gt;This isn’t just cosmetic; it’s cognitive safety. In a multi-account world, you’re juggling dozens of environments. This update assumes that engineers are human—that we get tired, stressed, and caffeine-deprived. It’s a sanity check for your career. Good UX doesn’t assume perfection; it assumes you're human.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ Hands-On: Build Your Safety Net Today
&lt;/h2&gt;

&lt;p&gt;Since you're likely rocking .ipynb files in VSCode (the ultimate dev setup, IMO), you’re used to seeing your environment in the kernel picker or the status bar. Let's get that same "don't-get-fired" energy in your browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Make it Pretty with the AWS CLI
&lt;/h3&gt;

&lt;p&gt;If your account name is company-billing-prod-final-v2, it’s going to get truncated. Don't click through menus like it's 2004—we're builders! Pop this into your terminal to set a punchy, unmistakable Alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Give your account a name that SCREAMS at you in the top bar&lt;/span&gt;
aws iam create-account-alias &lt;span class="nt"&gt;--account-alias&lt;/span&gt; PROD-SENSITIVE-ZONE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Boom&lt;/em&gt;. Now your top bar says PROD-SENSITIVE-ZONE in clear text instead of a random string of numbers. 💅&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The "Visual Fire Alarm" (Layered Security)
&lt;/h3&gt;

&lt;p&gt;Text is great, but color-coding saves careers. Since AWS hasn't given us a "Red Header" mode for Prod yet (maybe in the year 2047?), we have to be clever:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unified Settings&lt;/strong&gt;: Click your Name (top right) → Settings → Visual Mode. Choose a distinct theme for your main accounts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Browser Profiles&lt;/strong&gt;: This is the real pro-tip. Use different Browser Profiles (Chrome/Brave/Firefox) for different environments. I keep my Prod profile themed in "Emergency Red" with a massive warning icon. If the window isn't red, it isn't Prod.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. The "Don't Fire Me" Python Snippet
&lt;/h3&gt;

&lt;p&gt;Before running any destructive boto3 code in your Jupyter Notebook, run this check. It’s the programmatic version of "looking both ways before crossing the street."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;safety_check&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Grab the current identity
&lt;/span&gt;    &lt;span class="n"&gt;sts&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;sts&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_caller_identity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Account&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Put your real Prod ID here
&lt;/span&gt;    &lt;span class="n"&gt;PROD_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;123456789012&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;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PROD_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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🚨 ALERT: YOU ARE IN PRODUCTION! 🚨&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Step away from the Shift+Enter key unless you&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re sure!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="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;✅ Connected to: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;account_id&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="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;Environment: Non-Prod (Safe to play, fam)&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;safety_check&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 The Future of AWS UX
&lt;/h2&gt;

&lt;p&gt;While we celebrate this "giant leap for engineer-kind," we’re still looking toward the future. Maybe one day the "Delete Database" button won't be the exact same shade of blue as the "Save" button? Maybe we'll get a "High Stakes" mode that requires a physical key turn?&lt;/p&gt;

&lt;p&gt;Until then, appreciate the label. It might just be the thing that keeps you from becoming a "Why We Had an Outage" post-mortem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Thought&lt;/strong&gt;: The most dangerous bugs don’t live in your code; they live in interfaces that assume humans never slip. AWS finally acknowledged that we’re human. Sometimes, the biggest cloud innovation... is simply knowing exactly where you are before you hit 'Enter'. ☁️&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudcomputing</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>From Monoliths to Multitaskers: Building Your AWS AI Dream Team! 🚀🤖</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Sat, 07 Feb 2026 07:47:02 +0000</pubDate>
      <link>https://dev.to/aws-builders/from-monoliths-to-multitaskers-building-your-aws-ai-dream-team-5dnf</link>
      <guid>https://dev.to/aws-builders/from-monoliths-to-multitaskers-building-your-aws-ai-dream-team-5dnf</guid>
      <description>&lt;p&gt;After being away from active tech ecosystem for about a month, I have decided to shed some light on the current landscape of AI agents on AWS. And let me tell you, things have gotten wild while I was gone. We aren't just talking to single chatbots anymore; we're building entire autonomous squads.&lt;/p&gt;

&lt;p&gt;If you’ve been building with AI lately, you’ve probably hit the "Monolith Wall." You know the one: you try to get one Large Language Model (LLM) to handle your database, write your emails, troubleshoot your network, and maybe make a decent cup of coffee. It gets confused, starts hallucinating, and suddenly your "smart" app is telling users that the server is down because it's "feeling sleepy."&lt;/p&gt;

&lt;p&gt;The 2025 vibe is different. We are moving away from "User → LLM → Response" and toward "User → &lt;strong&gt;Agent Network&lt;/strong&gt; → Coordinated Action." Think of it like a D&amp;amp;D party: you wouldn't ask the Barbarian to pick a lock, and you wouldn't ask the Rogue to tank a dragon. You need specialists.    &lt;/p&gt;

&lt;p&gt;Today, we’re going to learn how to orchestrate a Multi-Agent System (MAS) on AWS Bedrock. Grab your coffee (or mana potion), and let’s dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠 The Secret Sauce: What is a "Bedrock Agent"?
&lt;/h2&gt;

&lt;p&gt;Before we build the team, we need to understand the individual "hero." A Bedrock Agent isn't just an LLM with a fancy name. It's a system with three core parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Brain (Model Provider)&lt;/strong&gt;: Usually something like Claude 3.5 Sonnet or the zippy new Amazon Nova series.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Manual (System Prompt)&lt;/strong&gt;: The instructions that tell the agent, "You are a world-class SRE," and give it boundaries (like "Don't delete production, please").&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Toolbelt (Action Groups)&lt;/strong&gt;: These are the superpowers! This is where the agent calls AWS Lambda functions to actually do things in the real world.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🎭 The Multi-Agent Party: Meet the Supervisor
&lt;/h2&gt;

&lt;p&gt;Why have one agent when you can have five? In a Supervisor-Agent Pattern, you have one "Dungeon Master" (the Supervisor) who listens to the user and decides which specialist to call.&lt;/p&gt;

&lt;p&gt;Imagine we're building a Telco Network Operations Assistant. Our party looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Specialty&lt;/th&gt;
&lt;th&gt;Data Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;*&lt;em&gt;The Supervisor *&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Orchestration &amp;amp; Planning&lt;/td&gt;
&lt;td&gt;Routes the query to the right specialist.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The Alchemist (KPI Agent)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Performance Metrics&lt;/td&gt;
&lt;td&gt;Checks throughput and latency.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The Scout (Alarm Agent)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-time Status&lt;/td&gt;
&lt;td&gt;Looks for active site failures.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The Chronicler (Log Agent)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;History &amp;amp; Patterns&lt;/td&gt;
&lt;td&gt;Scans CloudWatch logs for anomalies.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  🏗 Hands-On Walkthrough: Building the "Site Ops Hero"
&lt;/h2&gt;

&lt;p&gt;Let's get our hands dirty. We want to ask: "What's the status of site_dallas_001?" and get a correlated answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: The Magic Lambda (Your Action Group)&lt;/strong&gt;&lt;br&gt;
Your agent needs a bridge to the real world. We’ll use a Lambda function. When the agent calls it, it sends a JSON event that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"actionGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NetworkTools"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apiPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/get-site-alarms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&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="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;"site_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"site_dallas_001"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tip: Bedrock events aren't the same as API Gateway events! You’ll need to parse the &lt;code&gt;apiPath&lt;/code&gt; and &lt;code&gt;parameters&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Set up the Specialist Agents&lt;/strong&gt;&lt;br&gt;
In the Bedrock Console (or via Boto3), create your specialist agents. Give them Knowledge Bases so they can read your documentation stored in S3.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro-tip: Use Titan Text Embeddings v2 to turn your PDFs into "vector gold" that the agent can search in milliseconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Summon the Supervisor&lt;/strong&gt;&lt;br&gt;
Create a new agent and enable Collaboration Mode. Associate your specialist agents as "collaborators." Now, when you talk to the Supervisor, it creates a plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask the Scout for active alarms.&lt;/li&gt;
&lt;li&gt;Ask the Alchemist if performance is dropping.&lt;/li&gt;
&lt;li&gt;Combine the answers and tell the human exactly what’s broken.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  💰 Budgeting Your "Mana" (Cost Optimization)
&lt;/h2&gt;

&lt;p&gt;Running a whole team of AI agents can get pricey if you're not careful. Here’s how to keep your AWS bill from going "Critical Hit" on your wallet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tiered Intelligence&lt;/strong&gt;: Use the "big brain" (Claude 3.5 Sonnet or Nova Premier) for the Supervisor who needs to plan. Use the "fast brain" (Nova Lite or Micro) for simple sub-tasks. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt Caching&lt;/strong&gt;: If you’re sending the same 50-page manual to your agent every time, Prompt Caching can save you up to $90\%$ on input tokens. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch Inference&lt;/strong&gt;: If you’re doing something that doesn’t need an answer right now (like summarizing yesterday's logs), use Batch Mode for a 50% discount.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🛡 Staying Safe (Guardrails)
&lt;/h2&gt;

&lt;p&gt;You don't want your agent going rogue. Use &lt;strong&gt;Amazon Bedrock Guardrails&lt;/strong&gt; to set "Denied Topics." For example, you can explicitly forbid the agent from giving out employee home addresses or venting about its boss. You can even set up Contextual Grounding Checks to make sure the agent only answers based on your data, not its own imagination (aka hallucinations).&lt;/p&gt;

&lt;h2&gt;
  
  
  🏁 Conclusion: Your Adventure Awaits!
&lt;/h2&gt;

&lt;p&gt;Building multi-agent systems on AWS is like moving from playing a single-instrument solo to conducting a full philharmonic orchestra. It’s more complex, sure, but the results are magical.&lt;/p&gt;

&lt;p&gt;Whether you're automating SRE tasks  or building a Pokémon battle advisor, the tools are all there in Bedrock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;What are you waiting for? Go build something awesome! 🚀✨&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;P.S. When you're done, remember to delete your S3 buckets and Lambda functions so you don't wake up to a "Surprise Invoice" Boss Fight.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>machinelearning</category>
      <category>agents</category>
      <category>programming</category>
    </item>
    <item>
      <title>The AWS Lambda Setting I Wish I’d Changed Earlier</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Tue, 30 Dec 2025 09:10:50 +0000</pubDate>
      <link>https://dev.to/aws-builders/the-aws-lambda-setting-i-wish-id-changed-earlier-45i0</link>
      <guid>https://dev.to/aws-builders/the-aws-lambda-setting-i-wish-id-changed-earlier-45i0</guid>
      <description>&lt;p&gt;I've been using AWS Lambda for quite a few years, and like many people, I left the architecture on its default settings, i.e. x86 &lt;em&gt;(Without thinking 2x)&lt;/em&gt;. It worked really well, it scaled as needed, but the truth unfolded when invoices showed up. End of Story. &lt;strong&gt;&lt;em&gt;Fine Della Storia&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Upon closer examination of the situation, I paid more attention to the &lt;strong&gt;execution time &amp;amp; cost&lt;/strong&gt;, and then I thought to experiment with &lt;strong&gt;Arm64&lt;/strong&gt;, i.e. &lt;em&gt;AWS Graviton&lt;/em&gt;. And the rare cloud optimisations that actually delivered, followed. 🎇 &lt;strong&gt;A VISIBLE WIN&lt;/strong&gt; 🎇&lt;br&gt;
.&lt;br&gt;
.&lt;br&gt;
.&lt;br&gt;
&lt;em&gt;Now the main Question&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9zwmbgyideqw8z486yp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9zwmbgyideqw8z486yp.gif" alt="thinkging" width="498" height="329"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What led me to try &lt;em&gt;Arm64&lt;/em&gt;?
&lt;/h2&gt;

&lt;p&gt;The answer's as simple as it gets: A few Lambda handling API Requests &amp;amp; Background jobs were being called 1,000,000s of times per month and frankly, each one looked quite cheap, individually. But when bunched together, they burned a steamy hole in my pocket every month.&lt;/p&gt;

&lt;p&gt;Now being a &lt;strong&gt;lazy_developer&lt;/strong&gt; who uses snake cases while programming, I said &lt;strong&gt;NO&lt;/strong&gt; to refactoring the logic or the code. I just wanted a clean performance win. And that's when... &lt;strong&gt;Arm64&lt;/strong&gt; looked like a low-risk bet to me.&lt;/p&gt;
&lt;h2&gt;
  
  
  Time taken towards the CHANGE!!!
&lt;/h2&gt;

&lt;p&gt;Now, when I say I moved from x86 to Arm64, the first thing to pop in your head would be, "Nah! It's gonna take forever to change." Right? WRONG. I changed the lambda architecture from x86 to Arm64, redeployed and tested it within a matter of hours (And that's because I chose NOT to read the documentation first 🥲. It could have been sooner). That was it. No code was changed, no dependencies were broken, and no surprises. This alone was... unexpected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faumcar68onbwgi0worvr.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faumcar68onbwgi0worvr.gif" alt="magic" width="220" height="144"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What were my ACTUAL Observations
&lt;/h2&gt;

&lt;p&gt;The moment I started moving only a few Lambdas functions to Arm64, the execution time dropped noticeably, especially for CPU-based logic. And guess what... the Cold Start felt snappier on lightweight Node.js &amp;amp; Python functions. In the end, the monthly lambda cost dip without touching memory allocation felt like a cherry on top. &lt;/p&gt;

&lt;p&gt;So, nothing dramatic for every invocation, but at scale, the difference was truly impossible to ignore.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reality Check: The Cost
&lt;/h2&gt;

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

&lt;p&gt;Lambda billing isn't rocket science. It's simple maths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution time * Memory * Invocations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your function runs just 20-30 milliseconds faster, and it is executed millions of times, that tiny change in the execution speed turns into a new &lt;strong&gt;Apple Watch&lt;/strong&gt; (Yes, no kidneys needed 😜). This was the moment Arm64 stopped feeling like an optimisation experiment &amp;amp; started feeling like a worthy replacement to x86 as &lt;em&gt;default choice&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did Arm64 work the best for ME?
&lt;/h2&gt;

&lt;p&gt;In my small but meaningful experience, Arm64 shone brighter than star &lt;strong&gt;Sirius&lt;/strong&gt;, when I used it for API Gateway-backed Lambdas, Event Driven pipelines, background jobs, schedulers, data processing &amp;amp; automation or even for lightweight ML inference. In short, I opted for Arm64, where I used modern serverless workloads with updated runtimes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I would never use Arm64?
&lt;/h2&gt;

&lt;p&gt;It's not like I am completely against x86 now. I still used it in my project for some scenarios. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I kept x86 for older functions with legacy native binaries&lt;/li&gt;
&lt;li&gt;Or for rare dependencies that weren't Arm-ready, yet&lt;/li&gt;
&lt;li&gt;Or for the code, that I didn't want to touch, being that close to the deadline. (Programmers, IYKYK). For those who don't, we don't want the project go&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  What changes in the future?
&lt;/h2&gt;

&lt;p&gt;In the future, if I am starting a new serverless project (Or today, who knows):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I will definitely go for Arm64, by default&lt;/li&gt;
&lt;li&gt;I am gonna only move back to x86, only &amp;amp; only, if something explicitly breaks (Or my client doesn't love their 💵)&lt;/li&gt;
&lt;li&gt;Will benchmark early, instead of assuming&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Arm64 on AWS Lambda isn't a future feature, it's just a teeny tiny and quiet &lt;strong&gt;upgrade&lt;/strong&gt; that's already paying off its cost in real-life systems. It's not theoretical. IT's PRACTICAL. Well, if you care about factors such as performance, cost &amp;amp; efficiency and you're running modern hsuper heavy serverless workloads, listen to me (Or read this carefully): TRY IT ONCE. 9/10 Chances? Like me, you won't turn back 😉.&lt;/p&gt;

&lt;p&gt;Once again, this was my personal experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Buon Anno
&lt;/h3&gt;

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

&lt;h2&gt;
  
  
  HappyCoding
&lt;/h2&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
    <item>
      <title>How I Automated Document Insights Using AWS Textract, Bedrock, and QuickSight</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Fri, 12 Sep 2025 17:35:28 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-i-automated-document-insights-using-aws-textract-bedrock-and-quicksight-h7p</link>
      <guid>https://dev.to/aws-builders/how-i-automated-document-insights-using-aws-textract-bedrock-and-quicksight-h7p</guid>
      <description>&lt;h2&gt;
  
  
  Hello Everyone 👋
&lt;/h2&gt;

&lt;p&gt;So I had a bunch of scanned invoices and PDFs lying around from an old freelance gig. Manually reading them and extracting data felt like a punishment. Instead of going through them one by one, I decided to try something smarter (I know, common &lt;em&gt;Developer Syndrome&lt;/em&gt; 😜), to build a small AWS workflow that could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read scanned docs,&lt;/li&gt;
&lt;li&gt;Summarise key info,&lt;/li&gt;
&lt;li&gt;Convert it to structured data, and&lt;/li&gt;
&lt;li&gt;Visualise everything inside a dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s how I did it, step by step. You can follow the same process and tweak it for contracts, reports, receipts, whatever you’ve got.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Step 1: Set Up Your AWS Environment (Production-ready)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Pick a single region that supports everything
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goal:&lt;/strong&gt; Keep &lt;em&gt;S3, Textract, Bedrock,&lt;/em&gt; and &lt;em&gt;QuickSight&lt;/em&gt; in the &lt;strong&gt;same region&lt;/strong&gt; to avoid cross-region latency/fees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to choose:&lt;/strong&gt; Check that your chosen region offers &lt;strong&gt;Bedrock&lt;/strong&gt; (model availability varies) and &lt;strong&gt;Textract&lt;/strong&gt;. If QuickSight isn’t in that region, use &lt;strong&gt;Athena + Glue&lt;/strong&gt; as a bridge (S3 anywhere → Athena in QuickSight’s region).&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Edge cases&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Bedrock not in your favorite region:&lt;/em&gt; run &lt;strong&gt;Textract + S3&lt;/strong&gt; where your data lives, then send &lt;strong&gt;plain text only&lt;/strong&gt; to Bedrock in a supported region (privacy + cost trade-off).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Corporate constraint on regions:&lt;/em&gt; document the exception and add an S3 Replication rule or use Athena.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  2) Create an S3 bucket with the right defaults
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; &lt;code&gt;doc-insights-&amp;lt;env&amp;gt;-&amp;lt;region&amp;gt;-&amp;lt;account-id&amp;gt;&lt;/code&gt; (e.g., &lt;code&gt;doc-insights-prod-eu-central-1-123456789012&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Settings to enable right away&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Block Public Access&lt;/strong&gt;: ON (all four toggles).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioning&lt;/strong&gt;: ON (lets you recover bad parses).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default encryption&lt;/strong&gt;: &lt;strong&gt;SSE-KMS&lt;/strong&gt; (customer-managed key) for auditability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Object Ownership&lt;/strong&gt;: Bucket owner enforced (disables ACLs, simplifies perms).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle rules&lt;/strong&gt;:&lt;/li&gt;
&lt;li&gt;Move &lt;code&gt;raw/&lt;/code&gt; to infrequent access after N days.&lt;/li&gt;
&lt;li&gt;Expire &lt;code&gt;tmp/&lt;/code&gt; after M days.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;processed/&lt;/code&gt; longer for audits.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Folder layout (simple + scalable)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  s3://doc-insights-bucket/
    raw/               # uploads: PDFs, images
    textract-json/     # raw JSON from Textract (per doc)
    extracted/         # normalized tables/kv (CSV/Parquet)
    summaries/         # Bedrock text summaries (JSON/MD)
    processed/         # final CSVs for BI
    logs/              # app / pipeline logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Edge cases&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Big uploads / flaky networks:&lt;/em&gt; use &lt;strong&gt;multi-part upload&lt;/strong&gt; (SDK/CLI handles this).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;PII/PHI compliance:&lt;/em&gt; restrict bucket to VPC endpoints (below), rotate KMS keys, and enable &lt;strong&gt;CloudTrail data events&lt;/strong&gt; for S3.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Cross-account access:&lt;/em&gt; prefer &lt;strong&gt;bucket policy&lt;/strong&gt; + &lt;strong&gt;role assumption&lt;/strong&gt; over sharing long-lived keys.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  3) IAM roles &amp;amp; least-privilege policies
&lt;/h3&gt;

&lt;p&gt;Create three roles (one per workload) and scope them tightly:&lt;/p&gt;

&lt;h3&gt;
  
  
  a) &lt;strong&gt;Textract role&lt;/strong&gt; (used by your Lambda/ETL or notebook)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trusted entity:&lt;/strong&gt; &lt;code&gt;lambda.amazonaws.com&lt;/code&gt; (or your compute service)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline policy (example)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&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="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"textract:AnalyzeDocument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"textract:StartDocumentAnalysis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"textract:GetDocumentAnalysis"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::doc-insights-bucket/*"&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="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::doc-insights-bucket"&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="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"kms:Encrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"kms:GenerateDataKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"kms:DescribeKey"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:kms:REGION:ACCOUNT:key/KEY_ID"&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;blockquote&gt;
&lt;p&gt;Use &lt;strong&gt;Start/Get&lt;/strong&gt; APIs for async jobs (multi-page PDFs); &lt;strong&gt;AnalyzeDocument&lt;/strong&gt; is fine for small/sync runs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  b) &lt;strong&gt;Bedrock role&lt;/strong&gt; (for summarization)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&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="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"bedrock:InvokeModel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"bedrock:InvokeModelWithResponseStream"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::doc-insights-bucket/*"&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="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"kms:Encrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"kms:GenerateDataKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"kms:DescribeKey"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:kms:REGION:ACCOUNT:key/KEY_ID"&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;blockquote&gt;
&lt;p&gt;Limit &lt;code&gt;Resource&lt;/code&gt; to the specific model ARNs once you pick them (Claude, Mistral, etc.).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  c) &lt;strong&gt;QuickSight service role access to S3&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Inside &lt;strong&gt;QuickSight → Admin → Security &amp;amp; permissions&lt;/strong&gt;, grant access to your bucket. Backing IAM should allow:&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;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&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="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::doc-insights-bucket/processed/*"&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="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::doc-insights-bucket"&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;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Edge cases&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;SSE-KMS blocks reads:&lt;/em&gt; add the QuickSight role principal to your &lt;strong&gt;KMS key policy&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Cross-account S3:&lt;/em&gt; add a &lt;strong&gt;bucket policy&lt;/strong&gt; that allows the QuickSight account role + use a &lt;strong&gt;manifest file&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  4) KMS key policy that won’t bite you later
&lt;/h3&gt;

&lt;p&gt;Add all compute roles and the QuickSight role to the &lt;strong&gt;key policy&lt;/strong&gt; (not just IAM perms), e.g.:&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;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&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="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"EnableRoot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::ACCOUNT:root"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"kms:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"AllowUseOfKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::ACCOUNT:role/TextractRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::ACCOUNT:role/BedrockRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::ACCOUNT:role/service-role/QuickSight-ROLE"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"kms:Encrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"kms:GenerateDataKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"kms:DescribeKey"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="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;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Edge cases&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;AccessDenied (kms:Decrypt) in QuickSight/Athena:&lt;/em&gt; 99% of the time it’s the key policy missing the service role.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  5) Networking (private, compliant, and fast)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create VPC interface endpoints (PrivateLink)&lt;/strong&gt; for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt; (Gateway endpoint), &lt;strong&gt;Textract&lt;/strong&gt;, &lt;strong&gt;Bedrock&lt;/strong&gt;, &lt;strong&gt;STS&lt;/strong&gt;, &lt;strong&gt;Logs&lt;/strong&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Lock your compute (Lambda/ECS/EC2) to &lt;strong&gt;no-internet&lt;/strong&gt; and route AWS traffic via endpoints.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Edge cases&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;QuickSight can’t sit in your VPC:&lt;/em&gt; it’s managed; keep the &lt;strong&gt;S3 bucket policy open to QuickSight role&lt;/strong&gt; (not public) and allow KMS for that role.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Corporate egress proxy:&lt;/em&gt; verify Bedrock/Textract endpoints are reachable or use endpoints.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  6) Turn on Amazon Bedrock access (once per account/region)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;In the &lt;strong&gt;Bedrock console&lt;/strong&gt;, enable model access (Claude/Mistral etc.). Some models require an &lt;strong&gt;opt-in&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Edge cases&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Model not visible:&lt;/em&gt; you’re in a region without that model, or access not granted yet.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Throughput errors:&lt;/em&gt; request &lt;strong&gt;service quota&lt;/strong&gt; increases for &lt;code&gt;InvokeModel&lt;/code&gt; if you batch summaries.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  7) Enable Amazon QuickSight
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Choose the same &lt;strong&gt;region&lt;/strong&gt; as your S3 if possible.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Security &amp;amp; permissions&lt;/strong&gt;, tick your bucket and (if needed) &lt;strong&gt;Athena&lt;/strong&gt; access.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Decide &lt;strong&gt;SPICE&lt;/strong&gt; vs &lt;strong&gt;Direct Query&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPICE&lt;/strong&gt; (in-memory) is faster; watch &lt;strong&gt;capacity limits&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct&lt;/strong&gt; is fine for small CSVs; for large data, prefer &lt;strong&gt;Parquet in S3 via Athena&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Edge cases&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;CSV &amp;gt; size limit / too many rows:&lt;/em&gt; convert to &lt;strong&gt;Parquet&lt;/strong&gt; and query via &lt;strong&gt;Athena&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Timezone mismatches:&lt;/em&gt; set dataset time zone explicitly; normalize date formats (ISO-8601).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  8) Budget, cost alerts, and audit
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS Budgets:&lt;/strong&gt; create a monthly budget with email alerts (S3 + Textract + Bedrock + QuickSight).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudTrail data events:&lt;/strong&gt; ON for S3 (per-object audit).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch Logs:&lt;/strong&gt; centralize Lambda/app logs to &lt;code&gt;logs/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Edge cases&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Sudden spikes:&lt;/em&gt; enable &lt;strong&gt;S3 object-level CloudWatch metrics&lt;/strong&gt; and add alarms on object count/size in &lt;code&gt;raw/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  9) File formats &amp;amp; naming that make your life easy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uploads (raw):&lt;/strong&gt; accept PDF/JPEG/PNG/TIFF; normalize on ingest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outputs:&lt;/strong&gt; prefer &lt;strong&gt;CSV (QuickSight-friendly)&lt;/strong&gt; and/or &lt;strong&gt;Parquet&lt;/strong&gt; (Athena-friendly).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Names:&lt;/strong&gt; &lt;code&gt;raw/{yyyy}/{mm}/{dd}/{vendor}/{fileid}.pdf&lt;/code&gt;
&lt;code&gt;processed/{yyyy}/{mm}/{dd}/invoices.csv&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Edge cases&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Weird encodings:&lt;/em&gt; force &lt;strong&gt;UTF-8&lt;/strong&gt;, escape delimiters, include header row.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Duplicate filenames:&lt;/em&gt; use a UUID suffix or checksum in the key.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  10) Optional: IaC and CLI quick start
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Create bucket (CLI)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api create-bucket &lt;span class="nt"&gt;--bucket&lt;/span&gt; doc-insights-bucket &lt;span class="nt"&gt;--create-bucket-configuration&lt;/span&gt; &lt;span class="nv"&gt;LocationConstraint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$AWS_REGION&lt;/span&gt;
aws s3api put-bucket-encryption &lt;span class="nt"&gt;--bucket&lt;/span&gt; doc-insights-bucket &lt;span class="nt"&gt;--server-side-encryption-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{
  "Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms","KMSMasterKeyID":"arn:aws:kms:REGION:ACCOUNT:key/KEY_ID"}}]}'&lt;/span&gt;
aws s3api put-bucket-versioning &lt;span class="nt"&gt;--bucket&lt;/span&gt; doc-insights-bucket &lt;span class="nt"&gt;--versioning-configuration&lt;/span&gt; &lt;span class="nv"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Enabled
aws s3api put-public-access-block &lt;span class="nt"&gt;--bucket&lt;/span&gt; doc-insights-bucket &lt;span class="nt"&gt;--public-access-block-configuration&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s1"&gt;'{"BlockPublicAcls":true,"IgnorePublicAcls":true,"BlockPublicPolicy":true,"RestrictPublicBuckets":true}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Example lifecycle (CLI)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-bucket-lifecycle-configuration &lt;span class="nt"&gt;--bucket&lt;/span&gt; doc-insights-bucket &lt;span class="nt"&gt;--lifecycle-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{
  "Rules":[
    {"ID":"raw-to-ia","Filter":{"Prefix":"raw/"},"Status":"Enabled","Transitions":[{"Days":30,"StorageClass":"STANDARD_IA"}]},
    {"ID":"expire-tmp","Filter":{"Prefix":"tmp/"},"Status":"Enabled","Expiration":{"Days":7}}
  ]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📄 Step 2: Use Amazon Textract to Extract Text from Documents (Production-grade)
&lt;/h2&gt;

&lt;p&gt;Textract can do three things you’ll care about:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;OCR (lines/words)&lt;/strong&gt; → &lt;code&gt;DetectDocumentText&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic forms &amp;amp; tables&lt;/strong&gt; → &lt;code&gt;AnalyzeDocument&lt;/code&gt; with &lt;code&gt;FORMS&lt;/code&gt;, &lt;code&gt;TABLES&lt;/code&gt;, optionally &lt;code&gt;QUERIES&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invoice/receipt specialization&lt;/strong&gt; → &lt;code&gt;AnalyzeExpense&lt;/code&gt; (often best for invoices)&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If it’s an &lt;strong&gt;invoice/receipt&lt;/strong&gt;, try &lt;strong&gt;&lt;code&gt;AnalyzeExpense&lt;/code&gt;&lt;/strong&gt; first (it returns normalized fields like &lt;code&gt;VENDOR_NAME&lt;/code&gt;, &lt;code&gt;INVOICE_TOTAL&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If it’s a &lt;strong&gt;contract/form/report&lt;/strong&gt;, use &lt;strong&gt;&lt;code&gt;AnalyzeDocument&lt;/code&gt;&lt;/strong&gt; with &lt;code&gt;FORMS&lt;/code&gt; and &lt;code&gt;TABLES&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;&lt;code&gt;QUERIES&lt;/code&gt;&lt;/strong&gt; to target specific fields (e.g., “What is the due date?”) when layout varies wildly.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  1) Sync vs Async: choose the right API
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Synchronous&lt;/strong&gt; (single call; immediate result)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AnalyzeDocument&lt;/code&gt; (good for &lt;strong&gt;single-page&lt;/strong&gt; or &lt;strong&gt;small&lt;/strong&gt; docs; great for tests)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DetectDocumentText&lt;/code&gt; (basic OCR)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Asynchronous&lt;/strong&gt; (submit job → poll or page through results)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;StartDocumentAnalysis&lt;/code&gt; + &lt;code&gt;GetDocumentAnalysis&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Required for &lt;strong&gt;multi-page&lt;/strong&gt; and &lt;strong&gt;larger&lt;/strong&gt; PDFs/images&lt;/li&gt;
&lt;li&gt;Supports pagination via &lt;code&gt;NextToken&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Guidance:&lt;/strong&gt; Use &lt;strong&gt;sync&lt;/strong&gt; for quick console/dev tests or one-pagers; use &lt;strong&gt;async&lt;/strong&gt; for anything real (multi-page, batch).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  2) Pre-processing (massively improves accuracy)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input formats:&lt;/strong&gt; PDF, PNG, JPEG, TIFF work well; keep scans clean.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Quality:&lt;/strong&gt; Aim for readable text (good contrast, minimal blur). If possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deskew, denoise, increase contrast, convert to grayscale/PNG.&lt;/li&gt;
&lt;li&gt;Fix rotation (many scanners rotate by 90° or 180°).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Encrypted or password-protected PDFs:&lt;/strong&gt; decrypt before upload.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Color:&lt;/strong&gt; Grayscale often beats noisy full-color scans.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-page splits:&lt;/strong&gt; If you hit page/file limits, split before upload.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;File names:&lt;/strong&gt; Include vendor/date hints in the key to simplify downstream routing (e.g., &lt;code&gt;raw/2025/09/11/vendorX/invoice_123.pdf&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  3) Minimal but robust code paths (Python/boto3)
&lt;/h3&gt;

&lt;h4&gt;
  
  
  A. &lt;strong&gt;Synchronous&lt;/strong&gt;: quick test for forms + tables
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;textract&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;textract&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;eu-central-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;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Document&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;S3Object&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;Bucket&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;doc-insights-bucket&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;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice1.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="n"&gt;FeatureTypes&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;FORMS&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;TABLES&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;blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&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;Blocks&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="c1"&gt;# → Save raw JSON, parse KEY_VALUE_SET/TABLE blocks later
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  B. &lt;strong&gt;Asynchronous&lt;/strong&gt;: production for multi-page docs (handles pagination + retries)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;itertools&lt;/span&gt;

&lt;span class="n"&gt;textract&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;textract&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;eu-central-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;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_document_analysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;DocumentLocation&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;S3Object&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;Bucket&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;doc-insights-bucket&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;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;long-invoice.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="n"&gt;FeatureTypes&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;FORMS&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;TABLES&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;job_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JobId&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;get_all_pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_wait_s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;next_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="c1"&gt;# Wait for completion (simple poll)
&lt;/span&gt;    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IN_PROGRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IN_PROGRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_document_analysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JobId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NextToken&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;next_token&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;next_token&lt;/span&gt; \
              &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;textract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_document_analysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JobId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;job_id&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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JobStatus&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;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IN_PROGRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# If first page says in progress, wait; otherwise collect &amp;amp; keep paging
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;next_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;res&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;NextToken&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;next_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sleep&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;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_wait_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Textract job took too long.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUCCEEDED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;res&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;NextToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_document_analysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JobId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NextToken&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NextToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&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;Textract job failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;res&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;StatusMessage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;pages&lt;/span&gt;

&lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_all_pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Combine blocks across pages for parsing
&lt;/span&gt;&lt;span class="n"&gt;all_blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_iterable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  C. &lt;strong&gt;Invoices/Receipts&lt;/strong&gt;: specialized &lt;code&gt;AnalyzeExpense&lt;/code&gt; (often higher accuracy on totals/dates)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;textract&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;textract&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;eu-central-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;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Document&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;S3Object&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;Bucket&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;doc-insights-bucket&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;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice1.pdf&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="c1"&gt;# resp["ExpenseDocuments"][0]["SummaryFields"] includes types like:
# VENDOR_NAME, INVOICE_RECEIPT_DATE, INVOICE_RECEIPT_ID, TOTAL, DUE_DATE
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  D. &lt;strong&gt;Targeted fields&lt;/strong&gt; with &lt;code&gt;QUERIES&lt;/code&gt; (when layouts vary)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="n"&gt;textract&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;textract&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;eu-central-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;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Document&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;S3Object&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;Bucket&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;doc-insights-bucket&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;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice1.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="n"&gt;FeatureTypes&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;QUERIES&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;TABLES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# you can mix with FORMS too
&lt;/span&gt;    &lt;span class="n"&gt;QueriesConfig&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;Queries&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the invoice number?&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;Alias&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;invoice_no&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;Text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the total amount?&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;Alias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_amount&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;Text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the due date?&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;Alias&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;due_date&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You can run &lt;strong&gt;QUERIES + TABLES&lt;/strong&gt; together to pull exact fields and still capture line items.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  4) How to parse Textract blocks safely (just enough for Step 2)
&lt;/h3&gt;

&lt;p&gt;Textract represents a &lt;strong&gt;graph&lt;/strong&gt; of &lt;code&gt;Blocks&lt;/code&gt; with &lt;code&gt;Relationships&lt;/code&gt;. The key ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PAGE&lt;/code&gt;, &lt;code&gt;LINE&lt;/code&gt;, &lt;code&gt;WORD&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KEY_VALUE_SET&lt;/code&gt; (with &lt;code&gt;EntityTypes&lt;/code&gt;: &lt;code&gt;KEY&lt;/code&gt; or &lt;code&gt;VALUE&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TABLE&lt;/code&gt; → &lt;code&gt;CELL&lt;/code&gt; (cell has row/col indices)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;QUERY&lt;/code&gt; → &lt;code&gt;ANSWER&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Parsing outline (KV):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a dict of blocks by &lt;code&gt;Id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Collect all &lt;code&gt;KEY_VALUE_SET&lt;/code&gt; blocks.&lt;/li&gt;
&lt;li&gt;For each &lt;code&gt;KEY&lt;/code&gt; block, follow &lt;code&gt;Relationships&lt;/code&gt; to its &lt;code&gt;VALUE&lt;/code&gt; block.&lt;/li&gt;
&lt;li&gt;Extract text by concatenating child &lt;code&gt;WORD&lt;/code&gt; blocks; track &lt;code&gt;Confidence&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Normalize keys (&lt;code&gt;vendor&lt;/code&gt;, &lt;code&gt;invoice no&lt;/code&gt;, etc.) and keep the &lt;code&gt;BoundingBox&lt;/code&gt; for traceability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Parsing outline (TABLE):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find &lt;code&gt;TABLE&lt;/code&gt; blocks, get their child &lt;code&gt;CELL&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;(RowIndex, ColumnIndex)&lt;/code&gt; to reconstruct rows.&lt;/li&gt;
&lt;li&gt;Heuristically detect &lt;strong&gt;headers&lt;/strong&gt; (first row or bold-ish/upper-case hints if present in text).&lt;/li&gt;
&lt;li&gt;Output as CSV; preserve page number.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;We’ll do the full CSV/JSON conversion in &lt;strong&gt;Step 3&lt;/strong&gt;. Here we only ensure you’ve got clean raw JSON saved and a clear parser plan.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  5) Storage &amp;amp; idempotency
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Always&lt;/strong&gt; save raw responses (one file per source doc):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;textract-json/&amp;lt;doc-id&amp;gt;.json&lt;/code&gt; (for &lt;code&gt;AnalyzeDocument&lt;/code&gt;/&lt;code&gt;DetectDocumentText&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;textract-json/&amp;lt;doc-id&amp;gt;.expense.json&lt;/code&gt; (for &lt;code&gt;AnalyzeExpense&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Add metadata to input S3 objects (e.g., &lt;code&gt;processed=true&lt;/code&gt;, &lt;code&gt;textract_job_id=...&lt;/code&gt;) to avoid reprocessing.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Use S3 object ETag/size as a cheap duplicate detector.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  6) Confidence, validation &amp;amp; fallbacks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confidence thresholds:&lt;/strong&gt; treat fields under a threshold (e.g., &lt;code&gt;&amp;lt; 0.8&lt;/code&gt;) as &lt;strong&gt;“needs review.”&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Numeric sanity checks:&lt;/strong&gt; parse amounts with a currency regex, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sum(line_items) ≈ total (± a few cents, taxes/discounts considered)&lt;/li&gt;
&lt;li&gt;date is valid &amp;amp; plausible (e.g., due date ≥ invoice date)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Disambiguation:&lt;/strong&gt; fight common OCR confusions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;O&lt;/code&gt;↔&lt;code&gt;0&lt;/code&gt;, &lt;code&gt;l&lt;/code&gt;/&lt;code&gt;I&lt;/code&gt;↔&lt;code&gt;1&lt;/code&gt;, &lt;code&gt;S&lt;/code&gt;↔&lt;code&gt;5&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;↔&lt;code&gt;8&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fallback order for invoices:&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;AnalyzeExpense&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AnalyzeDocument (FORMS+TABLES)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DetectDocumentText&lt;/code&gt; + regex heuristics (as a last resort)

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop:&lt;/strong&gt; flag low-confidence docs to a review queue (CSV row with a &lt;code&gt;needs_review=1&lt;/code&gt; flag).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  7) Permissions &amp;amp; security edge cases (most common causes of failure)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt;: role needs &lt;code&gt;s3:GetObject&lt;/code&gt;, &lt;code&gt;s3:ListBucket&lt;/code&gt;, and if encrypted, &lt;strong&gt;&lt;code&gt;kms:Decrypt&lt;/code&gt;&lt;/strong&gt; on the CMK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KMS key policy&lt;/strong&gt;: include the &lt;strong&gt;calling role principal&lt;/strong&gt; (not just IAM permissions).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-account&lt;/strong&gt;: use bucket policy + role assumption; ensure KMS allows the assumable role’s principal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private networking&lt;/strong&gt;: if using VPC-only compute, add &lt;strong&gt;VPC endpoints&lt;/strong&gt; for Textract/S3/STS/Logs.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  8) Scaling, throughput, and cost control
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batching&lt;/strong&gt;: drive Textract from a queue (SQS) or Step Functions; cap concurrency to avoid throttling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries&lt;/strong&gt;: exponential backoff on &lt;code&gt;ThrottlingException&lt;/code&gt; and transient network errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;De-dup&lt;/strong&gt;: check if a doc was already processed (S3 metadata) before calling Textract again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialization saves money&lt;/strong&gt;: &lt;code&gt;AnalyzeExpense&lt;/code&gt; or &lt;code&gt;QUERIES&lt;/code&gt; can reduce post-processing and re-runs.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  9) Troubleshooting cheat-sheet
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fast fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AccessDenied&lt;/code&gt; on S3 or KMS&lt;/td&gt;
&lt;td&gt;Missing &lt;code&gt;s3:GetObject&lt;/code&gt; / &lt;code&gt;kms:Decrypt&lt;/code&gt; or KMS key policy excludes role&lt;/td&gt;
&lt;td&gt;Add actions to role &lt;strong&gt;and&lt;/strong&gt; role principal to &lt;strong&gt;KMS key policy&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job stuck “IN_PROGRESS” forever&lt;/td&gt;
&lt;td&gt;Not polling pages / ignoring &lt;code&gt;NextToken&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Implement full pagination loop until &lt;code&gt;NextToken&lt;/code&gt; is empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty or low-quality text&lt;/td&gt;
&lt;td&gt;Bad scans (blur, skew), wrong rotation, low contrast&lt;/td&gt;
&lt;td&gt;Preprocess image: deskew, increase contrast, convert to PNG, rotate correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fields missing on invoices&lt;/td&gt;
&lt;td&gt;Using generic forms instead of expense API&lt;/td&gt;
&lt;td&gt;Switch to &lt;strong&gt;&lt;code&gt;AnalyzeExpense&lt;/code&gt;&lt;/strong&gt; or add &lt;strong&gt;&lt;code&gt;QUERIES&lt;/code&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dates/amounts parsed wrong&lt;/td&gt;
&lt;td&gt;Locale/regex issues or OCR confusions&lt;/td&gt;
&lt;td&gt;Normalize formats (ISO-8601), add currency/number regex, apply character-swap heuristics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QuickSight can’t see outputs&lt;/td&gt;
&lt;td&gt;Using KMS + missing QS access&lt;/td&gt;
&lt;td&gt;Add QS role to &lt;strong&gt;bucket policy&lt;/strong&gt; and &lt;strong&gt;KMS key policy&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  10) Console workflow (great for validation)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Upload a sample to S3 → &lt;strong&gt;Textract Console&lt;/strong&gt; → &lt;strong&gt;Analyze&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Toggle &lt;strong&gt;Forms&lt;/strong&gt; + &lt;strong&gt;Tables&lt;/strong&gt;, confirm highlights match expectations.&lt;/li&gt;
&lt;li&gt;For invoices, test &lt;strong&gt;Expense analysis&lt;/strong&gt; and compare field names (often cleaner).&lt;/li&gt;
&lt;li&gt;Save the result JSON and move on to parsing (Step 3).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  ✏️ Step 3: Convert Textract Output to Structured Data (CSV or JSON)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Clean &amp;amp; normalize the extracted data
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep originals:&lt;/strong&gt; store the raw Textract JSON (read-only) so you can re-parse later without re-running Textract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whitespace &amp;amp; casing:&lt;/strong&gt; trim, collapse double spaces; keep a &lt;code&gt;_raw&lt;/code&gt; copy for each value if you’ll audit later.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Key normalization (FORMS/QUERIES):&lt;/strong&gt; map variants to canonical keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;invoice no&lt;/code&gt;, &lt;code&gt;inv #&lt;/code&gt;, &lt;code&gt;invoice number&lt;/code&gt; → &lt;code&gt;invoice_number&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;total&lt;/code&gt;, &lt;code&gt;grand total&lt;/code&gt;, &lt;code&gt;amount due&lt;/code&gt; → &lt;code&gt;total&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;supplier&lt;/code&gt;, &lt;code&gt;vendor&lt;/code&gt;, &lt;code&gt;billed by&lt;/code&gt; → &lt;code&gt;vendor_name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;invoice date&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt; → &lt;code&gt;invoice_date&lt;/code&gt;; &lt;code&gt;due date&lt;/code&gt; → &lt;code&gt;due_date&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dates:&lt;/strong&gt; convert to ISO-8601 (&lt;code&gt;YYYY-MM-DD&lt;/code&gt;). If ambiguous/parsing fails, leave blank and flag &lt;code&gt;needs_review&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Numbers &amp;amp; currency:&lt;/strong&gt; strip thousands separators, unify decimals (&lt;code&gt;.&lt;/code&gt;), extract currency symbol/code (&lt;code&gt;$&lt;/code&gt;→USD, &lt;code&gt;€&lt;/code&gt;→EUR, &lt;code&gt;£&lt;/code&gt;→GBP, &lt;code&gt;₨&lt;/code&gt;→PKR, &lt;code&gt;₹&lt;/code&gt;→INR). If none, set a default and flag for review.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confidence tracking:&lt;/strong&gt; carry Textract confidence (0–100). Anything below your threshold (e.g., &lt;strong&gt;80&lt;/strong&gt;) sets &lt;code&gt;needs_review = 1&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;OCR confusions:&lt;/strong&gt; apply gentle corrections when context demands numeric values (&lt;code&gt;O↔0&lt;/code&gt;, &lt;code&gt;I/l↔1&lt;/code&gt;, &lt;code&gt;S↔5&lt;/code&gt;, &lt;code&gt;B↔8&lt;/code&gt;) but still keep &lt;code&gt;needs_review&lt;/code&gt; if confidence was low.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  2) Choose your outputs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CSV (analytics-ready):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;KV (long format):&lt;/strong&gt; &lt;code&gt;doc_id, page, key_norm, value, confidence, needs_review&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tables (per page/table):&lt;/strong&gt; &lt;code&gt;doc_id, page, row, col, text, confidence, is_header&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invoice summary (if using Expense API):&lt;/strong&gt; one row per document with normalized fields&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;JSON (traceability):&lt;/strong&gt; keep structured JSON when you need bounding boxes/page indices for audits.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;(Optional) Parquet:&lt;/strong&gt; for Athena/QuickSight at scale.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  3) Dump key–value pairs to CSV
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What to capture:&lt;/strong&gt; each detected pair as one row in a &lt;strong&gt;long&lt;/strong&gt; table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;doc_id, page, key_raw, key_norm, value, conf_key, conf_val, needs_review&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Duplicates:&lt;/strong&gt; keep all candidates. Later, pick the &lt;strong&gt;highest confidence&lt;/strong&gt; (ties → closest to the label “Total”, latest on the page, or appears in a summary box).&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Missing values:&lt;/strong&gt; if a &lt;code&gt;KEY&lt;/code&gt; has no &lt;code&gt;VALUE&lt;/code&gt;, record &lt;code&gt;value = ""&lt;/code&gt; and set &lt;code&gt;needs_review = 1&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-line values:&lt;/strong&gt; join lines with a single space; preserve original in &lt;code&gt;value_raw&lt;/code&gt; if you keep audits.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  4) Convert tables from JSON to CSV
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reconstruct using cells:&lt;/strong&gt; one row per cell with indexes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;doc_id, page, row, col, text, confidence, span_rows, span_cols, is_header&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Headers:&lt;/strong&gt; default to &lt;strong&gt;row 1 = header&lt;/strong&gt;; mark &lt;code&gt;is_header = 1&lt;/code&gt;. If row 1 has poor confidence but contains many alphabetic tokens vs numeric rows below, still treat as header.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Merged cells:&lt;/strong&gt; repeat the merged text in the spanned area and record &lt;code&gt;span_rows/span_cols&lt;/code&gt; for fidelity.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-page tables:&lt;/strong&gt; keep separate by page; your business logic decides whether to stitch them later.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  5) Build the simple “wide” table for your dashboard
&lt;/h3&gt;

&lt;p&gt;Target columns from your outline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Invoice No (&lt;code&gt;invoice_number&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Date (&lt;code&gt;invoice_date&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Vendor (&lt;code&gt;vendor_name&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Amount (&lt;code&gt;total&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Due Date (&lt;code&gt;due_date&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;INV-123&lt;/td&gt;
&lt;td&gt;2025-08-03&lt;/td&gt;
&lt;td&gt;Alpha Supplies&lt;/td&gt;
&lt;td&gt;423.90&lt;/td&gt;
&lt;td&gt;2025-09-02&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Population priority:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expense API (if used):&lt;/strong&gt; &lt;code&gt;INVOICE_RECEIPT_ID&lt;/code&gt;, &lt;code&gt;INVOICE_RECEIPT_DATE&lt;/code&gt;, &lt;code&gt;VENDOR_NAME&lt;/code&gt;, &lt;code&gt;TOTAL&lt;/code&gt;, &lt;code&gt;DUE_DATE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FORMS/QUERIES:&lt;/strong&gt; after key normalization and numeric/date parsing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TABLES/OCR fallback:&lt;/strong&gt; totals row or summary box heuristics (e.g., last column named &lt;code&gt;Amount&lt;/code&gt;, &lt;code&gt;Total&lt;/code&gt;, &lt;code&gt;Balance&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also add helpful derived fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt; (&lt;code&gt;expense&lt;/code&gt;/&lt;code&gt;forms&lt;/code&gt;/&lt;code&gt;ocr&lt;/code&gt;), &lt;code&gt;needs_review&lt;/code&gt;, &lt;code&gt;month&lt;/code&gt;, &lt;code&gt;year&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  6) Quality &amp;amp; integrity checks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Arithmetic sanity:&lt;/strong&gt; if you have &lt;code&gt;subtotal&lt;/code&gt; and &lt;code&gt;tax&lt;/code&gt;, verify &lt;code&gt;subtotal + tax ≈ total&lt;/code&gt; (small tolerance for rounding). If not, &lt;code&gt;needs_review = 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date logic:&lt;/strong&gt; &lt;code&gt;due_date ≥ invoice_date&lt;/code&gt;; otherwise &lt;code&gt;needs_review = 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negatives &amp;amp; zeros:&lt;/strong&gt; &lt;code&gt;total&lt;/code&gt; must be &lt;code&gt;&amp;gt; 0&lt;/code&gt; for invoices; credits/notes may be negative tag with &lt;code&gt;doc_type = credit_note&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicates:&lt;/strong&gt; same &lt;code&gt;invoice_number&lt;/code&gt; appearing across different &lt;code&gt;doc_id&lt;/code&gt;s → keep the one with higher composite score (weighted confidence of critical fields) and mark others as &lt;code&gt;superseded&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two documents in one file:&lt;/strong&gt; if you detect two distinct vendors or invoice numbers in one &lt;code&gt;doc_id&lt;/code&gt;, set &lt;code&gt;multi_doc_suspected = 1&lt;/code&gt; and route to review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locale drift:&lt;/strong&gt; month names/currency symbols outside your default locale → still parse but set &lt;code&gt;locale_detected&lt;/code&gt; and &lt;code&gt;needs_review&lt;/code&gt; if parsing relied on heuristics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handwritten/low DPI scans:&lt;/strong&gt; values may be fragmented; prefer QUERIES for targeted fields and flag low confidence.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  7) Foldering
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;textract-json/            # raw API outputs (never edited)
extracted/
  kv/                     # &amp;lt;doc_id&amp;gt;_kv.csv
  tables/                 # &amp;lt;doc_id&amp;gt;_table_&amp;lt;page&amp;gt;.csv
  expense/                # &amp;lt;doc_id&amp;gt;_summary.csv, &amp;lt;doc_id&amp;gt;_line_items.csv (if used)
processed/
  invoices_wide.csv       # your dashboard input (or Parquet partitioned by year/month)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🧠 Step 4: Summarise the Document Using Bedrock
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) What you’ll summarise
&lt;/h3&gt;

&lt;p&gt;Feed Bedrock &lt;strong&gt;only what it needs&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Facts&lt;/strong&gt; you already extracted in Step 3 (invoice_number, vendor_name, total, due_date, currency, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top signal text&lt;/strong&gt;: page headers, totals area, vendor/address block, and (for invoices) the line-items table flattened to a few lines (don’t paste thousands of rows).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional full text&lt;/strong&gt;: chunked by page if you need narrative context.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Redact or truncate PII that isn’t needed (addresses beyond city/country, phone, full account numbers). Keep the &lt;strong&gt;doc_id&lt;/strong&gt; so you can trace back.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  2) Output format
&lt;/h3&gt;

&lt;p&gt;Ask Bedrock for &lt;strong&gt;structured JSON + a short narrative&lt;/strong&gt;. Example JSON schema:&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;"doc_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1–3 sentence human summary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key_fields"&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;"vendor_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;"string|null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string|null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invoice_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YYYY-MM-DD|null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"due_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YYYY-MM-DD|null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string|null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number|null"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"observations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;anomalies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;discounts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;late&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;notes&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;"data_quality"&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;"missing_fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"suspected_errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"confidence_note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This slots straight into your &lt;code&gt;processed/&lt;/code&gt; layer or QuickSight as a secondary dataset.&lt;/p&gt;




&lt;h3&gt;
  
  
  3) Prompting
&lt;/h3&gt;

&lt;p&gt;Use a &lt;strong&gt;system + user&lt;/strong&gt; style prompt; instruct the model not to invent data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You are a careful financial document assistant. Use ONLY the supplied text and fields.&lt;br&gt;
If a field is missing or unclear, return &lt;code&gt;null&lt;/code&gt; and add a helpful note in &lt;code&gt;data_quality&lt;/code&gt;.&lt;br&gt;
Output must be valid JSON matching the provided schema. Do not include any other text.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;User&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DOC_ID: {{doc_id}}

FACTS (from extraction):
- vendor_name: {{vendor_name}}
- invoice_number: {{invoice_number}}
- invoice_date: {{invoice_date}}
- due_date: {{due_date}}
- currency: {{currency}}
- total_amount: {{total}}

EXCERPTS (trimmed; page-tagged):
[PAGE 1]
{{top_of_page_text}}
[PAGE 2 - totals box]
{{totals_area_text}}
[OPTIONAL line items]
{{few_key_lines}}

TASK:
1) Produce a JSON object exactly matching this schema:
{{paste schema}}
2) Base fields on FACTS when available; only use EXCERPTS to confirm or fill blanks.
3) If numbers disagree, prefer FACTS but note the discrepancy in `data_quality.suspected_errors`.
4) Do not guess. Use `null` for unknowns.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this works&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;anchors&lt;/strong&gt; the model to your extracted fields (reduces hallucinations).&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;forces&lt;/strong&gt; a predictable JSON payload for downstream use.&lt;/li&gt;
&lt;li&gt;It adds a &lt;strong&gt;quality channel&lt;/strong&gt; for “known unknowns”.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  4) Model choice &amp;amp; params
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude (Anthropic)&lt;/strong&gt;: strong on instruction following &amp;amp; JSON fidelity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mistral&lt;/strong&gt;: concise, fast; good when payloads are small and schema is simple.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameters&lt;/strong&gt;: keep &lt;strong&gt;temperature low (≈0.0–0.2)&lt;/strong&gt; for factual stability; set &lt;strong&gt;max tokens&lt;/strong&gt; tight (e.g., 600–900) for cost control.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: If JSON validation fails sometimes, wrap with a &lt;strong&gt;“fix to valid JSON”&lt;/strong&gt; pass (or enable JSON mode where available).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  5) Long documents or Map/Reduce
&lt;/h3&gt;

&lt;p&gt;For multi-page or very verbose docs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Map step&lt;/strong&gt;: summarize &lt;strong&gt;each page&lt;/strong&gt; (fixed schema, plus page-level anomalies).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce step&lt;/strong&gt;: feed all page summaries (+ your Step-3 facts) into a second prompt that &lt;strong&gt;merges&lt;/strong&gt; into a single final JSON, breaking ties in favor of Step-3 facts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This avoids token limits and preserves page context.&lt;/p&gt;




&lt;h3&gt;
  
  
  6) Robust invocation
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;bedrock-runtime&lt;/code&gt; with the model you enabled (ids vary by region/provider). Two common patterns:&lt;/p&gt;

&lt;h3&gt;
  
  
  Non-streaming (simple)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="n"&gt;brt&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-runtime&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;YOUR_REGION&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_bedrock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&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="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic_version&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;bedrock-2023-05-31&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;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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system_prompt&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;role&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;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_prompt&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;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;brt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_id&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;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;out&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;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&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="c1"&gt;# Extract the text depending on provider; for Claude:
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,[])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&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;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Streaming
&lt;/h4&gt;

&lt;p&gt;Stream when you want low latency for UI, otherwise skip (adds complexity).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Exact request/response shapes differ by model family. Keep the &lt;strong&gt;modelId&lt;/strong&gt; and payload format configurable and test in the Bedrock console first.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  7) Validation &amp;amp; safety rails
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSON validation&lt;/strong&gt;: run a quick schema check (pydantic/jsonschema). If invalid, re-prompt the model with: “Return only valid JSON conforming to this schema. Do not add commentary.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No-invention rule&lt;/strong&gt;: instruct “If unsure, use &lt;code&gt;null&lt;/code&gt; and add a &lt;code&gt;data_quality&lt;/code&gt; note.” Penalize hallucinations by rejecting outputs that add fields you didn’t request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discrepancy handling&lt;/strong&gt;: when EXCERPTS disagree with FACTS, keep FACTS and add a note like:
&lt;em&gt;“Totals area shows 432.90 but extracted total is 423.90 (difference 9.00).”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII minimization&lt;/strong&gt;: don’t pass line-item text unless needed; mask account numbers (e.g., &lt;code&gt;****1234&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost control&lt;/strong&gt;: truncate excerpts, avoid full dumps; cap &lt;code&gt;max_tokens&lt;/code&gt;; batch summarize.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  8) Edge cases
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Edge case&lt;/th&gt;
&lt;th&gt;What to do in prompt / output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing total or due date&lt;/td&gt;
&lt;td&gt;Return &lt;code&gt;"total_amount": null&lt;/code&gt; / &lt;code&gt;"due_date": null&lt;/code&gt;; add a &lt;code&gt;data_quality.missing_fields&lt;/code&gt; entry.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple totals (invoice vs balance)&lt;/td&gt;
&lt;td&gt;Prefer FACTS; if absent, pick the one labeled “Total” near summary; note the alternative.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflicting currency symbols vs codes&lt;/td&gt;
&lt;td&gt;Prefer the currency from FACTS; if excerpt differs, note discrepancy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handwritten / low DPI&lt;/td&gt;
&lt;td&gt;Expect nulls and notes; don’t interpolate from context.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-currency line items&lt;/td&gt;
&lt;td&gt;Set currency to document currency; add observation like “Line items include mixed currency not used for total.”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-doc PDF&lt;/td&gt;
&lt;td&gt;If two vendors or two invoice numbers appear, say so in &lt;code&gt;observations&lt;/code&gt; and add &lt;code&gt;suspected_errors&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-English docs&lt;/td&gt;
&lt;td&gt;Add “Output in English.” Keep dates numeric (YYYY-MM-DD).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extremely long inputs&lt;/td&gt;
&lt;td&gt;Use Map/Reduce; only pass per-page top areas and final consolidation.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  9) Example “good” output
&lt;/h3&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;"doc_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-2025-0045"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invoice 0045 from Beta Traders for 89.50 EUR, due on 2025-08-28."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key_fields"&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;"vendor_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;"Beta Traders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0045"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invoice_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-08-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"due_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-08-28"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;89.50&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;"observations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Early-payment discount mentioned (2%)."&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;"data_quality"&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;"missing_fields"&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;"suspected_errors"&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;"confidence_note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"All fields aligned with extracted facts."&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;h3&gt;
  
  
  10) Where to store results
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Write JSON summaries to &lt;code&gt;summaries/&lt;/code&gt; and (optionally) a compact CSV view &lt;code&gt;processed/invoice_summaries.csv&lt;/code&gt; with &lt;code&gt;doc_id&lt;/code&gt;, &lt;code&gt;vendor_name&lt;/code&gt;, &lt;code&gt;total_amount&lt;/code&gt;, &lt;code&gt;due_date&lt;/code&gt;, &lt;code&gt;needs_review&lt;/code&gt;, &lt;code&gt;source='bedrock'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Join this dataset with &lt;code&gt;invoices_wide.csv&lt;/code&gt; for dashboard overlays (e.g., a tooltip with the narrative summary).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🗂️ Step 5: Store Final Data Back in S3
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Folder layout
&lt;/h3&gt;

&lt;p&gt;Use a layout that separates &lt;strong&gt;raw vs extracted vs consumable&lt;/strong&gt; and supports BI tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3://doc-insights-bucket/
  processed/
    wide/                 # final, dashboard-ready “wide” tables (CSV/Parquet)
    kv/                   # merged KV outputs across docs
    tables/               # flattened tables across docs
    expense/              # invoice summaries &amp;amp; line items (if using AnalyzeExpense)
    summaries/            # Bedrock JSON summaries
    parquet/              # same datasets in Parquet (for Athena/QuickSight at scale)
    _staging/             # temp writes (see atomic pattern)
    manifests/            # optional manifest/index files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Partitioning (for Athena/scale):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Prefer hive-style partitions for Parquet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;processed/parquet/invoices_wide/year=2025/month=09/day=12/part-0001.parquet&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Keep CSVs unpartitioned or lightly partitioned by month to avoid too many small files.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  2) Naming strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Document-scoped files:&lt;/strong&gt;
&lt;code&gt;processed/expense/summary/&amp;lt;doc_id&amp;gt;.csv&lt;/code&gt;
&lt;code&gt;processed/summaries/&amp;lt;doc_id&amp;gt;.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch/append files:&lt;/strong&gt;
&lt;code&gt;processed/wide/invoices_wide_YYYYMMDD.csv&lt;/code&gt; (or append into a single table and de-dupe by &lt;code&gt;doc_id&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Timestamped artifacts:&lt;/strong&gt; add an ISO timestamp or a monotonically increasing batch id:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.../2025-09-12T18-40-12Z_invoices_wide.csv&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Avoid collisions:&lt;/strong&gt; include &lt;code&gt;doc_id&lt;/code&gt; (or a UUID) in per-doc artifacts; never rely only on original filenames.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  3) Atomic write pattern
&lt;/h3&gt;

&lt;p&gt;S3 has strong read-after-write consistency, but clients can still read &lt;strong&gt;half-written&lt;/strong&gt; objects if your writer crashes mid-upload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write to &lt;code&gt;processed/_staging/&amp;lt;uuid&amp;gt;/&amp;lt;file.tmp&amp;gt;&lt;/code&gt; (or &lt;code&gt;status=writing&lt;/code&gt; tag).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; checksum/row count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy&lt;/strong&gt; to final key (e.g., &lt;code&gt;processed/wide/invoices_wide_20250912.csv&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Optionally delete the staging file, or keep with tag &lt;code&gt;status=archived&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Edge cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Large files (&amp;gt;5 GB):&lt;/strong&gt; use multipart upload; always send &lt;code&gt;Content-MD5&lt;/code&gt; for integrity when feasible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent writers:&lt;/strong&gt; add a lightweight &lt;strong&gt;lock&lt;/strong&gt; (S3 object &lt;code&gt;processed/_locks/invoices_wide.lock&lt;/code&gt; with short TTL) or write per-writer partitions then compact later.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  4) Metadata, tags, and headers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server-side encryption:&lt;/strong&gt; SSE-KMS with your CMK (consistent with earlier steps).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content-Type:&lt;/strong&gt; set correctly (&lt;code&gt;text/csv&lt;/code&gt;, &lt;code&gt;application/json&lt;/code&gt;, &lt;code&gt;application/parquet&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache-Control:&lt;/strong&gt; not crucial for data lakes; you can omit or set &lt;code&gt;no-cache&lt;/code&gt; for rapidly changing CSVs.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Object tags&lt;/strong&gt; (helpful for governance/search):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dataset=invoices&lt;/code&gt;, &lt;code&gt;stage=processed&lt;/code&gt;, &lt;code&gt;doc_id=&amp;lt;id&amp;gt;&lt;/code&gt;, &lt;code&gt;status=ready&lt;/code&gt;, &lt;code&gt;source=expense/forms/ocr/bedrock&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom metadata&lt;/strong&gt; (e.g., &lt;code&gt;x-amz-meta-rowcount&lt;/code&gt;, &lt;code&gt;x-amz-meta-hash&lt;/code&gt;): useful for quick validation.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  5) Security &amp;amp; access
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bucket policy:&lt;/strong&gt; already denies insecure transport; also allow the &lt;strong&gt;QuickSight service role&lt;/strong&gt; to &lt;code&gt;ListBucket&lt;/code&gt; and &lt;code&gt;GetObject&lt;/code&gt; on &lt;code&gt;processed/*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KMS key policy:&lt;/strong&gt; include the QuickSight role principal (not just IAM permissions) so it can decrypt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-account reads (optional):&lt;/strong&gt; prefer role assumption + bucket policy + KMS grants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC endpoints:&lt;/strong&gt; if compute is private, ensure endpoints for S3/KMS are present.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Edge cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;QuickSight can list but &lt;strong&gt;can’t read&lt;/strong&gt; → key policy is missing the role principal.&lt;/li&gt;
&lt;li&gt;Using &lt;strong&gt;SPICE&lt;/strong&gt;: watch dataset size limits; switch to &lt;strong&gt;Athena over Parquet&lt;/strong&gt; when CSVs grow.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  6) Data integrity &amp;amp; quality gates
&lt;/h3&gt;

&lt;p&gt;Before moving from &lt;code&gt;_staging&lt;/code&gt; → &lt;code&gt;processed&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema check:&lt;/strong&gt; headers exist and match expected column set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row count &amp;gt; 0&lt;/strong&gt; (or allow zero with a &lt;code&gt;note=empty_batch&lt;/code&gt; tag).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type validation:&lt;/strong&gt; dates parse (ISO-8601), amounts numeric, currency present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanity checks:&lt;/strong&gt; if files include &lt;code&gt;subtotal&lt;/code&gt;, &lt;code&gt;tax&lt;/code&gt;, &lt;code&gt;total&lt;/code&gt;, verify &lt;code&gt;subtotal + tax ≈ total&lt;/code&gt; (tolerance).&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Checksum:&lt;/strong&gt; compute SHA-256 of the file and store as sidecar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;processed/wide/invoices_wide_20250912.csv.sha256&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  7) Lifecycle &amp;amp; retention
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versioning:&lt;/strong&gt; keep enabled to roll back erroneous overwrites.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lifecycle rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transition &lt;code&gt;processed/_staging/&lt;/code&gt; to &lt;strong&gt;Expire after 7 days&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Transition &lt;code&gt;processed/*.csv&lt;/code&gt; to &lt;strong&gt;STANDARD_IA&lt;/strong&gt; after 30 days.&lt;/li&gt;
&lt;li&gt;Keep Parquet in &lt;strong&gt;STANDARD&lt;/strong&gt; (or &lt;strong&gt;GLACIER&lt;/strong&gt; if archived) based on access.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;S3 Inventory&lt;/strong&gt; (optional): enable for daily listings &amp;amp; encryption audit.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  8) Manifests &amp;amp; discovery (helps BI tools)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manifest file&lt;/strong&gt; lists the latest batch outputs:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  {
    "dataset": "invoices_wide",
    "updated_at": "2025-09-12T18:40:12Z",
    "files": [
      "s3://.../processed/wide/invoices_wide_20250912.csv"
    ],
    "row_count": 1243,
    "schema": ["doc_id","vendor_name","invoice_number","invoice_date","due_date","total","currency","needs_review","source","month","year"]
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save as &lt;code&gt;processed/manifests/invoices_wide.latest.json&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Glue Catalog&lt;/strong&gt; (for Athena): create/update a table pointing to your Parquet path with partitions &lt;code&gt;year, month, day&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  9) Eventing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;S3 → EventBridge&lt;/strong&gt; rule on &lt;code&gt;processed/&lt;/code&gt; prefix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trigger a Lambda to &lt;strong&gt;refresh QuickSight&lt;/strong&gt; (or kick an ingestion SPICE refresh).&lt;/li&gt;
&lt;li&gt;Or publish a Slack/Email notification with the manifest summary.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Idempotency:&lt;/strong&gt; include &lt;code&gt;run_id&lt;/code&gt; or &lt;code&gt;batch_id&lt;/code&gt; in event detail; ignore duplicates in the consumer.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  📊 Step 6: Create a Dashboard in QuickSight
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Pick your connection path
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Path A | S3 CSV (fast start)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best for small/medium datasets (≤ a few million rows per file).&lt;/li&gt;
&lt;li&gt;In QuickSight: &lt;strong&gt;Datasets → New dataset → S3&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Point to &lt;strong&gt;a single CSV&lt;/strong&gt; &lt;em&gt;or&lt;/em&gt; a &lt;strong&gt;manifest JSON&lt;/strong&gt; (recommended if you have many files).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Path B | Athena + Parquet (scales)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best for large data, partitions, or frequent refresh.&lt;/li&gt;
&lt;li&gt;Store Parquet at &lt;code&gt;processed/parquet/invoices_wide/year=YYYY/month=MM/day=DD/...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In QuickSight: &lt;strong&gt;Datasets → New dataset → Athena → select database &amp;amp; table&lt;/strong&gt; (from Glue Catalog).&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Edge cases&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If QuickSight is in a different region than your S3 bucket, prefer &lt;strong&gt;Athena&lt;/strong&gt; to avoid cross-region pain.&lt;/li&gt;
&lt;li&gt;Using SSE-KMS? Ensure the &lt;strong&gt;QuickSight service role&lt;/strong&gt; is in your &lt;strong&gt;KMS key policy&lt;/strong&gt; and bucket policy.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  2) Connect to your data
&lt;/h3&gt;

&lt;h4&gt;
  
  
  A) S3 with a manifest (many files)
&lt;/h4&gt;

&lt;p&gt;Create &lt;code&gt;processed/manifests/invoices_wide.latest.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fileLocations"&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="nl"&gt;"URIPrefixes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3://doc-insights-bucket/processed/wide/"&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;"globalUploadSettings"&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;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CSV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"delimiter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"textqualifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&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;"containsHeader"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in QuickSight S3 connector, paste this &lt;strong&gt;manifest file’s S3 URL&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  B) Athena (Parquet partitions)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Make sure your Glue table points to &lt;code&gt;processed/parquet/invoices_wide/&lt;/code&gt; and has partitions &lt;code&gt;year, month, day&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In QuickSight, choose &lt;strong&gt;Direct Query&lt;/strong&gt; (live) or &lt;strong&gt;Import to SPICE&lt;/strong&gt; (in-memory; faster but size-limited).&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Edge cases&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;“AccessDenied”&lt;/strong&gt; when previewing = missing KMS permission or bucket grant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No new data&lt;/strong&gt; after partition upload = run crawler/add partition OR enable partition projection.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  3) Prepare the dataset
&lt;/h3&gt;

&lt;p&gt;In the &lt;strong&gt;data prep&lt;/strong&gt; screen:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Map columns&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;invoice_number&lt;/code&gt; → Text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;invoice_date&lt;/code&gt;, &lt;code&gt;due_date&lt;/code&gt; → Date (format &lt;code&gt;YYYY-MM-DD&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;total&lt;/code&gt; → Decimal&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vendor_name&lt;/code&gt; → Text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;currency&lt;/code&gt; → Text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;needs_review&lt;/code&gt; → Integer/Boolean&lt;/li&gt;
&lt;li&gt;(Optional) &lt;code&gt;ingest_date&lt;/code&gt; or &lt;code&gt;processed_at&lt;/code&gt; → Date/Time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Create calculated fields (QuickSight expressions)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Month (bucket)&lt;/strong&gt;
&lt;code&gt;truncDate('MM', {invoice_date})&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Year&lt;/strong&gt;
&lt;code&gt;extract('YYYY', {invoice_date})&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overdue (flag)&lt;/strong&gt;
&lt;code&gt;ifelse({due_date} &amp;lt; now(), 1, 0)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Processing time (days)&lt;/strong&gt; – pick the source you actually have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you kept an ingest timestamp:
&lt;code&gt;dateDiff('DD', {invoice_date}, {ingest_date})&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Or from S3 key date (if you saved it):
&lt;code&gt;dateDiff('DD', {invoice_date}, {s3_ingest_date})&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clean total (fallback)&lt;/strong&gt; – if some totals are text:&lt;br&gt;&lt;br&gt;
&lt;code&gt;parseDecimal({total})&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Edge cases&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mixed date formats? Normalize upstream; otherwise use a &lt;strong&gt;calculated field&lt;/strong&gt; with &lt;code&gt;parseDate()&lt;/code&gt; per pattern and coalesce.&lt;/li&gt;
&lt;li&gt;Multiple currencies? Keep a &lt;strong&gt;currency&lt;/strong&gt; dimension; optional FX table join later.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  4) Build the visuals
&lt;/h3&gt;

&lt;h4&gt;
  
  
  A) Total amount by vendor (Top-N bar)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual&lt;/strong&gt;: Horizontal bar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X&lt;/strong&gt;: &lt;code&gt;sum({total})&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Y&lt;/strong&gt;: &lt;code&gt;vendor_name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort&lt;/strong&gt;: by &lt;code&gt;sum(total)&lt;/code&gt; desc&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter&lt;/strong&gt;: Top 10 vendors (add a Top N filter)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional&lt;/strong&gt;: color by &lt;code&gt;needs_review&lt;/code&gt; to spot messy vendors&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  B) Monthly invoice totals (trend)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual&lt;/strong&gt;: Line chart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X&lt;/strong&gt;: &lt;code&gt;truncDate('MM', {invoice_date})&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Y&lt;/strong&gt;: &lt;code&gt;sum({total})&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forecast&lt;/strong&gt; (optional): enable forecast to project next months&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooltip&lt;/strong&gt;: add &lt;code&gt;countDistinct({invoice_number})&lt;/code&gt; for volume context&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  C) Average processing time (days)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual&lt;/strong&gt;: KPI or bar by vendor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field&lt;/strong&gt;: &lt;code&gt;avg(dateDiff('DD', {invoice_date}, {ingest_date}))&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group by&lt;/strong&gt;: &lt;code&gt;vendor_name&lt;/code&gt; (or monthly bucket)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter&lt;/strong&gt;: exclude &lt;code&gt;needs_review = 1&lt;/code&gt; if you want only clean rows&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  D) Bonus quick wins
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overdue heatmap&lt;/strong&gt;: &lt;code&gt;vendor_name&lt;/code&gt; × &lt;code&gt;month&lt;/code&gt; with color &lt;code&gt;sum(ifelse({due_date} &amp;lt; now(), {total}, 0))&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality gate&lt;/strong&gt;: table filtered to &lt;code&gt;needs_review = 1&lt;/code&gt; (your manual review queue)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  5) Filters, controls, and parameters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Date range control&lt;/strong&gt;: Filter on &lt;code&gt;{invoice_date}&lt;/code&gt; with “Relative dates” (e.g., Last 12 months).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor control&lt;/strong&gt;: Dropdown on &lt;code&gt;vendor_name&lt;/code&gt; (multi-select).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence/quality toggle&lt;/strong&gt;: &lt;code&gt;needs_review&lt;/code&gt; = 0 by default (checkbox control).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Currency control&lt;/strong&gt; (if applicable): filter or parameter to select a single currency.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Edge cases&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your dataset mixes currencies, don’t sum across different codes in one visual unless you convert first.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  6) SPICE vs Direct Query
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPICE (Import)&lt;/strong&gt;: fastest visuals, schedule refreshes (e.g., hourly). Watch capacity limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct Query&lt;/strong&gt;: always live; best with Athena/Parquet; slightly slower per query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Recommended&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with &lt;strong&gt;SPICE&lt;/strong&gt; for S3 CSVs; switch to &lt;strong&gt;Athena + SPICE&lt;/strong&gt; as you scale.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  7) Refresh strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;S3 CSV path&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you &lt;strong&gt;overwrite one key&lt;/strong&gt; (e.g., &lt;code&gt;invoices_wide_latest.csv&lt;/code&gt;), set a refresh schedule (hourly/daily).&lt;/li&gt;
&lt;li&gt;If you &lt;strong&gt;append new files&lt;/strong&gt;, use a &lt;strong&gt;manifest&lt;/strong&gt; and schedule refresh.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Athena path&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After adding partitions, either run a crawler or use &lt;strong&gt;partition projection&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In QuickSight, schedule SPICE refresh or stay on Direct Query.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Edge cases&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refresh fails with KMS errors → add QuickSight role to CMK key policy.&lt;/li&gt;
&lt;li&gt;Stale data → you’re writing to new paths; either point QS at a &lt;strong&gt;stable key&lt;/strong&gt; or use manifests.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  8) Sharing, RLS, and governance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Share analysis → Publish dashboard&lt;/strong&gt;; grant users/groups access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row-Level Security (RLS)&lt;/strong&gt;: upload an RLS mapping dataset (&lt;code&gt;user&lt;/code&gt;, &lt;code&gt;vendor_name&lt;/code&gt;) to restrict rows per viewer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data dictionary tab&lt;/strong&gt;: add field descriptions so the team knows what “processing time” means.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  10) Minimal recipe
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Connect&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;S3: dataset → S3 → manifest → validate preview.&lt;/li&gt;
&lt;li&gt;Athena: dataset → Athena → select table.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Model&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Types: map date/number fields.&lt;/li&gt;
&lt;li&gt;Calcs: &lt;code&gt;month = truncDate('MM', {invoice_date})&lt;/code&gt;, &lt;code&gt;processing_days = dateDiff('DD', {invoice_date}, {ingest_date})&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Visualize&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Bar: &lt;code&gt;sum(total)&lt;/code&gt; by &lt;code&gt;vendor_name&lt;/code&gt; (Top 10).&lt;/li&gt;
&lt;li&gt;Line: &lt;code&gt;sum(total)&lt;/code&gt; by &lt;code&gt;month&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;KPI/Bar: &lt;code&gt;avg(processing_days)&lt;/code&gt; by vendor.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Control&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Date range (last 12 months).&lt;/li&gt;
&lt;li&gt;Vendor multi-select.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;needs_review&lt;/code&gt; toggle (default 0).&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Operationalize&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;SPICE refresh schedule (hourly/daily).&lt;/li&gt;
&lt;li&gt;If Athena: ensure partitions &amp;amp; permissions.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;This was honestly a fun little fortnight project.&lt;br&gt;
It saved me hours of manual work and gave me clean, visual summaries of everything I needed. By combining S3, Textract, Bedrock, and QuickSight, you can turn unstructured, scanned documents into analysis-ready data and clear dashboards. This pipeline handles extraction, normalisation, summarisation, governed storage, and visualisation, built with security (SSE-KMS), least-privilege IAM, and scalable patterns (async Textract, Parquet/Athena). It replaces manual review with consistent, auditable automation and surfaces vendor spend, monthly trends, and processing KPIs in minutes. From here, you can harden orchestration with Step Functions, add Row-Level Security in QuickSight, and extend the same approach to receipts, contracts, or reports. Small lift-big leverage.&lt;/p&gt;

&lt;p&gt;You could easily apply this to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Legal contracts&lt;/li&gt;
&lt;li&gt;Academic papers&lt;/li&gt;
&lt;li&gt;Medical prescriptions&lt;/li&gt;
&lt;li&gt;Tax docs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it out and get stuck, drop a comment, and I'd be happy to help.&lt;/p&gt;

&lt;h1&gt;
  
  
  HappyHacking 👋
&lt;/h1&gt;

</description>
      <category>aws</category>
      <category>machinelearning</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>🚀 Scaling React Like Big Tech: Folder Structures, Clean Code &amp; Beyond</title>
      <dc:creator>Mursal Furqan Kumbhar</dc:creator>
      <pubDate>Sat, 30 Aug 2025 18:55:22 +0000</pubDate>
      <link>https://dev.to/mursalfk/scaling-react-like-big-tech-folder-structures-clean-code-beyond-51bj</link>
      <guid>https://dev.to/mursalfk/scaling-react-like-big-tech-folder-structures-clean-code-beyond-51bj</guid>
      <description>&lt;h2&gt;
  
  
  Hey Everyone 👋
&lt;/h2&gt;

&lt;p&gt;I know it's been quite a long gap since my last article. But we are finally back with a bang.&lt;/p&gt;

&lt;p&gt;So if you’ve ever wondered how &lt;strong&gt;Airbnb, Shopify, or Meta&lt;/strong&gt; keep their codebases sane while thousands of engineers ship features daily — the answer is simple: &lt;strong&gt;structure + discipline + automation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This isn’t about writing code that just &lt;em&gt;works&lt;/em&gt;. It’s about writing code that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scales with your team.&lt;/li&gt;
&lt;li&gt;Survives onboarding of 10 new devs.&lt;/li&gt;
&lt;li&gt;Passes reviews without a hundred nitpicks.&lt;/li&gt;
&lt;li&gt;And doesn’t make you cry when you open it six months later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s break down what the big companies do differently — and how you can bring those practices into your own projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  📂 Folder Structure that Scales
&lt;/h2&gt;

&lt;p&gt;Top companies don’t just throw files into a random &lt;strong&gt;&lt;code&gt;components/&lt;/code&gt;&lt;/strong&gt; folder and hope for the best. They use &lt;strong&gt;feature-driven architecture&lt;/strong&gt;, which grows naturally with the product.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Traditional (Bad) Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;src/
├── components/
│   ├── Button.jsx
│   ├── Card.jsx
│   ├── Form.jsx
├── services/
├── hooks/
└── App.jsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks fine at first… until your app has &lt;strong&gt;300+ components&lt;/strong&gt; and no one knows which belongs to what.&lt;/p&gt;




&lt;h3&gt;
  
  
  ✅ Scalable (Feature-Based) Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;src/
├── features/
│   └── auth/
│       ├── components/
│       ├── hooks/
│       └── services/
├── shared/
│   ├── ui/
│   └── utils/
└── App.jsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 &lt;strong&gt;Key Difference&lt;/strong&gt;: Everything related to a single feature (Auth, Products, Cart, etc.) is grouped. Shared logic/components live in &lt;code&gt;shared/&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traditional (by type)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple to start&lt;/td&gt;
&lt;td&gt;Becomes chaos at scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Feature-Based&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Easy ownership, scalable, testable&lt;/td&gt;
&lt;td&gt;Slightly more setup upfront&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🎨 Design Systems &amp;amp; UI Libraries
&lt;/h2&gt;

&lt;p&gt;Big companies don’t reinvent the button every Tuesday. They use &lt;strong&gt;design systems&lt;/strong&gt; and &lt;strong&gt;UI libraries&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;Airbnb&lt;/strong&gt; → Lona&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Shopify&lt;/strong&gt; → Polaris&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Meta&lt;/strong&gt; → Reusable UI + strict guidelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Tools you can use: &lt;strong&gt;Chakra UI&lt;/strong&gt;, &lt;strong&gt;Tailwind + Headless UI&lt;/strong&gt;, &lt;strong&gt;Radix UI&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Visual Example
&lt;/h3&gt;

&lt;p&gt;❌ Without Design System:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"danger"&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"red"&lt;/span&gt; &lt;span class="na"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"red"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ With Design System:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DeleteButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Why Use Design Systems?&lt;/th&gt;
&lt;th&gt;Benefits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Consistency&lt;/td&gt;
&lt;td&gt;UI looks the same across app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Devs move faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility&lt;/td&gt;
&lt;td&gt;Built-in ally support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brand Identity&lt;/td&gt;
&lt;td&gt;Product feels cohesive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🧑‍💻 Hooks = Where Logic Lives
&lt;/h2&gt;

&lt;p&gt;Business logic doesn’t belong inside components. &lt;strong&gt;Custom hooks&lt;/strong&gt; make code &lt;strong&gt;reusable, testable, and clean&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// useProduct.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
   &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;fetchProducts&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ UI stays declarative.&lt;br&gt;
✅ Logic stays modular.&lt;/p&gt;

&lt;p&gt;📊 &lt;strong&gt;Diagram: Components vs Hooks&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Component] ---&amp;gt; UI (render)
       |
       v
 [Custom Hook] ---&amp;gt; Data Fetching, State, Business Logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🧼 Clean Code = Scalable Code
&lt;/h2&gt;

&lt;p&gt;Big tech engineers follow &lt;strong&gt;clean code principles&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SRP&lt;/strong&gt; → Single Responsibility Principle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KISS&lt;/strong&gt; → Keep It Simple, Stupid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DRY&lt;/strong&gt; → Don’t Repeat Yourself&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;Button variant="danger" color="red" bg="red" /&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;DeleteButton /&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;✅ Abstraction = Fewer Bugs + Cleaner Reviews&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Real-Time Code Reviews + Linting
&lt;/h2&gt;

&lt;p&gt;At big companies, &lt;strong&gt;automation + reviews are mandatory&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Code Reviews&lt;/strong&gt; → shared knowledge&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Pre-Commit Hooks (Husky)&lt;/strong&gt; → catch errors early&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;ESLint + Prettier&lt;/strong&gt; → consistent style&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CI/CD Pipeline Visual
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Developer Pushes Code] 
       |
       v
[Pre-Commit Checks: Lint + Tests]
       |
       v
[Pull Request Review]
       |
       v
[CI/CD: Build + Deploy + Monitor]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔐 Security &amp;amp; ♿ Accessibility First
&lt;/h2&gt;

&lt;p&gt;Security isn’t optional. Accessibility isn’t either.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Prevent &lt;strong&gt;XSS&lt;/strong&gt; and &lt;strong&gt;CSRF&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Proper &lt;strong&gt;token handling&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Input validation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Accessibility Checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Keyboard navigation works&lt;/li&gt;
&lt;li&gt;Screen reader support&lt;/li&gt;
&lt;li&gt;High color contrast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Use tools like &lt;code&gt;axe-core&lt;/code&gt;, &lt;code&gt;eslint-plugin-jsx-ally&lt;/code&gt;, &lt;code&gt;lighthouse&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Automation + Monitoring
&lt;/h2&gt;

&lt;p&gt;Big teams don’t just ship code — they ship &lt;strong&gt;pipelines + monitoring&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;CI/CD Pipelines&lt;/strong&gt; (GitHub Actions, Vercel, CircleCI)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Bundle Size Alerts&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Error Tracking&lt;/strong&gt; (Sentry, Datadog)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Performance Monitoring&lt;/strong&gt; (Lighthouse CI)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📊 Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Commit → Build → Test → Deploy → Monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📊 Documentation &amp;amp; Knowledge Sharing
&lt;/h2&gt;

&lt;p&gt;What separates side projects from &lt;strong&gt;real companies&lt;/strong&gt;?&lt;br&gt;
👉 &lt;strong&gt;Documentation.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Good README.md = contributor-friendly.&lt;/li&gt;
&lt;li&gt;Internal wikis = no lost knowledge.&lt;/li&gt;
&lt;li&gt;ADRs (Architecture Decision Records) = explain &lt;strong&gt;why&lt;/strong&gt; choices were made.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🧠 Culture = The Invisible Architecture
&lt;/h2&gt;

&lt;p&gt;Finally — even the best code structure won’t save a bad culture. Big companies win because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PRs = conversations, not fights.&lt;/li&gt;
&lt;li&gt;Automation is embraced, not skipped.&lt;/li&gt;
&lt;li&gt;Docs + testing = part of “done”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end of the day, &lt;strong&gt;how your team codes together&lt;/strong&gt; matters more than folder names.&lt;/p&gt;


&lt;h2&gt;
  
  
  🚦 Startup vs Big Tech Codebases
&lt;/h2&gt;

&lt;p&gt;Here’s how things usually look in a scrappy startup vs how big tech organizes for scale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────┐           ┌─────────────────────────┐
│   Startup Style    │           │     Big Tech Style      │
├────────────────────┤           ├─────────────────────────┤
│ 📂 components/     │           │ 📂 features/auth/       │
│ 📂 hooks/          │           │   ├── components/       │
│ 📂 services/       │           │   ├── hooks/            │
│ App.jsx            │           │   └── services/         │
│                    │           │ 📂 shared/ui/           │
│ Easy to start ✅   │           │ 📂 shared/utils/        │
│ Chaos later ❌      │           │ Scales across teams ✅  │
└────────────────────┘           └─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  📊 Side-by-Side Table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Startup-Style&lt;/th&gt;
&lt;th&gt;Big Tech-Style&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Folder Structure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Flat, all components in one folder&lt;/td&gt;
&lt;td&gt;Feature-based, modular&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Custom each time&lt;/td&gt;
&lt;td&gt;Design system + UI library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inside components&lt;/td&gt;
&lt;td&gt;Extracted into hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code Quality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quick fixes, less abstraction&lt;/td&gt;
&lt;td&gt;Clean code, reusable patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reviews&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ad-hoc, sometimes skipped&lt;/td&gt;
&lt;td&gt;Mandatory PRs + linting + tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Later concern&lt;/td&gt;
&lt;td&gt;Built-in from day one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;README only&lt;/td&gt;
&lt;td&gt;Wikis + ADRs + onboarding guides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Culture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;“Move fast, break things”&lt;/td&gt;
&lt;td&gt;“Move fast, don’t break prod”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🎯 Final Takeaway
&lt;/h2&gt;

&lt;p&gt;Big companies scale not because their engineers are magical unicorns 🦄, but because they follow &lt;strong&gt;battle-tested practices&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organize code by &lt;strong&gt;features&lt;/strong&gt;, not file types.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;design systems&lt;/strong&gt; for UI.&lt;/li&gt;
&lt;li&gt;Extract logic into &lt;strong&gt;hooks&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Follow &lt;strong&gt;clean code principles&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Automate reviews + pipelines.&lt;/li&gt;
&lt;li&gt;Prioritize &lt;strong&gt;security &amp;amp; accessibility&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Write &lt;strong&gt;documentation&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Build a &lt;strong&gt;team culture&lt;/strong&gt; around consistency.&lt;/li&gt;
&lt;li&gt;Startups often optimize for &lt;strong&gt;speed&lt;/strong&gt;, but end up with technical debt.&lt;/li&gt;
&lt;li&gt;Big Tech optimizes for &lt;strong&gt;scalability&lt;/strong&gt;, making onboarding, reviews, and growth smooth.&lt;/li&gt;
&lt;li&gt;The sweet spot? Start with startup speed, but gradually &lt;strong&gt;adopt Big Tech practices&lt;/strong&gt; before chaos hits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Happy Hacking 👋
&lt;/h2&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
