<?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: Erik Hanchett</title>
    <description>The latest articles on DEV Community by Erik Hanchett (@erikch).</description>
    <link>https://dev.to/erikch</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%2F1994%2F0j84YwMs.jpeg</url>
      <title>DEV Community: Erik Hanchett</title>
      <link>https://dev.to/erikch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erikch"/>
    <language>en</language>
    <item>
      <title>Nuxt vs SvelteKit: What works better?</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Wed, 22 Jul 2026 01:13:24 +0000</pubDate>
      <link>https://dev.to/erikch/nuxt-vs-sveltekit-what-works-better-132h</link>
      <guid>https://dev.to/erikch/nuxt-vs-sveltekit-what-works-better-132h</guid>
      <description>&lt;p&gt;Nuxt vs SvelteKit. Which one is better?&lt;/p&gt;

&lt;p&gt;That is is what I've been testing out this week. I built the same app twice. Once in Nuxt with the version 5 compatibility preview turned on, and once in SvelteKit using its experimental remote functions.&lt;/p&gt;

&lt;p&gt;I created a basic task app. Where you can add or remove tasks. I also logged the network requests and timing. The biggest thing I noticed was that the SvelteKit app would make one request, when creating a new task, while the Nuxt app needed two.&lt;/p&gt;

&lt;p&gt;That one difference turned out to be the most interesting part of the whole comparison, so let's jump in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Both apps are a simple task list backed by an in browser memory store. Both are production builds running locally, and both render the initial task list on the server.&lt;/p&gt;

&lt;p&gt;Quick caveat, Nuxt 5 is not released yet. My Nuxt app is stable Nuxt 4.5 with the compatibility flag set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// nuxt.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;compatibilityDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;future&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;compatibilityVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And SvelteKit's remote functions are still marked experimental in the docs. So this is a comparison of directions, not finished products.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;When I add a task in the Nuxt app, I get a POST to &lt;code&gt;/api/tasks&lt;/code&gt; (about 690 ms) followed by a GET to &lt;code&gt;/api/tasks&lt;/code&gt; (about 450 ms) to refresh the list. A little over 1,100 ms total, and the timeline panel reports two browser requests.&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%2Fbv3bxswcuz5iy702h5ns.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%2Fbv3bxswcuz5iy702h5ns.png" alt="Nuxt request" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I add a task in the SvelteKit app, I get one request. About 1,100 ms. The server still does both operations, the mutation and the read, but they come back in a single response.&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%2Fur0jeepdlow1acyb8w7d.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%2Fur0jeepdlow1acyb8w7d.png" alt="Svelte Request" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Plain refreshes were nearly identical. 447 ms in Nuxt, 448 ms in SvelteKit. I ran this quite a few times, and if I had to pick, SvelteKit felt slightly faster overall. But the totals were close enough that I wouldn't choose a framework based on them.&lt;/p&gt;

&lt;p&gt;Let's talk about how requests work in each. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Nuxt version: explicit API routes
&lt;/h2&gt;

&lt;p&gt;If you've used Nuxt before, this will feel familiar. I have two handlers in &lt;code&gt;server/api&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/api/tasks.get.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineEventHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readTasks&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/task-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&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;readTasks&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The POST handler validates the title and saves the task. These are normal HTTP endpoints. Anything that speaks HTTP can call them.&lt;/p&gt;

&lt;p&gt;On the page, &lt;code&gt;useFetch&lt;/code&gt; loads the initial data during SSR, so hydration doesn't fetch it again. When I add a task, I post with &lt;code&gt;$fetch&lt;/code&gt; and then call &lt;code&gt;refresh()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refresh&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;useFetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TaskSnapshot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task-dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitTask&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;refresh&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 code is explicit, and the network tab matches the code exactly. POST first, GET second.&lt;/p&gt;

&lt;p&gt;Could I avoid the second request? Sure. The POST could return the updated list and I could patch local state myself. I wrote it this way because invalidate-and-refetch is the workflow most of us reach for, and it's exactly the pattern SvelteKit's remote functions are designed to improve. I also find by adding a GET request we are verifying the exact output after the POST.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SvelteKit version: remote functions
&lt;/h2&gt;

&lt;p&gt;This is the experimental feature. You turn it on in &lt;code&gt;svelte.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;kit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;remoteFunctions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;compilerOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you create a file ending in &lt;code&gt;.remote.ts&lt;/code&gt; and export your server functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tasks.remote.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;command&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;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$app/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addTask&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;addTaskToStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readTasks&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$lib/server/task-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;valibot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;taskInput&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="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;requestId&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="nf"&gt;pipe&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="nf"&gt;string&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="nf"&gt;trim&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="nf"&gt;minLength&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="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="na"&gt;title&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="nf"&gt;pipe&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="nf"&gt;string&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="nf"&gt;trim&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="nf"&gt;minLength&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="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getTasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;readTasks&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;requestId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addTaskToStore&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="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// This runs on the server, and the refreshed query value&lt;/span&gt;
  &lt;span class="c1"&gt;// comes back in the same command response.&lt;/span&gt;
  &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The function bodies always run on the server. In the browser, they become typed wrappers around endpoints SvelteKit generates for you. I really enjoy how there is no public endpoint to hit, it's isolated. It's created for the call, which means I can use environment variables and secrets in there without thinking about it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;void getTasks().refresh()&lt;/code&gt; line is the API on the server side to trigger the refresh. After the mutation, SvelteKit refreshes the query on the server and packages the new value into the command's response. That's the single-flight mutation, and it's why the network tab shows one request instead of two.&lt;/p&gt;

&lt;p&gt;On the page, I just import and call the functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getTasks&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./tasks.remote&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitTask&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;SubmitEvent&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="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;render&lt;/span&gt; &lt;span class="nf"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;await tasks&lt;/code&gt; at the bottom works almost like a subscription. When the server-side refresh happens, the task list updates automatically. No server routes to worry about.&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%2F24q7zjeyi2ru8skt8l6t.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%2F24q7zjeyi2ru8skt8l6t.png" alt="remotefunctions" width="799" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My first look at this pattern, I thought it was a little complicated. But it clicked pretty fast, and the query-refreshes-inside-the-command idea makes sense once you see it in the network tab. If you've used server actions in Next or TanStack Start, this will feel like family. I'd say I still like TanStack Start's server actions a bit better, but this is close.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up&lt;/strong&gt;: remote functions have been available since SvelteKit 2.27 and they are still experimental. The API has changed several times over the past few months. If you adopt them early, pin your versions and budget time for migrations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What does Nuxt 5 offer?
&lt;/h2&gt;

&lt;p&gt;Nuxt 5 doesn't have an answer to remote functions. Most of the confirmed work is in the underlying structure. A new version of Nitro, a new Vite integration, and framework internals. The &lt;a href="https://nuxt.com/docs/getting-started/upgrade" rel="noopener noreferrer"&gt;upgrade guide&lt;/a&gt; walks through what to expect, and it's mostly foundation work rather than new application-level APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  My verdict
&lt;/h2&gt;

&lt;p&gt;I'm sticking with Nuxt. I love the API routes pattern, I love Vue, and nothing in this demo is a reason to rewrite an existing app. You can return updated data from a mutation today and skip the second request yourself if it matters.&lt;/p&gt;

&lt;p&gt;But I do miss server actions. Frameworks like SvelteKit, Next, and TanStack Start all have some version of a typed, non-public server function you can call from a component. I hope Nuxt adds something like it in a future update, beyond the server components they have now.&lt;/p&gt;

&lt;p&gt;If you're starting a SvelteKit project and can live with an experimental API, try remote functions first. The single-flight mutation is awesome, and the types crossing the boundary for free is a great developer experience.&lt;/p&gt;

&lt;p&gt;Which side are you on: explicit API routes, or remote functions that generate the transport for you? Let me know in the comments if you agree or disagree. &lt;/p&gt;

&lt;p&gt;BTW, I used &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; for all my research for this post and video! Check it out , it's an amazing harness!&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>sveltekit</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Used Spec-Driven Development to Build a Game (TanStack Start)</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 14 Jul 2026 22:26:53 +0000</pubDate>
      <link>https://dev.to/erikch/how-i-used-spec-driven-development-to-build-a-game-a5p</link>
      <guid>https://dev.to/erikch/how-i-used-spec-driven-development-to-build-a-game-a5p</guid>
      <description>&lt;p&gt;I've been deep in spec-driven development (SDD) lately, and I want to walk through what it actually is, then show you how I used it to build a &lt;a href="http://programwitherik.com/kirogame" rel="noopener noreferrer"&gt;game&lt;/a&gt; for &lt;a href="https://kiro.dev/birthday/" rel="noopener noreferrer"&gt;Kiro's&lt;/a&gt; first birthday.&lt;/p&gt;

&lt;p&gt;If you haven't run into spec-driven development, or SDD, don't worry, I got you. We'll cover what it is, where it helps, where it's overkill, and how I used it to make this game!&lt;/p&gt;

&lt;p&gt;Would you like to watch a video instead? Check it out!&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What is spec-driven development?
&lt;/h2&gt;

&lt;p&gt;SDD breaks down into three phases, and all of this happens before any code gets written.&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%2Fz890nfmi2bpt62xr5et5.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%2Fz890nfmi2bpt62xr5et5.png" alt="SDD phases" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First you create a requirements document. Then you have your AI editor turn that into a design document. Then, from the requirements and the design, you generate a list of tasks. &lt;/p&gt;

&lt;p&gt;The most important part is that there's a human in the middle at every step. You check the requirements. You check the design. You review and edit the tasks that get created. Nothing gets built until you've signed off on the plan.&lt;/p&gt;

&lt;p&gt;This isn't set in stone, though. Sometimes the requirements and design get combined into a single document. Other times you run it in a quick mode where all three happen at once. It depends on what you're trying to do. When I work on big features I create all three documents, when I'm trying to do something quick, like a demo, I might just use some sort of quick or plan mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs
&lt;/h2&gt;

&lt;p&gt;Like anything else, SDD comes with trade-offs.&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%2Ffke4na5ou12lwuyk4vmw.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%2Ffke4na5ou12lwuyk4vmw.png" alt="SDD Tradeoffs" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The thing I like most is the high first-pass success rate. If you actually do your job up front, writing a real requirements doc and a real design, the end product is a lot more likely to be what you wanted.&lt;/p&gt;

&lt;p&gt;It's also easier to review. You're breaking a big, complex feature into smaller steps, so you can check each one instead, and push up multiple smaller PRs. And it helps prevent you from going in the wrong direction. Instead of letting the agent run off in a bunch of directions and finding out way too late, you're checking the code after every step or two. &lt;/p&gt;

&lt;p&gt;The downside is the up front planning time. SDD takes more time before you see any code, so it's not meant for every task. For throwaway code or a small issue, it's not needed. On the other hand any features that takes more thought it does really well. You give up a little time at the start, and in return you get a more stable, better working app at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project: a Kiro birthday game
&lt;/h2&gt;

&lt;p&gt;Let me show you what I built.&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%2F9l71pzxpg2iwaxuy63sd.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%2F9l71pzxpg2iwaxuy63sd.png" alt="Game image" width="800" height="647"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a &lt;a href="http://programwitherik.com/kirogame" rel="noopener noreferrer"&gt;rogue-like game&lt;/a&gt; where you walk around and explore a bunch of buildings. Each building represents a different point in time for Kiro, because this is part of our Kiro birthday celebration. Kiro is turning one. So one building covers when Kiro went generally available, another is the house we had at re:Invent, and so on. Visit all the houses and you unlock a bonus house/section. I thought it would be a fun way to tell Kiro's history.&lt;/p&gt;

&lt;p&gt;Under the hood it's Phaser and TanStack Start. Phaser is the gaming library doing the 2D Final Fantasy style look, and TanStack Start is the web library wrapping it all. There's music in there too, which I'll get to.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I used SDD to build it
&lt;/h2&gt;

&lt;p&gt;I opened up Kiro and told it to use spec mode. You can do this right in the IDE, and as of v3 of the CLI you can use spec mode there as well. Either way, it creates those three documents: requirements, design, and tasks.&lt;/p&gt;

&lt;p&gt;I gave it a prompt describing the game. Something like, I'm creating this 2D video game, Final Fantasy style, here are the different classes and names, here are the different buildings we're going to have. From that it generated the requirements in EARS format, which is the classic structure of a user story plus acceptance criteria. As I mentioned earlier, I double checked it at every stage to make sure the docs it created were correct.&lt;/p&gt;

&lt;p&gt;From the requirements it built a design. You can skip this part if you'd rather keep everything in one doc, but I like having it. The design got into the technical details. Phaser as the TypeScript library for the front end, TanStack Start with useEffect all over the place, the high-level structure, the scenes, the boundaries, the game shell. &lt;/p&gt;

&lt;p&gt;Then came the task list, with every single task broken out. I told it to build an MVP first, so it took the full list and pulled the four most important tasks to the front. I went in and made sure that ordering made sense, had it implement those first four, looked at the output, and confirmed it held up before moving on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools that made it easier
&lt;/h2&gt;

&lt;p&gt;A few things helped a lot here.&lt;/p&gt;

&lt;p&gt;I used a couple of MCP servers. Context7 to grab the Phaser documentation, so the agent wasn't guessing at the API. And the Chrome browser MCP extension, so Kiro could open the app, look at it, and make sure it matched the requirements.&lt;/p&gt;

&lt;p&gt;I also ran auto mode. I bounced between models, and while Claude 4.8 is great, it does chew through a lot of credits. Auto mode did really well, especially paired with SDD. That's one of the underrated parts of this approach. When everything is already written down in the spec, you don't need the greatest state-of-the-art model to get what you need. Auto mode was more than enough for most of this.&lt;/p&gt;

&lt;p&gt;On top of that I used some skills. The &lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;AWS Agent Toolkit&lt;/a&gt;, plus the Amplify and serverless skills. I ran &lt;code&gt;/aws-amplify&lt;/code&gt; and it helped me get the game hosted on Amplify hosting. For the music I used ElevenLabs. I had a scripts folder wired up to a bunch of services, PixelLab for art, ElevenLabs for audio, even Hugging Face. All of it driven from inside Kiro, with a &lt;code&gt;.env&lt;/code&gt; file holding the keys, and it generated the assets for me too.&lt;/p&gt;

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

&lt;p&gt;That's the basic version of my spec-driven development workflow.  It's more planning up front, but for a real feature it pays off.&lt;/p&gt;

&lt;p&gt;If you want to try the game, check it &lt;a href="http://programwitherik.com/kirogame" rel="noopener noreferrer"&gt;out&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Do you use spec-driven development in your day-to-day? I'm curious what your experience has been. Take care, and thanks for reading.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>kiro</category>
      <category>programming</category>
    </item>
    <item>
      <title>TanStack Start vs Nuxt: One Framework to rule them all?</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 07 Jul 2026 21:38:55 +0000</pubDate>
      <link>https://dev.to/erikch/tanstack-vs-nuxt-one-framework-to-rule-them-all-4acl</link>
      <guid>https://dev.to/erikch/tanstack-vs-nuxt-one-framework-to-rule-them-all-4acl</guid>
      <description>&lt;p&gt;I love Nuxt and I really like TanStack Start. But which one is better? Or are they about the same?&lt;/p&gt;

&lt;p&gt;And if they are about the same, does it do anything my Nuxt setup can't, and is that worth leaving Vue for React? So I decided to build the same app in both frameworks and take a look. Read on below to find out!&lt;/p&gt;

&lt;p&gt;If you'd rather watch a video, check out the video on the same topic!&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The app
&lt;/h2&gt;

&lt;p&gt;In both frameworks I built a small GitHub user lookup app. You type a username, the profile gets fetched on the server, and the username lands in the URL as a &lt;code&gt;?user=&lt;/code&gt; query param so the result is shareable. Type &lt;code&gt;ErikCH&lt;/code&gt;, hit enter, and the card renders. Refresh the page and it's still there. It has the same behaviour so the difference lies in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difference one: server functions vs server routes
&lt;/h2&gt;

&lt;p&gt;On the Nuxt side we call a server route from &lt;code&gt;useAsyncData&lt;/code&gt;. Server routes are the more idiomatic way to use Nuxt to call things on the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/pages/index.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GithubUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~~/server/api/github.get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;definePageMeta&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&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="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;route&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="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&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;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&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;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;useAsyncData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;$fetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;watch&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;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&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;trim&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="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;props&lt;/code&gt; option on &lt;code&gt;definePageMeta&lt;/code&gt; maps the query into a typed page prop and re-runs on client navigation. &lt;code&gt;useAsyncData&lt;/code&gt; fetches when there's a username and refetches whenever it changes. The conditional that returns &lt;code&gt;Promise.resolve(null)&lt;/code&gt; skips the request on an empty query param, (or when you first load).&lt;/p&gt;

&lt;p&gt;The server route does the outbound call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/api/github.get.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;querySchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing ?user=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getValidatedQuery&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;querySchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&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;$fetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.github.com/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getValidatedQuery&lt;/code&gt; runs the schema and hands back a typed, validated &lt;code&gt;user&lt;/code&gt;. A bad shape, like an array or an empty string, gets a 400 before the handler runs.&lt;/p&gt;

&lt;p&gt;TanStack Start takes a different route. There's no separate API endpoint. You write a server function and call it directly from the loader.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/routes/index.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServerFn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getGithubUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServerFn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&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="nf"&gt;inputValidator&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://api.github.com/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`GitHub API error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&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="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;GithubUser&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One API covers reads and writes. This same &lt;code&gt;createServerFn&lt;/code&gt; handles a GET here, and it would handle a POST mutation the same way, called from a loader or straight from a component. The return type flows through to the caller with no manual annotation.&lt;/p&gt;

&lt;p&gt;The route wires it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;validateSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;loaderDeps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="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;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&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="na"&gt;error&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getGithubUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="na"&gt;error&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="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;validateSearch&lt;/code&gt; defines the query, &lt;code&gt;loaderDeps&lt;/code&gt; narrows it to just what the loader needs, and the loader runs on navigation, a lot like &lt;code&gt;script setup&lt;/code&gt; running when a Nuxt page loads.&lt;/p&gt;

&lt;p&gt;Between the two, I really like TanStack and it's server functions. I don't have to worry about another end point being exposed to the internet, and it's simple to use. Though both work for a lot of different scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server components in both
&lt;/h2&gt;

&lt;p&gt;Both frameworks also let you skip the client entirely with server components, and both still label the feature experimental.&lt;/p&gt;

&lt;p&gt;Nuxt calls them islands. You flip one flag in &lt;code&gt;nuxt.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;componentIslands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a component named with a &lt;code&gt;.server.vue&lt;/code&gt; suffix renders only on the server. It can call &lt;code&gt;useFetch&lt;/code&gt; directly, and no client-side JavaScript ships for it. Basically you embed the server component in a client component and that's it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/components/GithubUserCard.server.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GithubUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~~/server/api/github.get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="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;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;useFetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The card fetches on the server and streams its HTML into the page. When &lt;code&gt;?user=&lt;/code&gt; changes, Nuxt refetches the island in place. The GitHub response never lands in the browser bundle, which is nice to help reduce bundle size.&lt;/p&gt;

&lt;p&gt;TanStack Start added server components too, back in April. They're also experimental, and from the docs they look pretty different from the Next.js version people know, built around a readable-stream API. I tried it and found it more complicated than the Nuxt island, so for this app I stayed on the server function.&lt;/p&gt;

&lt;p&gt;Of everything here, the server function is still my favorite. Server routes earn their place when something outside your app needs to call in, like a webhook, or some sort of callback. For the rest, the function felt better and cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typed search params
&lt;/h2&gt;

&lt;p&gt;This is where TanStack Start made me a little jealous.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;validateSearch&lt;/code&gt; puts the query schema on the route itself, the &lt;code&gt;user&lt;/code&gt; param is typed everywhere it shows up. Not only in the component, but in navigation and links.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Typed against the route's search schema:&lt;/span&gt;
&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Link&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ErikCH&lt;/span&gt;&lt;span class="dl"&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;Look&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Link&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass a key that isn't in the schema and you get a red squiggle before you save. Delete the target on a &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; and the editor lists the real routes, because the routes are typed as well. I could see me using this all over, and if you're using an agent to help code, this will help catch the errors even quicker.&lt;/p&gt;

&lt;p&gt;Nuxt gets you close, with two caveats. Turn on &lt;code&gt;experimental.typedPages&lt;/code&gt; and your &lt;code&gt;&amp;lt;NuxtLink&amp;gt;&lt;/code&gt; routes are typed, so that half matches. With the &lt;code&gt;props&lt;/code&gt; plus Zod pattern above, the &lt;code&gt;user&lt;/code&gt; value reaching your component is typed and validated too. What Nuxt doesn't give you yet is the query itself typed on the link. You can type the prop, but not the &lt;code&gt;?user=&lt;/code&gt; on a &lt;code&gt;&amp;lt;NuxtLink&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you are on the Nuxt team, does anyone know a workaround? Leave a comment? I'd love a typed-query-params story on the router in Nuxt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What they actually share
&lt;/h2&gt;

&lt;p&gt;Interesting enough, both of these frameworks share a lot. Both build on Vite, so both dev servers start are super fast. Both run on Nitro for the server layer, which adds a lot of flexibility when deploying to different places. Overall in that sense they are very similar.&lt;/p&gt;

&lt;p&gt;One practical thing to weigh. As of this recording TanStack Start is still a Release Candidate (RC), so expect a few rough edges and bugs. Nuxt is on 4.4.x, production-hardened, with a modules ecosystem for images, i18n, SEO, and a lot that TanStack Start doesn't have yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=DBLXThKhTsY" rel="noopener noreferrer"&gt;Watch the video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nuxt.com" rel="noopener noreferrer"&gt;Nuxt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tanstack.com/start/latest/docs" rel="noopener noreferrer"&gt;TanStack Start docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tanstack.com/blog/announcing-tanstack-start-v1" rel="noopener noreferrer"&gt;TanStack Start v1 Release Candidate announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tanstack.com/start/latest/docs/framework/react/guide/server-components" rel="noopener noreferrer"&gt;TanStack Start server components guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zod.dev" rel="noopener noreferrer"&gt;Zod&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vite.dev" rel="noopener noreferrer"&gt;Vite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nitro.build" rel="noopener noreferrer"&gt;Nitro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The verdict
&lt;/h2&gt;

&lt;p&gt;So, should a Nuxt developer switch? For me, no, at least not today. I'm staying on Nuxt. I like Vue, the framework is mature, and TanStack features are nice, but not enough for me to move over (specially since it's still React).&lt;/p&gt;

&lt;p&gt;If you're already a React developer, or starting something new in React, TanStack Start is genuinely fun and I'd reach for it. React fans will feel at home in TanStack Start, Vue fans in Nuxt. I'll keep an eye on TanStack Start and pull it out for the odd project every now and then.&lt;/p&gt;

&lt;p&gt;One last note. I did all the research for the video and this post in &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;, an agent that works on the IDE, CLI and Web. &lt;/p&gt;

&lt;p&gt;If you've run both, tell me which way you'd go in the comments.&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>tanstack</category>
      <category>vue</category>
      <category>react</category>
    </item>
    <item>
      <title>Loop Engineering: Do Frontend and Fullstack Devs Actually Need It?</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 30 Jun 2026 17:59:18 +0000</pubDate>
      <link>https://dev.to/erikch/loop-engineering-do-frontend-and-fullstack-devs-actually-need-it-48eb</link>
      <guid>https://dev.to/erikch/loop-engineering-do-frontend-and-fullstack-devs-actually-need-it-48eb</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I keep hearing the term loop engineering. It's all over my feed, every AI newsletter, half the dev videos I open. So I had the same question you probably have. Is this actually important, and do I need to learn it as a frontend or fullstack developer?&lt;/p&gt;

&lt;p&gt;I spent some time with it and made a video breaking down what it is and where it fits. This post is the written version, with a practical example you can copy.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What loop engineering actually is
&lt;/h2&gt;

&lt;p&gt;Here's how I think about it. You give your coding agent a goal, and it runs over itself again and again, almost recursively, until that goal is met.&lt;/p&gt;

&lt;p&gt;You can also frame it as four parts. You give the agent a trigger or a system prompt with a clear ending state. It acts on that trigger. It observes to check whether the work is actually done. If it hits the stop condition, it stops. If it doesn't, it goes around again.&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%2Ft007ezbk7lnn3ddi1srv.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%2Ft007ezbk7lnn3ddi1srv.png" alt="Anatomy of a loop" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's basically it. The idea is that you are defining a goal and  a stop condition, then letting the agent close the gap on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's the agentic loop you already know
&lt;/h2&gt;

&lt;p&gt;If you've built anything with agents, this should feel familiar. It's the classic agentic loop. You give an agent a set of tools, and it uses those tools with its own reasoning to reach an answer.&lt;/p&gt;

&lt;p&gt;Say I want to research the latest tech trends. I give the agent a tool to search Google and a tool to search Reddit. On the first pass it searches Google and pulls what it finds. That isn't enough, so it loops again and pulls from Reddit. Then it combines both and hands the answer back to me.&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%2Fjt9tocg85rfm4hubn6eu.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%2Fjt9tocg85rfm4hubn6eu.png" alt="Agentic Loop" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You've seen the same thing in a chatbot. Ask it for the weather and it might call a few tools, look at the context of your question, and come back with what you asked for. Loop engineering is that pattern, pointed at your actual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical example: fixing a PR on a loop
&lt;/h2&gt;

&lt;p&gt;Let me show you where this earns its place day to day.&lt;/p&gt;

&lt;p&gt;I had a pull request that I pushed up using &lt;a href="https://kiro.dev/web" rel="noopener noreferrer"&gt;Kiro Web&lt;/a&gt;. It connected straight to my GitHub repo, I gave it an action, and it opened the PR for me. You can do this with Claude Code, Cursor, and other agents too.&lt;/p&gt;

&lt;p&gt;Then I noticed some tests were failing in my CI/CD pipeline. My GitHub Actions were red. So I gave it one prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fix all issues on the PR. Keep going until it's all fixed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last sentence is the stop condition. Kiro went to work in the background, running again and again, fixing the failures and looping until everything passed. I didn't babysit it through each round.&lt;/p&gt;

&lt;p&gt;Tests are a great place to start with this, because the pass or fail signal gives the loop a clear way to check its own work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automations are the other big use case
&lt;/h2&gt;

&lt;p&gt;The other place loops shine is on a schedule. &lt;a href="https://addyosmani.com/blog/loop-engineering/" rel="noopener noreferrer"&gt;Addy Osmani&lt;/a&gt; wrote a great article on loop engineering where he points out that automations running on a schedule, doing discovery and triage on their own, are a strong fit for this.&lt;/p&gt;

&lt;p&gt;If you can find work that a cron job or a scheduled tool can kick off every day, that's a candidate. A couple of examples:&lt;/p&gt;

&lt;p&gt;At the end of every day, run all the tests, and if anything is broken, let the agent fix it. Or keep your documentation current. The loop checks the docs against the latest code and keeps going until they match.&lt;/p&gt;

&lt;p&gt;If you use Claude Code, there's a &lt;code&gt;/loop&lt;/code&gt; command and a &lt;code&gt;/goal&lt;/code&gt; command worth knowing. They let you set up in-session scheduled work, and &lt;code&gt;/goal&lt;/code&gt; lets you set a persistent, verifiable objective instead of a single one-off task. You can do the same thing in Kiro and other tools, you just have to be more explicit and write your own system prompts to drive it.&lt;/p&gt;

&lt;h2&gt;
  
  
  My take
&lt;/h2&gt;

&lt;p&gt;Now the part where I push back a little.&lt;/p&gt;

&lt;p&gt;Don't tear up your whole workflow to jam loops in everywhere. There are real spots where they make sense, especially when your agent supports them and you've found an automation worth setting up. But I still think spec-driven development and plain back-and-forth vibe coding will solve most of your problems day to day. Loops are a tool to reach for in the right moment, not a religion.&lt;/p&gt;

&lt;p&gt;If you want to go deeper, three people shaped how I think about this. Kent C. Dodds made an excellent video on what he's discovered and how he works this way. Theo did a great one too. And Addy Osmani's article on loop engineering is worth your time.&lt;/p&gt;

&lt;p&gt;So I'll ask you what I asked at the end of the video. On your next project, are you going to try loop engineering? Or is it just hype? Let me know.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=jCZKiHrUNT4" rel="noopener noreferrer"&gt;Kent C. Dodds on loop engineering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=iJVJwmCKW9o" rel="noopener noreferrer"&gt;Theo (t3.gg) on loops&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://addyosmani.com/blog/loop-engineering/" rel="noopener noreferrer"&gt;Addy Osmani — Loop Engineering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/web/" rel="noopener noreferrer"&gt;Kiro Web&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>3 Tools That Make AI Suck Less at Coding</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 22 Jun 2026 00:47:45 +0000</pubDate>
      <link>https://dev.to/erikch/3-tools-that-make-ai-suck-less-at-coding-4c3</link>
      <guid>https://dev.to/erikch/3-tools-that-make-ai-suck-less-at-coding-4c3</guid>
      <description>&lt;p&gt;I've completely updated my workflow, and I use AI coding agents for everything. Writing code, testing, and just helping me be more productive. However, AI isn't perfect and often after a long back-and-forth session I'll have working code, but it will have a few code smells. Duplicated code, dead-end code, lengthy functions are common. One way to fix that is to add all these problems to an AGENTS.md file and that works. But that's not always possible, depending on the codebase and team I'm working with.&lt;/p&gt;

&lt;p&gt;To that end, I went looking for tools that could help me detect and fix these issues. And so after some research I found three tools that could work almost as a pipeline. One finds these issues, one reviews the changes before they ever reach a pull request, and one helps me evaluate my agents. I use &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; for most of my day to day, so I wired all of these into that loop, but they run with whatever agent you already have.&lt;/p&gt;

&lt;p&gt;If you'd rather watch than read, here's the full walkthrough:&lt;/p&gt;

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

&lt;p&gt;This post covers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finding dead code and duplication with Fallow&lt;/li&gt;
&lt;li&gt;Reviewing the AI's diff with CodeRabbit before you open a PR&lt;/li&gt;
&lt;li&gt;Measuring whether the output improves with Kiln&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;Node.js 20 or newer, so you have &lt;code&gt;npx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A JavaScript or TypeScript project, ideally one with some AI-generated code in it&lt;/li&gt;
&lt;li&gt;A coding agent like &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;, Claude Code, or Cursor for the review-loop parts&lt;/li&gt;
&lt;li&gt;Git, for the diff-based workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Find the mess: Fallow
&lt;/h2&gt;

&lt;p&gt;The first problem I've seen often is that AI can generate a mess of code. It will duplicate a block, leave an orphaned file behind, and grow a function to a thousand lines without ever flagging it, because each individual edit looked reasonable in isolation. You need a tool that reads the whole codebase at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/fallow-rs/fallow" rel="noopener noreferrer"&gt;Fallow&lt;/a&gt; is the one I reached for. It's static codebase intelligence for JS and TS, and it works a lot like ESLint, except it's built around these common AI code issues. You can run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx fallow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a full report. The sections I care about most are dead code, duplication, and complexity. Dead code finds files that aren't reachable from any entry point, exports nothing imports, and dependencies you've stopped using. &lt;strong&gt;When I ran this on my vibe-coded chess app (see video) it found dozens of these problems&lt;/strong&gt;  Duplication shows you the exact line ranges that got copied between two places. Complexity scores each function on how branchy and how hard to test it is, then ranks your worst offenders so you know where to start.&lt;/p&gt;

&lt;p&gt;There's a fix command for most things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx fallow fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather have an agent fix it you can use a JSON output and the skill. Run it with &lt;code&gt;--json&lt;/code&gt; and you get structured findings an agent can read and act on. Fallow also ships an agent skill you can install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add fallow-rs/fallow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that's in place, I can tell &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; to build a feature and then run Fallow on its own work to clean up after itself. It catches the duplicate block it just wrote and removes it before I even see it. You can also drop &lt;code&gt;fallow audit&lt;/code&gt; into CI so it compares your branch against main and only flags what your change introduced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus: Knip
&lt;/h3&gt;

&lt;p&gt;If Fallow is more than you need, &lt;a href="https://knip.dev" rel="noopener noreferrer"&gt;Knip&lt;/a&gt; does the dead-code half on its own and has been around longer. It finds unused files, dependencies, and exports in JS and TS projects in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx knip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've found knip to be a little slower then Fallow. YMMV.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the diff: CodeRabbit
&lt;/h2&gt;

&lt;p&gt;Finding dead code is great, but what about reviews?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.coderabbit.ai/" rel="noopener noreferrer"&gt;CodeRabbit&lt;/a&gt; puts a review step back in. It's AI code review that runs on your diff, and the version I like runs locally from the CLI so it happens before the PR exists. Install it with one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://cli.coderabbit.ai/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point it at your uncommitted changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;coderabbit review &lt;span class="nt"&gt;--plain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads the diff and comes back with specific findings. It also hands you a fix prompt you can paste straight back into your agent, so the loop closes quickly.&lt;/p&gt;

&lt;p&gt;Jack Herrington made the point in his video that lower-end models get noticeably more useful once CodeRabbit is in the loop, because the review catches what the cheaper model missed. That makes sense to me. The agent writes, CodeRabbit reviews, the agent fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the improvement: Kiln
&lt;/h2&gt;

&lt;p&gt;The first two tools clean up individual changes. This last one answers a bigger question. When I tweak a prompt, swap a model, or add a skill, is my agent's output actually better, or does it just feel better because the last three runs went well?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Kiln-AI/Kiln" rel="noopener noreferrer"&gt;Kiln&lt;/a&gt; is the tool I use to stop guessing. It's built for creating and evaluating AI systems, so you set up datasets, run evals, and compare results across changes. &lt;/p&gt;

&lt;p&gt;For my use case, I connected it to some output a LLM was producing. I ran an eval on it to make sure it was as I expected. When I added the Fallow skill, I could iterate on the system prompt and strings. It's the same instinct behind writing tests for your code, pointed at the thing generating the code.&lt;/p&gt;

&lt;p&gt;This is the least flashy of the three and the one I'd skip first if you're in a hurry. But if you're serious about tuning a workflow you'll use every day, measuring beats vibes every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to pick
&lt;/h2&gt;

&lt;p&gt;You don't have to use all three, so here are some good guide lines for each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your agent leaves dead code and duplication everywhere, start with Fallow, or Knip if you want something smaller.&lt;/li&gt;
&lt;li&gt;If you need more code reviews, add CodeRabbit and let it review before you do.&lt;/li&gt;
&lt;li&gt;If you're tuning prompts, models, or skills and want proof you're improving, set up Kiln.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep in mind these tools don't replace knowing what good code looks like. They raise the floor on what your AI ships so the version that lands in your editor is closer to something you'd have written yourself. That's extremely important. AI made writing code fast, and it made reviewing code the bottleneck. These three move that bottleneck.&lt;/p&gt;

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

&lt;p&gt;AI compresses how long it takes to generate code and stretches how long it takes to trust it. Fallow and Knip find the mess it leaves behind, CodeRabbit reviews the change before it slips past you, and Kiln tells you whether any of your tuning is working. &lt;/p&gt;

&lt;p&gt;If you want to see these running inside a real agent loop, I walk through all of them in the &lt;a href="https://www.youtube.com/watch?v=NMJ_CfB11x0" rel="noopener noreferrer"&gt;companion video&lt;/a&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/fallow-rs/fallow" rel="noopener noreferrer"&gt;Fallow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://knip.dev" rel="noopener noreferrer"&gt;Knip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.coderabbit.ai/" rel="noopener noreferrer"&gt;CodeRabbit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Kiln-AI/Kiln" rel="noopener noreferrer"&gt;Kiln&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tooling</category>
    </item>
    <item>
      <title>The most popular AI coding skills right now</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 15 Jun 2026 17:04:07 +0000</pubDate>
      <link>https://dev.to/aws/the-most-popular-ai-coding-skills-right-now-4183</link>
      <guid>https://dev.to/aws/the-most-popular-ai-coding-skills-right-now-4183</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;It's crazy to me that some GitHub repos, that were just created in the last year, have more stars then some of the most popular programming frameworks out there. At the time of writing, &lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;Superpowers&lt;/a&gt;, is around 226,000, ahead of the Vue repo and way ahead of Next.js. A separate repo based on Andrej Karpathy's coding advice has 174,000. And they aren't even code. They're folders of markdown files that tell your AI coding agent how to write software better.&lt;/p&gt;

&lt;p&gt;In other words these are agent skills, and they've blown up over the past year into one of the most-starred new categories of developer tooling. The format started at Anthropic, a &lt;code&gt;SKILL.md&lt;/code&gt; file with a short description and a set of instructions, and now it works in Claude Code, &lt;a href="https://kiro.dev/" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; and all your other favorite coding agents.&lt;/p&gt;

&lt;p&gt;This post is a tour of some of the skills I find most interesting that people are using everyday. I'll be in Kiro for most of this, but these run in whatever agent you already have, with install paths that vary per agent.&lt;/p&gt;

&lt;p&gt;This post covers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What an agent skill actually is&lt;/li&gt;
&lt;li&gt;The big workflow skills (Superpowers, Karpathy, Matt Pocock, GStack, GSD)&lt;/li&gt;
&lt;li&gt;Framework skills and the "ships with your package" model&lt;/li&gt;
&lt;li&gt;The Agent Toolkit for AWS&lt;/li&gt;
&lt;li&gt;How to pick one and try it safely&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you like, you can watch a &lt;a href="https://youtu.be/enWhBCyU36U?si=wySkthIQYr6592m5" rel="noopener noreferrer"&gt;full video&lt;/a&gt; on the subject instead! Also if nothing else, please check out the &lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt;! More information below. &lt;em&gt;Full disclosure, I'm a developer advocate for AWS&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an agent skill?
&lt;/h2&gt;

&lt;p&gt;A skill is a directory with a &lt;code&gt;SKILL.md&lt;/code&gt; file inside it. The file has a name, a one-line description, and instructions. These instructions include which steps to follow, which APIs to call, which mistakes to avoid and how to check the result.&lt;/p&gt;

&lt;p&gt;Skills should only run when needed. Your agent reads the short description, decides if the skill is relevant to what you asked, and only then loads the full instructions. Reference files load on demand after that. So you can have fifty skills installed and your context window stays clean, because the agent pulls in the two it needs and ignores the rest. &lt;em&gt;In practice, I've found I still need to manually tell my agent to run the skills by using the &lt;code&gt;/&lt;/code&gt; command and the skill name. YMMV&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The big workflow skills
&lt;/h2&gt;

&lt;p&gt;Most of the popular general-purpose skills are trying to fix the problem agents often have, going in the wrong direction. Agents when left alone often rush in, touch too many files, and they skip planning. These five take different approaches to slowing it down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Superpowers (obra/superpowers)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/obra/superpowers" rel="noopener noreferrer"&gt;Superpowers&lt;/a&gt; is the most-starred of the bunch and the most opinionated. It is a full development methodology that triggers automatically. It really reminds me of a super powered Spec-Driven Development (SDD) workflow. &lt;/p&gt;

&lt;p&gt;The moment the agent sees you are building something, it stops and asks what you are actually trying to do. It works a spec out of the conversation, shows it to you (optionally in a web browser), and only after you sign off does it write an implementation plan. Then it hands the plan to subagents that build and review each other's work. Be aware, this can eat up a lot of tokens.&lt;/p&gt;

&lt;p&gt;It is available through the official Claude plugin marketplace, plus Codex, Cursor, Gemini CLI, and a handful of others. You can also get it working with Kiro by from &lt;a href="https://www.skills.sh/obra/superpowers/brainstorming" rel="noopener noreferrer"&gt;Skills.sh&lt;/a&gt;. Though, I'd probably skip it if you're using Kiro, since most of the SDD is already there in Kiro. &lt;/p&gt;

&lt;p&gt;If you want one system that owns the whole loop from idea to reviewed code, this is the one to reach for. In Claude Code, install it from the official plugin marketplace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin install superpowers@claude-plugins-official
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Karpathy guidelines (multica-ai/andrej-karpathy-skills)
&lt;/h3&gt;

&lt;p&gt;This one is almost the opposite. It is a short file of behavioral rules, now at &lt;a href="https://github.com/multica-ai/andrej-karpathy-skills" rel="noopener noreferrer"&gt;174,000 stars&lt;/a&gt;. Andrej Karpathy did not write it, despite the name, which I find really funny. It is a community-built file derived from a January post where Karpathy listed the ways LLM coding agents tend to fail, and the repo later moved to the multica-ai org.&lt;/p&gt;

&lt;p&gt;The rules come down to four simple ideas. Think before coding, keep it simple, make surgical changes, and stay focused on the goal. If Superpowers feels like too much process for you, this is the low-commitment starting point. It is a single &lt;code&gt;CLAUDE.md&lt;/code&gt; (or AGENTS.md, or you can use it as a Skill), so the simplest install is to drop it into your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; CLAUDE.md https://raw.githubusercontent.com/multica-ai/andrej-karpathy-skills/main/CLAUDE.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Personally, I'm not sure if this is worth it in my own testing. However, it's easy enough to add it to your own &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt; so give it a shot though and make your own opinion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Matt Pocock's skills (mattpocock/skills)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/mattpocock/skills" rel="noopener noreferrer"&gt;Matt Pocock's collection&lt;/a&gt; sits in the middle and takes a deliberate stance against the all-in-one systems. In fact, he's often said he doesn't like SDD, and thinks it's too cumbersome. His framing is that approaches which own your entire process also take away your control and make bugs in that process hard to fix. So his skills are small, composable, and easy to adapt. You install the ones you want with the Vercel &lt;code&gt;skills&lt;/code&gt; CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills@latest add mattpocock/skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;His most popular one, and the one I like the most, is the &lt;code&gt;grill-me&lt;/code&gt;, which makes the agent interview you about what you are building before it writes anything. There is a more powerful version, &lt;code&gt;grill-with-docs&lt;/code&gt;, that does the same interview and also builds a shared-language document for your project. The repo's pitch is that this shared-language document pays off across sessions. There is also &lt;code&gt;tdd&lt;/code&gt; for a red-green-refactor loop, &lt;code&gt;diagnose&lt;/code&gt; for debugging, and &lt;code&gt;improve-codebase-architecture&lt;/code&gt; for rescuing a project that has turned into spaghetti.&lt;/p&gt;

&lt;p&gt;Another skill called &lt;code&gt;caveman&lt;/code&gt; helps reduce token costs. I find the &lt;code&gt;caveman&lt;/code&gt; and &lt;code&gt;grill-me&lt;/code&gt; skills the most useful. Though, I find &lt;code&gt;grill-me&lt;/code&gt; a little annoying after a while, when I just want to get something done. &lt;/p&gt;

&lt;h3&gt;
  
  
  GStack (garrytan/gstack)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/garrytan/gstack" rel="noopener noreferrer"&gt;GStack&lt;/a&gt; is Garry Tan's Claude Code setup, and at nearly 110,000 stars it is one of the most popular skill collections out there. Instead of one mode that handles everything, it gives the agent distinct roles, each behind its own slash command. It has product vision, designer, engineering manager, release manager, doc engineer, QA, and post-launch retrospective. It is the same genre as Superpowers, a full workflow, but organized around the roles on a software team rather than a sequence of phases. If you think in terms of "who would handle this part," GStack maps to that mental model nicely. It installs with a clone and a setup script (pass &lt;code&gt;--host kiro&lt;/code&gt;, &lt;code&gt;--host cursor&lt;/code&gt;, and so on for other agents):&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 &lt;span class="nt"&gt;--single-branch&lt;/span&gt; &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/.claude/skills/gstack &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also works with Kiro nicely.&lt;/p&gt;

&lt;h3&gt;
  
  
  GSD Core (open-gsd/gsd-core)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/open-gsd/gsd-core" rel="noopener noreferrer"&gt;GSD Core&lt;/a&gt; (Git. Ship. Done.) is a spec-driven system that pitches itself as a fix for context rot, the slow drop in quality as your context window fills up during a long session. It breaks work into atomic plans that run in fresh context windows, keeping the main session light while subagents do the heavy lifting. It targets more than a dozen agents. The project moved from the old &lt;code&gt;gsd-build&lt;/code&gt; org to its current home at &lt;code&gt;open-gsd/gsd-core&lt;/code&gt;, and the installer prompts for your runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @opengsd/gsd-core@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While using this, I found that it burned through tokens fast. It also takes over your whole workflow, that I disliked. Still, it's worth trying out to see if it works well for your own personal projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework skills and the "ships with your package" model
&lt;/h2&gt;

&lt;p&gt;The skills above are framework-agnostic. The more interesting development for frontend folks is skills that know your specific stack.&lt;/p&gt;

&lt;p&gt;The Vue community moved early here. &lt;a href="https://github.com/vuejs-ai/skills" rel="noopener noreferrer"&gt;vuejs-ai/skills&lt;/a&gt; is a community Vue 3 skills project (the README notes the author may propose transferring it to the Vue org), and its &lt;code&gt;vue-best-practices&lt;/code&gt; skill is a structured workflow covering the Composition API, reactivity, and the patterns you want a model to follow. &lt;a href="https://github.com/antfu/skills" rel="noopener noreferrer"&gt;Anthony Fu's collection&lt;/a&gt; covers Vue, Vite, Nuxt, Pinia, VueUse, and Vitest. Both install through the same CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add vuejs-ai/skills
npx skills add antfu/skills &lt;span class="nt"&gt;--skill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The newer idea is shipping skills inside the package itself. Three things are worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/skilld-dev/skilld" rel="noopener noreferrer"&gt;skilld&lt;/a&gt; generates version-aware skills from the npm dependencies you already have, pulling from docs, release notes, and GitHub issues. It solves the problem where a model's training cutoff means it writes last year's version of your library.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tanstack.com/intent" rel="noopener noreferrer"&gt;TanStack Intent&lt;/a&gt; (&lt;code&gt;@tanstack/intent&lt;/code&gt;) flips that around for maintainers. It lets library authors generate and ship &lt;code&gt;SKILL.md&lt;/code&gt; files inside their npm package, versioned with each release, so the skill updates when the library does. It launched recently and ties straight into the TanStack ecosystem.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/vercel-labs/skills" rel="noopener noreferrer"&gt;Vercel skills CLI&lt;/a&gt; is the &lt;code&gt;npx skills&lt;/code&gt; tool that installs any of this across 70-plus agents. If you use Kiro, it loads skills from &lt;code&gt;.kiro/skills/&lt;/code&gt; automatically with no config, which makes it an easy way to try things on camera or in a real project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is each of those three:&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;# Generate version-aware skills from your installed npm dependencies&lt;/span&gt;
npx &lt;span class="nt"&gt;-y&lt;/span&gt; skilld

&lt;span class="c"&gt;# Library maintainers: scaffold SKILL.md files inside your own package&lt;/span&gt;
npx @tanstack/intent scaffold

&lt;span class="c"&gt;# The universal installer used throughout this post — pattern is `npx skills add &amp;lt;owner/repo&amp;gt;`&lt;/span&gt;
npx skills add vercel-labs/skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Agent Toolkit for AWS
&lt;/h2&gt;

&lt;p&gt;AWS ships skills too, in the same open format, so they run in Kiro, Claude Code, Cursor, and Codex.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.aws.amazon.com/agent-toolkit/latest/userguide/skills.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt; was announced in May 2026 and is the officially supported way to bring AWS expertise into your agent. It pairs plugins for Claude Code and Codex with a curated collection of skills and the &lt;a href="https://aws.amazon.com/blogs/aws/the-aws-mcp-server-is-now-generally-available/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS MCP Server&lt;/a&gt;, which reached general availability on May 6, 2026. The toolkit covers areas like deployment, serverless, and database work. Setup is per-agent. In Kiro you add the AWS MCP Server through your MCP config and then pull in skills with &lt;code&gt;npx skills&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install AWS-curated skills via the universal CLI&lt;/span&gt;
npx skills add aws/agent-toolkit-for-aws/skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are building on AWS,  you should download this. Even if you are not using AWS I would download it for the future. Because with this the agent loads service-specific guidance instead of guessing at an API it half-remembers from training.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to pick one and try it safely
&lt;/h2&gt;

&lt;p&gt;Personally, I would use Matt Pococks skills and try out one of the other larger skill workflows like Superpowers. See if it works for you. Then pair it with a framework specific skills as well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last thing, and it matters. A skill is instructions you are dropping into your agent's context, so treat it the way you treat an npm dependency. Read the &lt;code&gt;SKILL.md&lt;/code&gt; before you install it. A repo with 100,000 stars is popular, but stars are a popularity signal, not a quality review. Skim the file, see what it tells your agent to do, and keep the ones that match how you actually work.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Agent skills went from a quiet Anthropic format to repos with more stars than the frameworks we build on, and it makes sense why. They take the patterns a good engineer carries in their head and hand them to the model at the start of every task. The big workflow skills give you structure while the framework skills give you correctness, and the AWS plugins give you service-specific knowledge you would otherwise paste in by hand.&lt;/p&gt;

&lt;p&gt;My advice: install one, read what it actually tells the agent, and see if the output matches how you'd have built it yourself. That's the real test, and it's the one only you can run. If you want to see these in action, I walk through them in the companion video.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>aws</category>
      <category>programming</category>
    </item>
    <item>
      <title>TanStack Start Is Kind of a Big Deal</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:06:43 +0000</pubDate>
      <link>https://dev.to/erikch/tanstack-start-is-kind-of-a-big-deal-4nec</link>
      <guid>https://dev.to/erikch/tanstack-start-is-kind-of-a-big-deal-4nec</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;People keep telling me TanStack Start is kind of a big deal, and I wanted to know if that holds up or not. I've been spending a lot of time at conferences lately, and TanStack Start comes up quite often in conversation. The community is split if server components is the right answer. TanStack Start has gone the opposite direction with it's clean client-side first components approach, with lot so ways to call server-side code, even in the same component.&lt;/p&gt;

&lt;p&gt;I'm a Vue and Nuxt person most days, so I'm not here to dunk on anyone's framework. What I want to figure out is simpler: are there specific things TanStack Start does that Next.js and Nuxt don't, and are they good enough to switch for?&lt;/p&gt;

&lt;p&gt;After some research I have come up with three things I really like about TanStack Start. These things alone aren't probably enough for me to switch, but I'm getting close.&lt;/p&gt;

&lt;p&gt;If you'd like to watch a video instead, check this out!&lt;/p&gt;

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

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

&lt;ul&gt;
&lt;li&gt;Node.js 22+&lt;/li&gt;
&lt;li&gt;Comfort with React and TypeScript&lt;/li&gt;
&lt;li&gt;You do not need any Next.js or TanStack experience&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;A GitHub user lookup. You type a username, the app fetches that user from the GitHub API on the server, and renders their profile. It's a perfect app to show these three features.&lt;/p&gt;

&lt;p&gt;You can find the full code in the &lt;a href="https://github.com/ErikCH/tanstack-start-nextjs-killer-demo" rel="noopener noreferrer"&gt;demo repo&lt;/a&gt;. Let's start by creating the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create the app
&lt;/h2&gt;

&lt;p&gt;TanStack has a CLI, so scaffolding is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @tanstack/cli@latest create my-app &lt;span class="nt"&gt;--framework&lt;/span&gt; React
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It asks about a package manager and a few add-ons, then sets up a project on Vite with file-based routing. Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;my-app
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dev server was ready in under a second on my machine. That Vite-powered startup is really nice, and it's the same Vite speed I covered in my earlier Vite videos.&lt;/p&gt;

&lt;p&gt;The structure is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── routes/
│   ├── __root.tsx      # the document shell
│   └── index.tsx       # the home route
├── router.tsx          # router config
└── routeTree.gen.ts    # auto-generated, don't edit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the three features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature 1: One server function for reads and writes
&lt;/h2&gt;

&lt;p&gt;Let me be fair up front, Next.js can also call server code directly. Next has React Server Functions, and in mutation contexts those are Server Actions. You mark a function with &lt;code&gt;"use server"&lt;/code&gt; and call it from a component, no API route required. Nuxt has its own version with &lt;code&gt;server/api&lt;/code&gt; routes plus &lt;code&gt;useFetch&lt;/code&gt;, which gives you typed responses too. So "call a function on the server" is not unique.&lt;/p&gt;

&lt;p&gt;However, the difference is the constraint. Next.js Server Actions run as POST requests and are built for mutations. The Next docs themselves steer you to Server Components or Route Handlers for reading data. You &lt;em&gt;can&lt;/em&gt; call a Server Action from a client component to read, but it still goes over POST and isn't the idiomatic, cacheable GET path. &lt;/p&gt;

&lt;p&gt;TanStack Start doesn't split it this way. One primitive, &lt;code&gt;createServerFn&lt;/code&gt;, handles both a GET read and a POST mutation, and you call it the same way from anywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServerFn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GithubUser&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;login&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;avatar_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;html_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;public_repos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;followers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;following&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getGithubUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServerFn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&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="nf"&gt;inputValidator&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GithubUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.github.com/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/vnd.github+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// a token here stays on the server, never ships to the client:&lt;/span&gt;
        &lt;span class="c1"&gt;// Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`User "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" not found`&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&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="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;GithubUser&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;.handler&lt;/code&gt; runs only on the server, so a token never reaches the browser. I set &lt;code&gt;method: 'GET'&lt;/code&gt; because this is a read, and I call it straight from my route loader like a normal async function. There is no route handler, or RSC boundary to think about and no endpoint string to keep in sync. (This snippet is trimmed for clarity. The version in the repo adds &lt;code&gt;encodeURIComponent&lt;/code&gt; on the input and separate handling for 404 and 403 rate-limit responses.)&lt;/p&gt;

&lt;p&gt;You can watch this happen in the browser too. Open the network tab, run a lookup, and you won't see a request to &lt;code&gt;api.github.com&lt;/code&gt; anywhere. The only call is the one to my own server. The GitHub fetch is happening server-side, which is exactly where I want it, and any token isn't leaked.&lt;/p&gt;

&lt;p&gt;I really like how types work here. I annotate the handler's return as &lt;code&gt;GithubUser&lt;/code&gt; once, and that type flows through the loader and into the component without me re-typing it at each call, and that holds whether it's a GET or a POST. Rename a field in the interface and every call that still reads the old property lights up red. (One caveat, that's compile-time propagation, not runtime validation. I cast the GitHub response with &lt;code&gt;as GithubUser&lt;/code&gt;, so if you want to prove the external JSON actually matches, you'd add a runtime schema check.) You can get there with a route handler too, with shared types or a schema validator. The difference is that TanStack infers the chain for you by default instead of asking you to wire it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature 2: Search params that are actually typed
&lt;/h2&gt;

&lt;p&gt;This is the one where TanStack genuinely has an advantage as of today. Next.js gives you generic string and string-array search params, not route-local schema validation. &lt;code&gt;useSearchParams&lt;/code&gt; hands you a read-only &lt;code&gt;URLSearchParams&lt;/code&gt;, and while &lt;code&gt;typedRoutes&lt;/code&gt; plus the &lt;code&gt;PageProps&lt;/code&gt; helper have improved path, navigation, and page-prop typing, none of that validates or transforms the &lt;em&gt;values&lt;/em&gt; inside the query string the way TanStack Router does. You can get there in Next with Zod, &lt;code&gt;nuqs&lt;/code&gt;, or &lt;code&gt;next-typesafe-url&lt;/code&gt;, but it's something you add on. Nuxt can validate a route with &lt;code&gt;definePageMeta&lt;/code&gt;'s &lt;code&gt;validate&lt;/code&gt; function, which can return &lt;code&gt;false&lt;/code&gt; or a &lt;code&gt;Partial&amp;lt;NuxtError&amp;gt;&lt;/code&gt; to reject a route, but it doesn't turn &lt;code&gt;route.query&lt;/code&gt; into a typed, validated query object for your component.&lt;/p&gt;

&lt;p&gt;TanStack Router treats search params as validated route state out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;validateSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;loaderDeps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="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;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&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="na"&gt;error&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getGithubUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="na"&gt;error&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="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I validate &lt;code&gt;?user=&lt;/code&gt; once. After that, &lt;code&gt;Route.useSearch()&lt;/code&gt; gives me &lt;code&gt;{ user: string }&lt;/code&gt;, fully typed, anywhere in the component. The loader reads that param and runs the server function, so loading the page with &lt;code&gt;?user=ErikCH&lt;/code&gt; in the URL loads the profile directly, with no extra client wiring. The lookup is shareable and survives a refresh, and I never wrote client state to make that happen. You can plug in Zod if you want richer schemas. &lt;/p&gt;

&lt;h2&gt;
  
  
  Feature 3: Type safety that runs end to end by default
&lt;/h2&gt;

&lt;p&gt;Typed navigation by itself isn't unique, and I want to be straight about that. Next.js has &lt;code&gt;typedRoutes&lt;/code&gt; for statically typed links, and Nuxt has typed navigation built in through &lt;code&gt;experimental.typedPages&lt;/code&gt;, plus the &lt;code&gt;nuxt-typed-router&lt;/code&gt; module for more. So all three can stop you from typoing a route.&lt;/p&gt;

&lt;p&gt;The difference is how far the chain reaches and how much setup it takes. Next's &lt;code&gt;typedRoutes&lt;/code&gt; types the path, not the search param values. Nuxt's typed pages are opt-in and cover routes and params. In TanStack it's on by default, and the same type system covers your route params, your search params, and your loader data in one connected chain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNavigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;lookup&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;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FormEvent&lt;/span&gt;&lt;span class="p"&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="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// typed: { user: string }&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I pass a search param that doesn't exist, or the wrong type, the type checker flags it. Vite itself transpiles TypeScript without type checking, so this is &lt;code&gt;tsc --noEmit&lt;/code&gt; (I keep it in a &lt;code&gt;typecheck&lt;/code&gt; script and run it in CI) or your editor catching it inline. And because the loader's return type flows into &lt;code&gt;Route.useLoaderData()&lt;/code&gt;, the data I render is typed by the same chain that typed the navigation. That whole path, from the server function return through the loader, the search params, and the link, is one thing instead of three features you wire up separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding in AI
&lt;/h2&gt;

&lt;p&gt;I lean on AI coding assistants like Kiro for a lot of my code, and TanStack Start is new enough that the models don't have great knowledge on it. When I asked for a server function, I'd sometimes get an older API shape back, because the training data is behind.&lt;/p&gt;

&lt;p&gt;TanStack ships a fix for exactly this, and it's the last thing I showed in the video. There's a package called TanStack Intent that wires your coding agent into current TanStack patterns. You install it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @tanstack/intent@latest &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That creates or updates your agent config, defaulting to &lt;code&gt;AGENTS.md&lt;/code&gt; (it can target others like &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;.cursorrules&lt;/code&gt; too), with skill-loading instructions. Your agent reads it, sees which TanStack skills are available, and pulls the current docs for whatever it's working on instead of guessing from stale training data.&lt;/p&gt;

&lt;p&gt;So I opened Kiro CLI, which picks up that &lt;code&gt;AGENTS.md&lt;/code&gt; on its own, and gave it this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please review my existing repository against the newly loaded TanStack Intent rules. Check my implementation for anti-patterns, missing edge cases, or deprecated syntax.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It worked through the skills and came back with a list, a couple of &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; notes, some dev-tools setup for TanStack Start, a shell component thing. One last thing, I wasn't pinning the latest version tags in my &lt;code&gt;package.json&lt;/code&gt;. Though not really required all the time, I did like how it was looking at the &lt;code&gt;package.json&lt;/code&gt; file in general.&lt;/p&gt;

&lt;p&gt;The second guardrail is the type safety from earlier. When the AI guessed an old &lt;code&gt;createServerFn&lt;/code&gt; shape, &lt;code&gt;tsc&lt;/code&gt; and my editor flagged it right away. I didn't have to catch it in review. The types caught it for me.&lt;/p&gt;

&lt;p&gt;This is the same point I made in my Vue in the Age of AI video. AI writes more of our code now, so the frameworks that verify the AI's work for you are worth more than they used to be. &lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;Nothing to tear down for local development. Stop the dev server with Ctrl+C. If you deployed, TanStack Start uses Nitro under the hood, so you can remove whatever Node target you set up. Those hosting resources can incur charges, so tear them down if you were only testing.&lt;/p&gt;

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

&lt;p&gt;So is it kind of a big deal? I'd put it this way. If you want explicit control, fast Vite builds, and type safety that runs from the server function through search params to your links, TanStack Start is genuinely the most compelling React framework I've tried in a while. The server functions and typed search params alone are just really nice to have.&lt;/p&gt;

&lt;p&gt;It's not for everyone yet. The ecosystem is smaller than Next.js, there are fewer plugins and it's young. The TanStack CLI is still marked alpha, there are fewer production references to learn from, and the deployment and debugging knowledge isn't as standardized as Next.js. If you need the hiring pool and the deployment story Next.js has, that's a real reason to wait.&lt;/p&gt;

&lt;p&gt;But "the default React framework is finally in question" is true for the first time in years, and after building with it, I get why people are switching. If you're a Nuxt person like me, the typed search params and server functions will feel like the things you wish you had without reaching for extra modules.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tanstack.com/start/latest" rel="noopener noreferrer"&gt;TanStack Start docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tanstack.com/start/latest/docs/framework/react/start-vs-nextjs" rel="noopener noreferrer"&gt;TanStack Start vs Next.js (official)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tanstack.com/router/latest/docs/framework/react/how-to/validate-search-params" rel="noopener noreferrer"&gt;Search params validation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://inngest.com/blog/migrating-off-nextjs-tanstack-start" rel="noopener noreferrer"&gt;Inngest: Why we migrated off Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ErikCH/tanstack-start-nextjs-killer-demo" rel="noopener noreferrer"&gt;Demo repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tanstack</category>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Mastering the Latest TypeScript: What's New in 6.0 (and a Peek at 7)</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Thu, 04 Jun 2026 17:01:27 +0000</pubDate>
      <link>https://dev.to/erikch/mastering-the-latest-typescript-whats-new-in-60-and-a-peek-at-7-4m8o</link>
      <guid>https://dev.to/erikch/mastering-the-latest-typescript-whats-new-in-60-and-a-peek-at-7-4m8o</guid>
      <description>&lt;p&gt;TypeScript 6.0 has been out for a few months now, and it's a bit of an unusual release. It's the last compiler that will be written in TypeScript and JavaScript. Later this year, TypeScript 7 arrives with the compiler rewritten in Go. Microsoft has seen roughly 10x speed improvements on large codebases. That makes 6.0 the bridge release. &lt;/p&gt;

&lt;p&gt;The good news is that the features I want to show you aren't going anywhere. Everything here is forward compatible with TypeScript 7. They're also just genuinely nice to use. I'll walk through five additions worth adopting today, show the tsconfig.json changes that add them, and finish with what the Go rewrite means for you.&lt;/p&gt;

&lt;p&gt;If you'd rather watch than read, the full video is here.&lt;/p&gt;

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

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

&lt;ul&gt;
&lt;li&gt;Node.js 20 or newer&lt;/li&gt;
&lt;li&gt;TypeScript 6.0 (&lt;code&gt;npm install -D typescript@6&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A code editor with TypeScript support (I'm using Kiro)&lt;/li&gt;
&lt;li&gt;Basic familiarity with &lt;code&gt;tsconfig.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Native Temporal date and time types
&lt;/h2&gt;

&lt;p&gt;The built-in JavaScript &lt;code&gt;Date&lt;/code&gt; API isn't that great. Months are zero-indexed, time zone handling is painful, and most of us reach for an API library to help. TypeScript 6 ships type support for &lt;code&gt;Temporal&lt;/code&gt;, the TC39 date/time API, so you get a modern API with no extra dependency.&lt;/p&gt;

&lt;p&gt;Creating a date is readable now. You pass the year, month, and day as named fields instead of memorizing argument order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;launch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PlainDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;day&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="c1"&gt;// Arithmetic that reads the way you think about it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;twoWeeksLater&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;weeks&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aMonthEarlier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;months&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Time zones are really neat to use. You can format the same instant across several zones without juggling offsets by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zonedDateTimeISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;America/Los_Angeles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zones&lt;/span&gt; &lt;span class="o"&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;America/Los_Angeles&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;America/New_York&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;Europe/London&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;Asia/Tokyo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;zones&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withTimeZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And duration math, the thing that usually sends you to a library, is built in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plainDateISO&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PlainDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-09-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;until&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;largestUnit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;day&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;days&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; days until launch`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get all this without a date library, and you avoid the off-by-one month bugs that &lt;code&gt;Date&lt;/code&gt; gives you. This is the feature I expect people to adopt first.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Map.getOrInsert() and getOrInsertComputed()
&lt;/h2&gt;

&lt;p&gt;If you work with &lt;code&gt;Map&lt;/code&gt;, you know the check-then-set code you have to write. You want to insert a value only if the key isn't already there, so you write something like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&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="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cache&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&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="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// non-null assertion because get() can return undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things bother me there. The &lt;code&gt;if&lt;/code&gt; guard is boilerplate, and the &lt;code&gt;!&lt;/code&gt; non-null assertion exists only because &lt;code&gt;get()&lt;/code&gt; is typed to possibly return &lt;code&gt;undefined&lt;/code&gt;. TypeScript 6 adds &lt;code&gt;Map.getOrInsert()&lt;/code&gt;, which adds all of that into one call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&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="c1"&gt;// returns 1, inserts if missing&lt;/span&gt;
&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// key exists, so this is ignored&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cache&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// still 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It checks for the key, returns the existing value if present, and inserts only when the key is missing. You don't overwrite anything, and the guard and the assertion both go away.&lt;/p&gt;

&lt;p&gt;There's a companion method, &lt;code&gt;getOrInsertComputed()&lt;/code&gt;, that takes a factory function instead of a value. The function only runs when the key is actually missing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrInsertComputed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;computing a&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// logs "computing a", inserts 1&lt;/span&gt;

&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrInsertComputed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;this never runs&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="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="c1"&gt;// key already exists, so the factory is skipped entirely&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second factory never executes. If your value is expensive to produce, a database call, a heavy calculation, parsing something large, &lt;code&gt;getOrInsertComputed()&lt;/code&gt; skips the work entirely when the key already exists. &lt;code&gt;getOrInsert()&lt;/code&gt; evaluates its argument no matter what, so reach for the computed version when the value isn't cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. RegExp.escape() for safe dynamic regex
&lt;/h2&gt;

&lt;p&gt;Building a regular expression from user input is a classic problem that we all run into. Say you take a version string and try to match it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pattern&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;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;version 1.0 and build 1x0 are different&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// matches BOTH "1.0" and "1x0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That matches more than you wanted. The &lt;code&gt;.&lt;/code&gt; in the input is a regex wildcard, so it matches any character, including the &lt;code&gt;x&lt;/code&gt; in &lt;code&gt;1x0&lt;/code&gt;. TypeScript 6 adds &lt;code&gt;RegExp.escape()&lt;/code&gt;, which escapes special characters in a string so it's treated literally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pattern&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;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;version 1.0 and build 1x0 are different&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// matches only "1.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the dot is a literal dot, and you match exactly what you meant. Any time you build a regex from a value you didn't write yourself, wrap it in &lt;code&gt;RegExp.escape()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The &lt;code&gt;#&lt;/code&gt; subpath imports (and the tsconfig that powers them)
&lt;/h2&gt;

&lt;p&gt;Deep relative imports like &lt;code&gt;../../../utils/format&lt;/code&gt; are hard to read and break when you move files. The common fix is a path alias with &lt;code&gt;@&lt;/code&gt;, but that can collide with real npm packages, since plenty of scoped packages start with &lt;code&gt;@&lt;/code&gt;. TypeScript 6 leans into Node's subpath imports, which use a &lt;code&gt;#&lt;/code&gt; prefix that can't be confused with a package name.&lt;/p&gt;

&lt;p&gt;Setup happens in two files. First, &lt;code&gt;tsconfig.json&lt;/code&gt; needs a module resolution mode that understands subpath imports.&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;"compilerOptions"&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;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"esnext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bundler"&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;If you're not using a bundler, the Node-native equivalent works too.&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;"compilerOptions"&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;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nodenext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nodenext"&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 you declare the actual mapping in &lt;code&gt;package.json&lt;/code&gt; under &lt;code&gt;imports&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;"imports"&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;"#utils/*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/utils/*.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"#components/*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/components/*.js"&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;Now your imports are clean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;formatDate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#utils/format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#components/Button&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;Without one of those module resolution settings, the &lt;code&gt;#&lt;/code&gt; imports won't resolve, so the tsconfig change is the part that actually unlocks the feature. If you watched my earlier &lt;a href="https://youtu.be/WbIz38_54KM?si=699kmcW7B-72Tfk7" rel="noopener noreferrer"&gt;video&lt;/a&gt; on tsconfig paths, this is the modern, standardized version of that same idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Smarter generic inference
&lt;/h2&gt;

&lt;p&gt;This one removes annotations you used to have to write by hand. Take a generic interface like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CreatePair&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;Before TypeScript 6, you usually had to pin the generic explicitly when you used it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreatePair&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;consume&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript 6 infers &lt;code&gt;T&lt;/code&gt; from the value you provide, so the annotation becomes optional.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;        &lt;span class="c1"&gt;// T inferred as number&lt;/span&gt;
  &lt;span class="na"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the compiler reads &lt;code&gt;42&lt;/code&gt; and figures out the type, you write less and still get full type safety and autocomplete everywhere. It's a small change you'll feel constantly in real code.&lt;/p&gt;

&lt;h2&gt;
  
  
  tsconfig.json changes worth making now
&lt;/h2&gt;

&lt;p&gt;Beyond the subpath imports config above, TypeScript 6 shifts a few defaults in a direction that TypeScript 7 will push as well. A few minutes auditing your config today saves friction later.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strict&lt;/code&gt; now defaults to &lt;code&gt;true&lt;/code&gt;. If you've explicitly set &lt;code&gt;strict: false&lt;/code&gt;, you're working against where the language is heading. This is a good moment to enable it or write down why you're opting out.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;verbatimModuleSyntax: true&lt;/code&gt; is encouraged and replaces the older &lt;code&gt;importsNotUsedAsValues&lt;/code&gt;. It forces explicit &lt;code&gt;import type&lt;/code&gt; for type-only imports, which keeps your emitted output predictable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;baseUrl&lt;/code&gt; is no longer required for &lt;code&gt;paths&lt;/code&gt; to work. If you only kept &lt;code&gt;baseUrl&lt;/code&gt; around to enable path aliases, you can drop it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;moduleResolution: "node"&lt;/code&gt; is on its way out. If you're still on the legacy resolver, 6.0 is the time to move to &lt;code&gt;nodenext&lt;/code&gt; or &lt;code&gt;bundler&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's coming in TypeScript 7
&lt;/h2&gt;

&lt;p&gt;TypeScript 7 is the headline behind all of this. The compiler is being rewritten in Go (the project codename is Corsa), and the speed gains look impressive. The Go compiler parallelizes across CPU cores, so the bigger your codebase, the more you benefit.&lt;/p&gt;

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

&lt;p&gt;TypeScript 6 isn't a flashy release, and that's the point. Temporal, the new &lt;code&gt;Map&lt;/code&gt; methods, &lt;code&gt;RegExp.escape()&lt;/code&gt;, subpath imports, and smarter inference all make day-to-day code a little cleaner, and none of it gets in your way when you move to 7. The single most valuable thing you can do today is adopt these APIs and audit your tsconfig.json so the Go-compiler upgrade is a non-event.&lt;/p&gt;

&lt;p&gt;I also built an &lt;a href="https://github.com/ErikCH/typescript-6-whats-new" rel="noopener noreferrer"&gt;experimental skill&lt;/a&gt; that helps you upgrade older TypeScript projects to 6.0. If you use Kiro, Claude Code, or another agentic editor, give it a try on a TypeScript 5 project and let me know how it goes. The link is in the video description.&lt;/p&gt;

&lt;p&gt;Watch the full walkthrough on YouTube, and drop a comment if you've started using any of these in your own projects.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>node</category>
    </item>
    <item>
      <title>Your Default Vite Config Is Not Enough (New with Vite 8)</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 25 May 2026 17:14:13 +0000</pubDate>
      <link>https://dev.to/erikch/8-vite-config-options-every-developer-should-know-vite-8-22im</link>
      <guid>https://dev.to/erikch/8-vite-config-options-every-developer-should-know-vite-8-22im</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Last week I was at &lt;a href="https://vueconf.us/" rel="noopener noreferrer"&gt;Vueconf US&lt;/a&gt;. It's one of my favorite conferences and I try to go every year. This year Evan You gave a talk on the future of Vue and tooling. Halfway through he started talking about Vite and some of the new features of Vite 8 that just came out. I was amazed at how far Vite has come and the millions of downloads a month it's getting. What I found especially interesting was how few people really knew about these features. Evan asked the audience by a show of hands who had tried out &lt;a href="https://vite.dev/blog/announcing-vite8" rel="noopener noreferrer"&gt;Vite 8&lt;/a&gt;, and only a few people raised their hands! So when I got home I decided to write this blog post and create a video on some configurations you must try out with Vite, including some new features that just came out with Vite 8.&lt;/p&gt;

&lt;p&gt;If you haven't heard yet, Vite 8 shipped in March 2026 with the most significant architectural change since Vite 2. One of the most significant is the Rust-based bundler called Rolldown that delivers &lt;a href="https://vite.dev/blog/announcing-vite8" rel="noopener noreferrer"&gt;10-30x faster builds&lt;/a&gt;. If you've been using Vite with zero config, that's fine. The defaults are great. However, there are a handful of options that, once you know them, you'll reach for in every project.&lt;/p&gt;

&lt;p&gt;This post covers 8 config options that you should know. Two are new in Vite 8, and six have been around but you probably have not heard of them. All of them go in your &lt;code&gt;vite.config.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I put this post together using &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;, an AI-powered IDE. The &lt;code&gt;forwardConsole&lt;/code&gt; option in section 1 is something Kiro benefits from directly. &lt;/p&gt;

&lt;p&gt;I also made a video covering all of these with a live demo. Check it out if you prefer watching over reading:&lt;/p&gt;

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

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

&lt;ul&gt;
&lt;li&gt;Node.js 20.19+ or 22.12+ (required by Vite 8)&lt;/li&gt;
&lt;li&gt;A Vite project (any framework)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To upgrade to Vite 8:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;vite@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. server.forwardConsole
&lt;/h3&gt;

&lt;p&gt;New in Vite 8. When you enable this, browser console errors and warnings show up in your Vite terminal instead of only in DevTools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;forwardConsole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's useful when you're working with AI coding agents that can't see the browser, or when you just want to keep your eyes on the terminal instead of switching to DevTools. Vite auto-enables it when it detects an AI coding agent via &lt;a href="https://www.npmjs.com/package/@vercel/detect-agent" rel="noopener noreferrer"&gt;&lt;code&gt;@vercel/detect-agent&lt;/code&gt;&lt;/a&gt;. Otherwise it defaults to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can get more granular with it too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;forwardConsole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;unhandledErrors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;logLevels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output includes source-mapped stack traces, so you see the original TypeScript line numbers, not the compiled output.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. resolve.tsconfigPaths
&lt;/h3&gt;

&lt;p&gt;Also new in Vite 8. Before this you had to install the &lt;code&gt;vite-tsconfig-paths&lt;/code&gt; plugin to use TypeScript path aliases. Now it's built in and works in both the dev server and build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tsconfigPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One setup mistake I see a lot: make sure &lt;code&gt;paths&lt;/code&gt; is inside &lt;code&gt;compilerOptions&lt;/code&gt;, not at the top level of the JSON. Easy to get wrong when editing by hand:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wrong&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;paths&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;top&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;level,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ignored&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Vite&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;"files"&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;"paths"&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;"@/*"&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;"./src/*"&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;"references"&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;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tsconfig.app.json"&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;correct&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;paths&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;inside&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;compilerOptions&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;"files"&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;"references"&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;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tsconfig.app.json"&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;"compilerOptions"&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;"paths"&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;"@/*"&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;"./src/*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@components/*"&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;"./src/components/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a small performance cost, so it's opt-in rather than the default. The &lt;a href="https://www.typescriptlang.org/tsconfig/#paths" rel="noopener noreferrer"&gt;TypeScript team also notes&lt;/a&gt; that &lt;code&gt;paths&lt;/code&gt; is intended to inform TypeScript about mappings handled by other tools, not to change emit behavior. Still, I think it's well worth trying out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;resolve.alias vs resolve.tsconfigPaths: which should you use?&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;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;resolve.alias&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;resolve.tsconfigPaths&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Config location&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vite.config.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tsconfig.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works with plain JS&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single source of truth&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requires Vite 8&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (built-in)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance cost&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Small&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use &lt;code&gt;tsconfigPaths&lt;/code&gt; if you want one place to define aliases and you're already on TypeScript. Use &lt;code&gt;alias&lt;/code&gt; for plain JS projects, monorepos, or when you want aliases independent of TypeScript's config.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. server.proxy
&lt;/h3&gt;

&lt;p&gt;I don't see a lot of developers using this one. The proxy isn't really about fixing CORS. It's about making your dev environment behave like production.&lt;/p&gt;

&lt;p&gt;In production, your frontend and API are usually on the same domain. They might have the same origin, so CORS is not needed. For example, in dev, your frontend may be on &lt;code&gt;localhost:5173&lt;/code&gt; and your backend on &lt;code&gt;localhost:8080&lt;/code&gt;. If you have different ports, and different origins, tne you'll get a CORS error. The proxy closes helps fix this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;changeOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;api/&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="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;Any request to &lt;code&gt;/api/*&lt;/code&gt; gets forwarded to &lt;code&gt;http://localhost:8080/*&lt;/code&gt;. The browser only ever sees &lt;code&gt;localhost:5173&lt;/code&gt;, so no cross-origin request and no CORS error.&lt;/p&gt;

&lt;p&gt;A few other reasons to reach for it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Working against a teammate's service or a third-party API that hasn't added &lt;code&gt;localhost&lt;/code&gt; to their CORS config? You can't change their server, but you can proxy through Vite.&lt;/li&gt;
&lt;li&gt;Adding &lt;code&gt;Access-Control-Allow-Origin: localhost:5173&lt;/code&gt; to your backend is a dev concern leaking into prod config. The proxy keeps that out entirely.&lt;/li&gt;
&lt;li&gt;You can inject auth headers for external services in the proxy &lt;code&gt;configure&lt;/code&gt; callback, keeping them out of the browser bundle. This is dev-only though. For production, handle auth server-side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;changeOrigin: true&lt;/code&gt; makes the request appear to come from the target host, which some backends require. &lt;code&gt;rewrite&lt;/code&gt; strips the &lt;code&gt;/api&lt;/code&gt; prefix before forwarding.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. server.hmr.overlay
&lt;/h3&gt;

&lt;p&gt;By default, Vite shows a full-screen red overlay when there's a server error. Runtime errors, HMR failures, and transform errors all trigger it. If you find it disruptive while editing, you can turn it off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;hmr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;overlay&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Errors still appear in the terminal and browser console. Turn it off when you're doing UI-heavy work and the overlay keeps blocking the component you're editing. I've been turning the overlay off a lot when I work. It's been so much nicer. Leave it on if you like the overlay.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. resolve.alias
&lt;/h3&gt;

&lt;p&gt;While I prefer the &lt;code&gt;tsconfigpaths&lt;/code&gt;, sometimes you have to use &lt;code&gt;resolve.alias&lt;/code&gt; instead. It is the explicit way to set up import shortcuts without touching tsconfig at all. It's great for JS projects, monorepos, or when you want aliases that aren't tied to TypeScript's path resolution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;path.resolve&lt;/code&gt; (or &lt;code&gt;fileURLToPath&lt;/code&gt; with ESM) to get a proper absolute path. Passing a bare string like &lt;code&gt;'/src'&lt;/code&gt; resolves to the filesystem root on most systems, not your project root.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. server.open
&lt;/h3&gt;

&lt;p&gt;I've been really liking this option. Yes it's small but once you turn it on, you'll never turn it off (at least most of the time). Set &lt;code&gt;server.open: true&lt;/code&gt; and Vite opens your browser automatically when the dev server starts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also pass a path string to land on a specific route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pairs well with conditional config (see the Bonus section) so you can set &lt;code&gt;open&lt;/code&gt; only when running &lt;code&gt;vite dev&lt;/code&gt;, not during build.&lt;/p&gt;




&lt;h3&gt;
  
  
  7. build.sourcemap
&lt;/h3&gt;

&lt;p&gt;By default, Vite doesn't generate sourcemaps for production builds, so errors in production point at minified output. Turn this on and stack traces point at your actual source files. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FYI: Source maps are turned on for dev builds, so don't accidentaly turn this on by accident and leak your source code in production!&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;npm run build&lt;/code&gt; then &lt;code&gt;npm run preview&lt;/code&gt; (preview runs &lt;code&gt;vite preview&lt;/code&gt;). Open DevTools, go to Sources, and you'll see your actual &lt;code&gt;.vue&lt;/code&gt; and &lt;code&gt;.ts&lt;/code&gt; files. Click any line in a stack trace and it jumps to the original source. This is perfect for debugging and adding in break points.&lt;/p&gt;

&lt;p&gt;Three modes are available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;sourcemap&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="c1"&gt;// separate .map files alongside the bundle&lt;/span&gt;
  &lt;span class="nx"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// sourcemap embedded directly in the JS file&lt;/span&gt;
  &lt;span class="nx"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// .map files generated, no reference in the bundle&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;'hidden'&lt;/code&gt; is useful if you're uploading sourcemaps to an error monitoring service like &lt;a href="https://docs.sentry.io/platforms/javascript/sourcemaps/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;. They can decode your stack traces server-side without the browser ever loading the maps. One thing to know: &lt;code&gt;'hidden'&lt;/code&gt; only removes the &lt;code&gt;//# sourceMappingURL=&lt;/code&gt; comment from the bundle. The &lt;code&gt;.map&lt;/code&gt; files are still generated and will be deployed alongside your JS unless you explicitly exclude them. If you want them truly private, add a step to strip &lt;code&gt;*.map&lt;/code&gt; files before uploading. Otherwise your source maps might leak to production!&lt;/p&gt;




&lt;h3&gt;
  
  
  8. envPrefix
&lt;/h3&gt;

&lt;p&gt;Occasionally I look for this one, it's useful when I'm migrating code and the prefixs are different. By default, Vite only exposes env variables prefixed with &lt;code&gt;VITE_&lt;/code&gt; to your client code. Everything else in your &lt;code&gt;.env&lt;/code&gt; file stays server-side. &lt;code&gt;envPrefix&lt;/code&gt; lets you change that prefix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;envPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUBLIC_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this set, your &lt;code&gt;.env&lt;/code&gt; file might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;PUBLIC_API_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com&lt;/span&gt;
&lt;span class="py"&gt;PUBLIC_APP_NAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;My App&lt;/span&gt;
&lt;span class="py"&gt;SECRET_DB_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;supersecret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in your components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_API_URL&lt;/span&gt;   &lt;span class="c1"&gt;// "https://api.example.com"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_APP_NAME&lt;/span&gt;  &lt;span class="c1"&gt;// "My App"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SECRET_DB_PASSWORD&lt;/span&gt; &lt;span class="c1"&gt;// undefined, never sent to the browser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only variables matching your prefix make it into the bundle. Everything else is stripped at build time, even if it's in the same &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;If you're migrating from Create React App (&lt;code&gt;REACT_APP_&lt;/code&gt;) or Next.js (&lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;), you can match your existing naming convention instead of renaming every variable. You can also pass an array to expose multiple prefixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;envPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUBLIC_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;APP_&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;h3&gt;
  
  
  Bonus: conditional config and define
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;This bonus is more for the advanced users out there! If you are let me know in the comments!&lt;/em&gt; You can pass a function to &lt;code&gt;defineConfig&lt;/code&gt; instead of an object. It receives &lt;code&gt;command&lt;/code&gt; (&lt;code&gt;'serve'&lt;/code&gt; or &lt;code&gt;'build'&lt;/code&gt;) and &lt;code&gt;mode&lt;/code&gt; (&lt;code&gt;'development'&lt;/code&gt; or &lt;code&gt;'production'&lt;/code&gt;), so you can change config based on context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serve&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;define&lt;/code&gt; bakes global constants into the build at compile time. Values are inlined directly into the output, so there's no runtime cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;__APP_VERSION__&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;__BUILD_DATE__&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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;Add type declarations to &lt;code&gt;vite-env.d.ts&lt;/code&gt; so TypeScript knows about them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;__APP_VERSION__&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;__BUILD_DATE__&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;Most of these are config-only changes. A few touch other files: &lt;code&gt;tsconfigPaths&lt;/code&gt; requires &lt;code&gt;paths&lt;/code&gt; in your &lt;code&gt;tsconfig.json&lt;/code&gt;, &lt;code&gt;envPrefix&lt;/code&gt; pairs with a &lt;code&gt;.env&lt;/code&gt; file, and &lt;code&gt;define&lt;/code&gt; needs type declarations in &lt;code&gt;vite-env.d.ts&lt;/code&gt;. To revert any option, remove it from &lt;code&gt;vite.config.ts&lt;/code&gt; and Vite falls back to its defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why aren't my TypeScript path aliases working in Vite?&lt;/strong&gt;&lt;br&gt;
Make sure &lt;code&gt;paths&lt;/code&gt; is inside &lt;code&gt;compilerOptions&lt;/code&gt; in your &lt;code&gt;tsconfig.json&lt;/code&gt;, not at the top level. Vite 8's built-in &lt;code&gt;resolve.tsconfigPaths: true&lt;/code&gt; reads from &lt;code&gt;compilerOptions.paths&lt;/code&gt; only. A top-level &lt;code&gt;paths&lt;/code&gt; key is silently ignored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between resolve.alias and resolve.tsconfigPaths in Vite?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;resolve.alias&lt;/code&gt; is defined directly in &lt;code&gt;vite.config.ts&lt;/code&gt; and works for any project including plain JavaScript. &lt;code&gt;resolve.tsconfigPaths&lt;/code&gt; reads aliases from your &lt;code&gt;tsconfig.json&lt;/code&gt; and requires TypeScript. Use &lt;code&gt;tsconfigPaths&lt;/code&gt; if you want a single source of truth; use &lt;code&gt;alias&lt;/code&gt; for JS projects or monorepos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Vite's server.proxy fix CORS errors?&lt;/strong&gt;&lt;br&gt;
Yes, but the real purpose is making dev match production. The proxy routes requests through the Vite dev server so the browser never makes a cross-origin request. No CORS header needed on your backend. It's cleaner than adding &lt;code&gt;Access-Control-Allow-Origin: localhost:5173&lt;/code&gt; to your server config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are Vite production sourcemaps safe to deploy?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;sourcemap: 'hidden'&lt;/code&gt; generates &lt;code&gt;.map&lt;/code&gt; files without referencing them in the bundle, so browsers won't load them. You can upload them to Sentry or similar for private stack trace decoding. Note: the &lt;code&gt;.map&lt;/code&gt; files are still built and will be deployed unless you add a step to strip &lt;code&gt;*.map&lt;/code&gt; files before uploading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What env variables does Vite expose to the browser?&lt;/strong&gt;&lt;br&gt;
Only variables prefixed with &lt;code&gt;VITE_&lt;/code&gt; by default (or your custom &lt;code&gt;envPrefix&lt;/code&gt;). Everything else in &lt;code&gt;.env&lt;/code&gt; is stripped at build time and never sent to the browser. That includes secrets like database passwords or API keys that don't have the prefix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does server.forwardConsole work with all frameworks?&lt;/strong&gt;&lt;br&gt;
Yes, it's a Vite dev server feature, not framework-specific. It works with Vue, React, Svelte, and any other Vite-based setup. Vite auto-enables it when it detects an AI coding agent via &lt;code&gt;@vercel/detect-agent&lt;/code&gt;; otherwise it defaults to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;forwardConsole&lt;/code&gt; and &lt;code&gt;tsconfigPaths&lt;/code&gt; are both opt-in and default to &lt;code&gt;false&lt;/code&gt;. The others (&lt;code&gt;proxy&lt;/code&gt;, &lt;code&gt;hmr.overlay&lt;/code&gt;, &lt;code&gt;resolve.alias&lt;/code&gt;, &lt;code&gt;server.open&lt;/code&gt;, &lt;code&gt;build.sourcemap&lt;/code&gt;, &lt;code&gt;envPrefix&lt;/code&gt;) have been around for a while but are easy to miss if you've never needed them.&lt;/p&gt;

&lt;p&gt;The full demo project is available at &lt;a href="https://github.com/ErikCH/vite-config-tips" rel="noopener noreferrer"&gt;https://github.com/ErikCH/vite-config-tips&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Here is me speaking at VueConf this year! &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%2Fdmyk36uejpwdmjyeh2u9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdmyk36uejpwdmjyeh2u9.jpg" alt="Erik Speaking at VueConf" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by Erik Hanchett, AWS Developer Advocate. He covers frontend development, AWS, and AI/agents at &lt;a href="https://programwitherik.com" rel="noopener noreferrer"&gt;programwitherik.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vite</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>5 Things to Do After Installing OpenClaw (Before You Break It)</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 11 May 2026 18:51:25 +0000</pubDate>
      <link>https://dev.to/erikch/5-things-to-do-after-installing-openclaw-before-you-break-it-1153</link>
      <guid>https://dev.to/erikch/5-things-to-do-after-installing-openclaw-before-you-break-it-1153</guid>
      <description>&lt;p&gt;I've been running OpenClaw for a while now and I think it's pretty awesome.&lt;/p&gt;

&lt;p&gt;If you haven't heard of it, &lt;a href="https://openclaw.ai/" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt; is a self-hosted AI agent that runs on your own hardware: a spare laptop, a Mac Mini, or a VPS. At AWS we have &lt;a href="https://aws.amazon.com/lightsail/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Lightsail&lt;/a&gt; which is a great option for running OpenClaw on a dedicated instance. (Full disclosure: I'm a Developer Advocate for AWS, so I'll be recommending AWS services where relevant. Feel free to use whatever VPS provider you'd like.) It's not a chatbot you open in a browser tab. It's always on. It connects to your messaging apps (Telegram, Discord, WhatsApp), reads your files, runs scheduled tasks, and takes action on your behalf. By default, a heartbeat runs every 30 minutes, it checks for instructions in &lt;code&gt;HEARTBEAT.md&lt;/code&gt; and acts proactively. You can customize the interval or disable it entirely, but it's on out of the box.&lt;/p&gt;

&lt;p&gt;The whole thing is built around plain-text markdown files. Your agent's personality lives in &lt;code&gt;SOUL.md&lt;/code&gt;. Its long-term memory lives in &lt;code&gt;MEMORY.md&lt;/code&gt;. Its knowledge of you lives in &lt;code&gt;USER.md&lt;/code&gt;. These files are your agent. &lt;/p&gt;

&lt;p&gt;The default install is not entirely production ready. Your gateway might be exposed to the public internet. Your default model might be eating your API budget. And without a few key configuration steps, your agent has no idea who you are or what it's allowed to do.&lt;/p&gt;

&lt;p&gt;These are the five things that I would do after you install OpenClaw.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;FYI:&lt;/strong&gt; OpenClaw moves fast — updates ship 2–3 times a week and the UI changes regularly. The steps here are based on v2026.5.x. If something looks different in your version, check the &lt;a href="https://docs.openclaw.ai/" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; first. And as always, be careful giving any agent access to your files, accounts, and messaging apps. Start with read-only tasks and expand permissions as you build trust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Opening the Dashboard&lt;/li&gt;
&lt;li&gt;1. Lock Down Your Gateway&lt;/li&gt;
&lt;li&gt;2. Switch to a Cheaper Model&lt;/li&gt;
&lt;li&gt;3. Write Your SOUL.md With Boundaries&lt;/li&gt;
&lt;li&gt;4. Seed Your Memory Files&lt;/li&gt;
&lt;li&gt;5. Back Up Your Workspace and Use /new&lt;/li&gt;
&lt;li&gt;Resources&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;OpenClaw installed and running&lt;/li&gt;
&lt;li&gt;Access to your API provider dashboard (Anthropic, OpenAI, or similar)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Opening the Dashboard
&lt;/h2&gt;

&lt;p&gt;The dashboard runs at &lt;code&gt;http://127.0.0.1:18789&lt;/code&gt; by default. The easiest way to open it with your token already attached is to run this from the terminal on the machine where OpenClaw is installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens the dashboard in your browser with a clean URL. If it asks for a token, run this to get it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw config get gateway.auth.token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then paste it into the dashboard login prompt. See the &lt;a href="https://docs.openclaw.ai/web/dashboard" rel="noopener noreferrer"&gt;dashboard docs&lt;/a&gt; for more detail.&lt;/p&gt;

&lt;p&gt;If you've already logged in before, just go straight to &lt;code&gt;http://127.0.0.1:18789&lt;/code&gt;. Your browser should have the session cached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Lock Down Your Gateway
&lt;/h3&gt;

&lt;p&gt;Good news: the default install gets this right. Bind mode is &lt;code&gt;loopback&lt;/code&gt; and auth mode is &lt;code&gt;token&lt;/code&gt; out of the box. If you installed via the quick-start script, verify this. Some install paths skip token setup during onboarding.&lt;/p&gt;

&lt;p&gt;The reason this is still Step 1 is that it's easy to accidentally break, and most people never verify it. &lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;Settings → Infrastructure&lt;/strong&gt; and confirm these two settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gateway Bind Mode:&lt;/strong&gt; should be &lt;code&gt;loopback&lt;/code&gt; for a local setup. This controls who can reach the port at all — &lt;code&gt;loopback&lt;/code&gt; means only &lt;code&gt;127.0.0.1&lt;/code&gt;, so nothing outside the machine can connect regardless of auth settings. &lt;code&gt;lan&lt;/code&gt; binds to all network interfaces, which is fine on a home machine where your interfaces are private. On a VPS where the network interface has a public IP, &lt;code&gt;lan&lt;/code&gt; puts the port on the internet. That's where the real exposure happens. If you need remote access, use &lt;code&gt;tailnet&lt;/code&gt; (Tailscale) rather than &lt;code&gt;lan&lt;/code&gt; — it keeps the port off the public internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gateway Auth Mode:&lt;/strong&gt; should be &lt;code&gt;token&lt;/code&gt;. This controls what happens once someone reaches the port. If it's &lt;code&gt;none&lt;/code&gt;, anyone who can reach the port connects with no credentials at all. On &lt;code&gt;loopback&lt;/code&gt; that's low risk. On a public bind it's a disaster. There are thousands of OpenClaw instances exposed on the public internet right now, most of them are the result of changing bind mode without realizing what that exposes.&lt;/p&gt;

&lt;p&gt;While you're here, check two more things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Insecure Control UI Auth Toggle:&lt;/strong&gt; may be enabled from your initial setup. Turn it off. When on (&lt;code&gt;allowInsecureAuth: true&lt;/code&gt;), it lets localhost Control UI sessions bypass device identity checks in non-secure HTTP contexts, it's a compatibility workaround that shouldn't be left on in normal use. The security audit will flag it if it's enabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control UI Allowed Origins:&lt;/strong&gt; should list your specific origins (e.g., &lt;code&gt;http://localhost:18789&lt;/code&gt;). Never set this to &lt;code&gt;*&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To verify from the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw config get gateway.auth.mode
&lt;span class="c"&gt;# should print: token&lt;/span&gt;

openclaw config get gateway.bind
&lt;span class="c"&gt;# should print: loopback&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If either prints something different, fix it in the dashboard before moving on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run the security audit.&lt;/strong&gt; OpenClaw has a built-in security audit command. Run it now:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;For a more thorough check that includes live probes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw security audit &lt;span class="nt"&gt;--deep&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it flags issues, &lt;code&gt;--fix&lt;/code&gt; applies the safe defaults automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw security audit &lt;span class="nt"&gt;--fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then tell your agent to keep checking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Read https://docs.openclaw.ai/gateway/security, run &lt;span class="sb"&gt;`&lt;/span&gt;openclaw security audit &lt;span class="nt"&gt;--deep&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;,
and send me a summary of any findings. Then schedule yourself to repeat this twice a day.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pin your version.&lt;/strong&gt; OpenClaw ships updates 2–3 times a week and some of them break things. The community built a tracker: &lt;a href="https://isitstable.com/openclaw" rel="noopener noreferrer"&gt;isitstable.com/openclaw&lt;/a&gt;. Check it before you update, then install the recommended stable release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw update &lt;span class="nt"&gt;--tag&lt;/span&gt; &amp;lt;stable-version&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This handles the doctor check and gateway restart automatically. If you need to roll back to an older version, the manual fallback is &lt;code&gt;npm i -g openclaw@&amp;lt;version&amp;gt;&lt;/code&gt; followed by &lt;code&gt;openclaw doctor&lt;/code&gt; and &lt;code&gt;openclaw gateway restart&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you're ready to update, check the tracker, read the changelog, then update during a quiet period.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Switch to a Cheaper Model
&lt;/h3&gt;

&lt;p&gt;Someone in the community went from $47/week to $6/week with two changes. It's one of the most upvoted posts in &lt;a href="https://www.reddit.com/r/openclaw/comments/1rp8t9r/" rel="noopener noreferrer"&gt;r/openclaw&lt;/a&gt;. That's the whole story for this step.&lt;/p&gt;

&lt;p&gt;The default model on most OpenClaw installs is whatever was set during onboarding, often a top-tier model like GPT-5 or Opus. That's great for setup,  you want the best reasoning when configuring your workspace files and personality. For daily use it works fine too, but it gets expensive fast. Your agent is checking calendars, drafting short replies, and running heartbeat tasks every 30 minutes. None of that needs frontier-model reasoning, and you're paying frontier-model prices for all of it.&lt;/p&gt;

&lt;p&gt;Here's a breakdown of what the community recommends based on your budget and hardware:&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%2Fw5txfrh2203j754jbone.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%2Fw5txfrh2203j754jbone.png" alt="Description of different models to use" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want the best results and don't want to think about cost, Opus with Sonnet as fallback is the standard recommendation. Just be warned. It's expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cheapest option: use your ChatGPT Plus subscription.&lt;/strong&gt; If you're already paying $20/month for ChatGPT Plus, you can connect OpenClaw to it via OpenAI Codex OAuth. That means you're using your existing subscription instead of paying separately per token. OpenAI explicitly supports this for third-party tools like OpenClaw. To set it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw onboard &lt;span class="nt"&gt;--auth-choice&lt;/span&gt; openai-codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you're already past onboarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw models auth login &lt;span class="nt"&gt;--provider&lt;/span&gt; openai-codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens an OAuth flow to sign in with your ChatGPT account. Once connected, set your model to &lt;code&gt;openai/gpt-5.5&lt;/code&gt; and OpenClaw automatically uses the Codex subscription runtime. Usage limits apply. Check &lt;a href="https://openai.com/chatgpt/pricing/" rel="noopener noreferrer"&gt;OpenAI's plan comparison&lt;/a&gt; for current details on what Plus vs Pro covers.&lt;/p&gt;

&lt;p&gt;Note: Anthropic does allow Claude subscription auth (Claude Pro or Max) via Claude CLI reuse — run &lt;code&gt;openclaw models auth login --provider anthropic --method cli --set-default&lt;/code&gt;. For production use, an API key is more reliable.&lt;/p&gt;

&lt;p&gt;If you want to keep bills low without going local, &lt;strong&gt;Minimax M2.7 via API&lt;/strong&gt; is worth knowing about. It's not as capable as Opus or Sonnet but it's significantly cheaper and the community rates it as surprisingly good for most daily tasks.&lt;/p&gt;

&lt;p&gt;If your machine has 32GB+ RAM, local models become viable and can bring your API costs close to zero for routine work. A common setup: Opus or Sonnet for planning and complex reasoning, then a local model like &lt;strong&gt;Qwen Coder&lt;/strong&gt; (8B parameter, free to run) for the actual code generation. On Apple Silicon (Mac Mini, MacBook Pro) the unified memory means RAM alone is enough for good performance on smaller models. On a PC or Linux box, a dedicated GPU makes a big difference, without one, local models run on CPU and can be slow enough to be frustrating for interactive use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where to change it:&lt;/strong&gt; First, make sure your API key is set. The cleanest way is to add it to &lt;code&gt;~/.openclaw/.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.openclaw/.env&lt;/span&gt;
&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-...
&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenClaw picks this up automatically at startup. Make sure this file isn't committed to a public repo, it contains your credentials.&lt;/p&gt;

&lt;p&gt;Then go to &lt;strong&gt;Settings → AI &amp;amp; Agents → Models tab&lt;/strong&gt; and edit the Model JSON field directly:&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;"primary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anthropic/claude-sonnet-4-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anthropic/claude-haiku-4-5"&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;The fallback kicks in if the primary is rate-limited or unavailable. Hit &lt;strong&gt;Save → Apply&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The SOUL.md trick:&lt;/strong&gt; The second change that made the $47→$6 difference was adding one line to SOUL.md:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Only use a more powerful model when I explicitly ask for deep analysis.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets the agent self-route. Routine tasks stay cheap. You ask for "deep analysis" when you actually need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your costs:&lt;/strong&gt; Go to &lt;strong&gt;Control → Usage&lt;/strong&gt; in the sidebar. It shows total tokens, cost, and a breakdown by model. Check it daily in your first week until you understand your baseline. Keep an eye on the cache hit rate, a high rate means you're not paying full price for repeated context, since cached tokens cost significantly less than fresh input tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Write Your SOUL.md With Boundaries
&lt;/h3&gt;

&lt;p&gt;SOUL.md is the character sheet for your agent. It defines personality, voice, temperament, and what it's allowed to do. Most people fill it in during onboarding with vague answers and never touch it again. That's a mistake.&lt;/p&gt;

&lt;p&gt;The file lives in your OpenClaw workspace alongside a handful of other markdown files: &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;MEMORY.md&lt;/code&gt;, &lt;code&gt;USER.md&lt;/code&gt;, &lt;code&gt;IDENTITY.md&lt;/code&gt;. Together these plain-text files are your agent. If you ever migrate to new hardware, these files are what you bring with you.&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%2F9tw412gn8o1gmpti76ck.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%2F9tw412gn8o1gmpti76ck.png" alt="OpenClaw workspace files" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;Agent&lt;/strong&gt; in the sidebar and click the &lt;strong&gt;SOUL&lt;/strong&gt; button, under &lt;strong&gt;Files&lt;/strong&gt;. The default template is already there and it's actually pretty good. What it doesn't have is your specific boundaries. Add these at the bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Identity&lt;/span&gt;
You are [name]. You are a collaborator, not an assistant.
Push back when I'm wrong. Challenge ideas during brainstorms.
When I'm heads-down, keep it crisp.

&lt;span class="gu"&gt;## Defaults&lt;/span&gt;
Only use a more powerful model when I explicitly ask for deep analysis.
For routine tasks, use the cheapest capable model.

&lt;span class="gu"&gt;## Boundaries&lt;/span&gt;
Never delete files without explicit confirmation.
Never send messages on my behalf without showing me the draft first.
Never make purchases or API calls that cost money without asking.
If a task fails twice with the same error, stop and report. Do not retry.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last section is easy to skip. Without explicit boundaries, your agent will make reasonable-sounding decisions that you didn't actually authorize.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The aggressive note-taker trick:&lt;/strong&gt; Add this to both SOUL.md and AGENTS.md:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an aggressive note-taker. After every significant session,
write key decisions, preferences, and facts into your daily log.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SOUL.md gets it so the behavior is part of the agent's core identity. AGENTS.md gets it so it's enforced as an operating rule. Together they make it stick. Without both, the agent may follow the instruction in some sessions but not others. This is the single biggest thing you can do to prevent memory degradation over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Seed Your Memory Files
&lt;/h3&gt;

&lt;p&gt;The biggest complaint from people who've been using OpenClaw for a few weeks is that it starts forgetting things. The experience gets less magical and more frustrating as you keep re-explaining context it should already know.&lt;/p&gt;

&lt;p&gt;This is a known problem in the community and it's not fully solved. But it's manageable if you set things up right from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seed MEMORY.md before your first real session.&lt;/strong&gt; Go to &lt;strong&gt;Agent&lt;/strong&gt; in the sidebar, then click the &lt;strong&gt;MEMORY&lt;/strong&gt; button. On a fresh install it shows as "MISSING". Clicking it opens an empty editor and saving creates the file.&lt;/p&gt;

&lt;p&gt;MEMORY.md is for curated, distilled facts the agent needs to remember across every session: standing rules, key decisions, strong preferences. It gets injected into the context window on every turn, so keep it lean. This is not a journal or a log:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="gu"&gt;## Preferences&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Communication style: direct and concise, skip filler phrases
&lt;span class="p"&gt;-&lt;/span&gt; Response length: brief by default, thorough when I ask for detail
&lt;span class="p"&gt;-&lt;/span&gt; Don't ask clarifying questions for simple tasks. Just do it.

&lt;span class="gu"&gt;## Standing Rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Always show me a draft before sending anything externally (email, messages, posts)
&lt;span class="p"&gt;-&lt;/span&gt; Ask before making calendar changes or bookings
&lt;span class="p"&gt;-&lt;/span&gt; If a task fails twice with the same error, stop and report. Don't keep retrying.
&lt;span class="p"&gt;-&lt;/span&gt; Never modify config files without telling me what you're changing and why

&lt;span class="gu"&gt;## Key Context&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Main tools I use: [e.g. VS Code, GitHub, Notion, Slack]
&lt;span class="p"&gt;-&lt;/span&gt; Current focus: [e.g. shipping a feature, writing content, job search]
&lt;span class="p"&gt;-&lt;/span&gt; Things I'm working on this month: [1-3 active projects]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep MEMORY.md short. If it gets too long, OpenClaw truncates it when injecting into context, which means the stuff at the bottom silently disappears. Detailed notes belong in &lt;code&gt;memory/YYYY-MM-DD.md&lt;/code&gt; daily logs, not here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;USER.md is different.&lt;/strong&gt; Click the &lt;strong&gt;USER&lt;/strong&gt; button on the same page. This is the agent's profile of you as a person: your background, role, working style, what you care about. The agent builds this over time, but seeding it upfront saves weeks of it figuring things out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# User Profile&lt;/span&gt;

&lt;span class="gu"&gt;## Who I Am&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Name: [your name]
&lt;span class="p"&gt;-&lt;/span&gt; Role: [what you do, e.g. "frontend engineer at a fintech startup"]
&lt;span class="p"&gt;-&lt;/span&gt; Location / timezone: [city, UTC offset]
&lt;span class="p"&gt;-&lt;/span&gt; Working hours: [e.g. 9am–6pm weekdays]

&lt;span class="gu"&gt;## How I Work&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; I prefer [async / real-time] communication
&lt;span class="p"&gt;-&lt;/span&gt; I get annoyed when: [e.g. things are over-explained, asked obvious questions]
&lt;span class="p"&gt;-&lt;/span&gt; I work best when: [e.g. given context upfront, shown options not just answers]

&lt;span class="gu"&gt;## What I'm Trying to Do With OpenClaw&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [e.g. automate my morning briefing, manage email triage, track projects]
&lt;span class="p"&gt;-&lt;/span&gt; My biggest time sink right now: [what you want help with most]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The short version: USER.md is who you are. MEMORY.md is what the agent must remember. AGENTS.md is the agent's operating procedure: how it behaves, what it does on startup, routing rules. All three get injected into every session, so they all matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable Dreaming.&lt;/strong&gt; Go to your OpenClaw dashboard, find the Dreaming tab, and toggle it on. This is a beta feature with two phases: a shallow phase that writes session summaries to &lt;code&gt;DREAMS.md&lt;/code&gt;, and a deep phase that promotes the strongest memories into &lt;code&gt;MEMORY.md&lt;/code&gt; as permanent long-term storage. If you enable it and don't see things showing up in MEMORY.md right away, that's normal. Only the deep phase promotes to permanent memory, and it runs on its own schedule. Without Dreaming, session logs accumulate but never get distilled into anything the agent reliably recalls.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Back Up Your Workspace and Use /new
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How sessions actually work.&lt;/strong&gt; Every message you send in a session gets included in every future API call in that session. After a few hours of chatting, you're sending thousands of tokens of old conversation with every new message. It costs money, and it makes your agent slower and more confused as the context gets cluttered.&lt;/p&gt;

&lt;p&gt;OpenClaw handles this in two ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compaction&lt;/strong&gt; happens automatically when the context window fills up. It summarizes older conversation history into a condensed entry to free up space. You can also trigger it manually with &lt;code&gt;/compact&lt;/code&gt; before a long task, or guide what it focuses on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/compact Focus on the deployment task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;/new&lt;/code&gt;&lt;/strong&gt; starts a completely fresh session. Your agent keeps everything: SOUL.md, MEMORY.md, USER.md, all your workspace files. You're just clearing the conversation buffer. Use it when you're switching topics or starting a new task. Many people in the community report meaningful cost savings just from using &lt;code&gt;/new&lt;/code&gt; regularly. Most people don't know it exists.&lt;/p&gt;

&lt;p&gt;Also learn &lt;code&gt;/btw&lt;/code&gt;. It fires off a side question without touching your main session context. Great for quick lookups mid-task without opening a whole new session.&lt;/p&gt;

&lt;p&gt;The key rule: anything important needs to be written to files, not just said in chat. Session memory dies on compaction. If you told your agent something three days ago in conversation, it may not remember it. If it's in MEMORY.md, it will.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Back up your workspace files.&lt;/strong&gt; Your agent's entire state (personality, memory, projects, tools) lives in markdown files in the OpenClaw workspace directory. If something goes wrong (and eventually something will), you want to be able to roll back. Make sure to use a &lt;strong&gt;private&lt;/strong&gt; repo. Just ask your agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Help me put my OpenClaw workspace files onto a private GitHub repo
and commit changes automatically after each session.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This takes about ten minutes and has saved people from complete resets when their agent went off the rails or a config change broke something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check Control → Usage daily in your first week.&lt;/strong&gt; The usage panel shows total tokens, cost, and a breakdown by model. You're looking for two things: your baseline daily cost, and your cache hit rate. A high cache hit rate means you're not paying full price for repeated context. That's healthy. If costs are higher than expected, check whether your context profile is set too large under &lt;strong&gt;Settings → AI &amp;amp; Agents → Agent Defaults → Bootstrap Max Chars&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;After the first week you'll have a sense of what normal looks like. Set a budget alert at 2x your daily baseline so you get notified if something starts running away. A misconfigured heartbeat task or a loop in an automation can burn through tokens fast without any visible output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;a href="https://aws.amazon.com/lightsail/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Try Amazon Lightsail&lt;/a&gt; — run OpenClaw on a dedicated VPS&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openclaw.ai/" rel="noopener noreferrer"&gt;OpenClaw docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openclaw.ai/gateway/security" rel="noopener noreferrer"&gt;OpenClaw security audit docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clawhub.openclaw.ai/" rel="noopener noreferrer"&gt;ClawHub skills marketplace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://isitstable.com/openclaw" rel="noopener noreferrer"&gt;Is it stable?&lt;/a&gt; — check before you update&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://youtu.be/oOCN30ulVyo?si=EM4_BcRy53Opf8-t" rel="noopener noreferrer"&gt;Tina's OpenClaw setup guide&lt;/a&gt; — the video that inspired a lot of this post&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;These five steps take a bit of time to do right. The config changes (gateway, model, SOUL.md) take about 30 minutes. Getting your memory files properly seeded and GitHub backup set up will take a bit longer, but it's worth doing before your first real session.&lt;/p&gt;

&lt;p&gt;Let me know in the comments if I missed anything. I'm still learning this thing too.&lt;/p&gt;

</description>
      <category>openclaw</category>
      <category>aiagents</category>
      <category>selfhosted</category>
      <category>aws</category>
    </item>
    <item>
      <title>Build Your Own AI Butler - A Scheduled Agent That Runs Itself!</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Wed, 06 May 2026 16:00:47 +0000</pubDate>
      <link>https://dev.to/aws/build-your-own-ai-butler-a-scheduled-agent-that-runs-itself-3dmk</link>
      <guid>https://dev.to/aws/build-your-own-ai-butler-a-scheduled-agent-that-runs-itself-3dmk</guid>
      <description>&lt;p&gt;I want an AI agent that works for me. I want it to search up the latest news, and I want it to deliver it to me in a daily message. I want to chat with it, and get advice. I want to expand its capabilities in the future. And most importantly I want to control it. Basically, I want Jarvis.&lt;/p&gt;

&lt;p&gt;Yes Jarvis, the fictional AI character in the Marvel Cinematic Universe (MCU), serving as Tony Stark's butler. I've never had my own agentic butler, and now that I think about it who wouldn't? 😆 A few years ago I would have told you this would have never happened. But today with the rise of agents the future is here.&lt;/p&gt;

&lt;p&gt;Well sort of... we are getting closer than ever though!&lt;/p&gt;

&lt;p&gt;To take on this task one of the most obvious routes was to use something like OpenClaw. Except, OpenClaw is too new, and a little too much for what I needed, so like any good engineer I decided to roll my own. &lt;em&gt;&lt;a href="https://openclaw.ai/" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt; is an open-source, autonomous AI agent that runs locally on a your machine or VPS to automate tasks&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Recently &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore&lt;/a&gt; released its managed harness in preview. You define the agent in a config file and a system prompt, and then deploy it. That was it! From my research it looked very straightforward to get up and running, and it could save me a lot of time in the long run. So I decided to take the journey to create my very own Jarvis.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want the conceptual breakdown of what a harness actually is in more detail, &lt;a href="https://dev.to/aws/what-is-an-agent-harness-a-hands-on-guide-with-agentcore-harness-1h33"&gt;my friend Morgan's post on dev.to&lt;/a&gt; covers that well. This post is the hands-on build.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After several hours of tinkering and research I created "The Pulse." It opens &lt;a href="https://news.ycombinator.com" rel="noopener noreferrer"&gt;Hacker News&lt;/a&gt; in a real browser, grabs Reddit posts via RSS, saves everything to persistent storage, and every 6 hours writes a trending stories digest. It messages me every day, or I can message it anytime and ask what's happening in AI. It reads from the data it's been collecting all day.&lt;/p&gt;

&lt;p&gt;If you're more like me and love videos, check out the full video below on how to get started. I'm also including the source code so you can try it yourself. Otherwise continue on and learn all about how to get started! Feel free to let me know how it goes.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Full code:&lt;/strong&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/aws-samples" rel="noopener noreferrer"&gt;
        aws-samples
      &lt;/a&gt; / &lt;a href="https://github.com/aws-samples/sample-AgentCore-Managed-Harness-News" rel="noopener noreferrer"&gt;
        sample-AgentCore-Managed-Harness-News
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;AgentCore Managed Harness News Sample — The Pulse&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A sample AgentCore managed harness project that runs a scheduled news digest agent ("The Pulse") on Amazon Bedrock AgentCore and posts the results to Telegram.&lt;/p&gt;
&lt;p&gt;The agent is scheduled with EventBridge Scheduler, invoked via a Lambda trigger, and uses AgentCore Memory to retain context across runs. It's a realistic end-to-end example of building a recurring, long-running agent on AWS.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What's in here&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;AgentCore-News-Sample/
├── agentcore/
│   ├── agentcore.json          # Project config (runtimes, memories, etc.)
│   ├── aws-targets.json        # AWS account + region to deploy into
│   ├── cdk/                    # CDK infrastructure (@aws/agentcore-cdk)
│   │   ├── bin/cdk.ts          # CDK entry — synthesizes both stacks
│   │   ├── lib/cdk-stack.ts    # AgentCore stack (agent, memory, roles)
│   │   ├── lib/scheduler-stack.ts  # Scheduler + Lambda trigger
│   │   └── lambda/pulse-trigger/   # Lambda that invokes the harness
│   └── .llm-context/           # Type definitions for AI coding assistants&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/aws-samples/sample-AgentCore-Managed-Harness-News" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;AgentCore Harness is in preview. The CLI, pricing, and some APIs may change. Check the &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AgentCore docs&lt;/a&gt; for the latest.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;
  Table of Contents
  &lt;ul&gt;
&lt;li&gt;What We're Building&lt;/li&gt;
&lt;li&gt;Why Not Just Use Lambda?&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Create the Harness Project&lt;/li&gt;
&lt;li&gt;Configure the Agent&lt;/li&gt;
&lt;li&gt;Deploy&lt;/li&gt;
&lt;li&gt;Test the Hourly Collection&lt;/li&gt;
&lt;li&gt;Test the Summary Digest&lt;/li&gt;
&lt;li&gt;Ask It Questions&lt;/li&gt;
&lt;li&gt;Pull Data Out Without Burning Tokens&lt;/li&gt;
&lt;li&gt;Schedule It With EventBridge and Lambda&lt;/li&gt;
&lt;li&gt;Troubleshooting&lt;/li&gt;
&lt;li&gt;Cost Estimate&lt;/li&gt;
&lt;li&gt;Cleanup&lt;/li&gt;
&lt;li&gt;What's Next&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&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%2Fcpan3tm1n4vjbpmz3437.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%2Fcpan3tm1n4vjbpmz3437.png" alt="The Pulse architecture diagram" width="800" height="726"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Don't worry if this diagram looks intimidating! The most complicated part is just getting the EventBridge connected with the correct IAM roles to the harness and dealing with the infrastructure code if you're new to that. Let's break down the flow here.&lt;/p&gt;

&lt;p&gt;EventBridge fires every hour and triggers a Lambda. &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/what-is-amazon-eventbridge.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;EventBridge&lt;/a&gt; is AWS's scheduler service, think of it like a cron job in the cloud. The &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Lambda&lt;/a&gt; makes one API call to the harness. Inside the harness, the agent runs in its own microVM, which is basically a lightweight virtual machine that spins up just for your session with its own CPU, memory, and filesystem. The agent opens a browser, scrapes HN, fetches Reddit RSS, and saves a snapshot. Every 6th run it reads all the snapshots, finds trends, and writes a digest. And because the session ID stays the same across runs, all that data accumulates on &lt;code&gt;/mnt/data&lt;/code&gt;. You can also just talk to it anytime through the CLI and ask what's trending.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use Lambda?
&lt;/h2&gt;

&lt;p&gt;Before we get into the details, one question you may be thinking is couldn't we do all this with just a Lambda, some &lt;a href="https://github.com/strands-agents/sdk-python" rel="noopener noreferrer"&gt;Strands Agents&lt;/a&gt; code, an LLM, and a dream? For a simple one-shot task, you are probably right. But this agent needs a few things Lambda can't give you, which makes this approach well worth it.&lt;/p&gt;

&lt;p&gt;First, the agent opens a real browser to scrape Hacker News. Lambda can't spin up a managed browser session. You'd need &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Fargate&lt;/a&gt; plus Playwright, and now you're managing containers. I love containers, but let's skip it for this scenario.&lt;/p&gt;

&lt;p&gt;Second, it saves hourly snapshots to &lt;code&gt;/mnt/data/runs/&lt;/code&gt; and reads them back 6 hours later. Lambda's &lt;code&gt;/tmp&lt;/code&gt; gets wiped after every invocation. You'd need &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;S3&lt;/a&gt; plus custom read/write logic for every file operation. That's a lot of work for what should be "save a file."&lt;/p&gt;

&lt;p&gt;Third, the 6-hour summary run reads all the accumulated data, compares scores across snapshots, identifies trends, and writes a digest. That can take a while. Lambda maxes out at 15 minutes. The harness defaults to a 1-hour timeout per invocation (I set mine to 30 minutes since that's plenty), and the microVM itself can stay alive for up to 8 hours across multiple invocations.&lt;/p&gt;

&lt;p&gt;And then there's memory. AgentCore Memory is a built-in service that stores facts, preferences, and conversation summaries across sessions. So "What did I ask about yesterday?" works because the memory carries context forward. In Lambda, you'd need &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;DynamoDB&lt;/a&gt; plus a custom integration.&lt;/p&gt;

&lt;p&gt;Could you build all of this with Lambda plus S3 plus Fargate plus DynamoDB plus Step Functions? Sure. That's five services instead of one. That's four too many for this tutorial.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Node.js 20+&lt;/li&gt;
&lt;li&gt;AWS credentials configured in a preview region (&lt;code&gt;us-west-2&lt;/code&gt;, &lt;code&gt;us-east-1&lt;/code&gt;, &lt;code&gt;eu-central-1&lt;/code&gt;, or &lt;code&gt;ap-southeast-2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CDK bootstrapped in your account (&lt;code&gt;cdk bootstrap&lt;/code&gt;). &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/home.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;CDK&lt;/a&gt; is the AWS Cloud Development Kit. It lets you define infrastructure in TypeScript instead of clicking through the console. If you haven't used it before, &lt;code&gt;npm install -g aws-cdk&lt;/code&gt; and then &lt;code&gt;cdk bootstrap&lt;/code&gt; in your account.&lt;/li&gt;
&lt;li&gt;The AgentCore CLI:
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Bedrock model access for Claude Sonnet 4.6 in your region. As of late 2025, Bedrock gives you automatic access to all serverless models in your region without needing to enable them individually. If your organization uses IAM policies or SCPs to restrict model access, make sure Sonnet isn't blocked. You can verify by opening the &lt;a href="https://us-west-2.console.aws.amazon.com/bedrock/home?region=us-west-2&amp;amp;trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Bedrock console&lt;/a&gt; and trying the model in the playground.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create the Harness Project
&lt;/h2&gt;

&lt;p&gt;This is where the fun begins. We'll create our very first managed harness.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; thepulse &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model-provider&lt;/span&gt; bedrock &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-storage-mount-path&lt;/span&gt; /mnt/data

&lt;span class="nb"&gt;cd &lt;/span&gt;thepulse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one command scaffolds the whole project. Harness config, memory, session storage, CDK app, IAM role. The &lt;code&gt;--session-storage-mount-path&lt;/code&gt; flag wires up persistent storage so the platform creates a &lt;code&gt;/mnt/data&lt;/code&gt; mount point for you. You don't have to worry about a Dockerfile, or container to manage.&lt;/p&gt;

&lt;p&gt;Now add the browser tool. AgentCore Browser is a managed headless Chrome instance that the harness can spin up on demand. Your agent can navigate pages, click elements, extract content, basically anything you'd do with Playwright but without managing the browser infrastructure yourself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore add tool &lt;span class="nt"&gt;--harness&lt;/span&gt; thepulse &lt;span class="nt"&gt;--type&lt;/span&gt; agentcore_browser &lt;span class="nt"&gt;--name&lt;/span&gt; browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what you end up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thepulse/
├── agentcore/
│   ├── agentcore.json          # Project-level resource config
│   ├── aws-targets.json        # Deployment targets (account, region)
│   ├── .env.local              # Local dev config (gitignored)
│   └── cdk/                    # CDK TypeScript app
├── app/
│   └── thepulse/
│       ├── harness.json        # Agent config
│       └── system-prompt.md    # Agent instructions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configure the Agent
&lt;/h2&gt;

&lt;p&gt;Our AgentCore CLI already set up the model, memory, and session storage. You just need to tweak the execution limits and write the system prompt.&lt;/p&gt;

&lt;p&gt;Edit &lt;code&gt;app/thepulse/harness.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;"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;"thepulse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&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;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bedrock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"modelId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"global.anthropic.claude-sonnet-4-6"&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;"tools"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agentcore_browser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"browser"&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;"skills"&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;"memory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"thepulseMemory"&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;"sessionStoragePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/mnt/data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxIterations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timeoutSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth mentioning here.&lt;/p&gt;

&lt;p&gt;I set &lt;code&gt;maxIterations&lt;/code&gt; to 75 because browser interactions are chatty. That eats through iterations fast, and 75 gives room for the full HN browser scrape plus Reddit RSS plus file operations without the agent hitting the ceiling.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;timeoutSeconds&lt;/code&gt; is 1800 (30 minutes). Browser sessions take time, and the summary run reads a lot of data. I didn't hit too many timeouts when testing, but this was a just-in-case update.&lt;/p&gt;

&lt;p&gt;This next setting is really useful. The platform creates the &lt;code&gt;/mnt/data&lt;/code&gt; mount point automatically when you set &lt;code&gt;sessionStoragePath&lt;/code&gt;. This mount point will be shared per session, no matter if the session times out.&lt;/p&gt;

&lt;p&gt;Now edit &lt;code&gt;app/thepulse/system-prompt.md&lt;/code&gt;. The CLI created this file with a default prompt. Replace it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You are The Pulse, an AI research agent that monitors Hacker News
and Reddit for AI/ML stories.

You have two modes:

&lt;span class="gu"&gt;### HOURLY COLLECTION&lt;/span&gt;
When the prompt says "hourly run":

&lt;span class="gs"&gt;**Hacker News (browser):**&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Use the browser tool to open https://news.ycombinator.com
&lt;span class="p"&gt;2.&lt;/span&gt; Extract the top 30 stories: title, score, comment count, URL
&lt;span class="p"&gt;3.&lt;/span&gt; Close the browser session when done

&lt;span class="gs"&gt;**Reddit (RSS — do NOT use the browser):**&lt;/span&gt;
Reddit blocks automated browser access (403). Use the RSS feed instead:
&lt;span class="p"&gt;1.&lt;/span&gt; Shell: &lt;span class="sb"&gt;`curl -s -A 'ThePulse/1.0'
   'https://www.reddit.com/r/MachineLearning/.rss'`&lt;/span&gt;
&lt;span class="p"&gt;2.&lt;/span&gt; Parse the XML to extract post titles, URLs, authors, and dates
&lt;span class="p"&gt;3.&lt;/span&gt; Note: RSS does not include upvote scores. That's expected.
&lt;span class="p"&gt;4.&lt;/span&gt; Also try r/artificial: &lt;span class="sb"&gt;`curl -s -A 'ThePulse/1.0'
   'https://www.reddit.com/r/artificial/.rss'`&lt;/span&gt;

&lt;span class="gs"&gt;**Save the snapshot:**&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Combine HN and Reddit data into a single JSON file
&lt;span class="p"&gt;2.&lt;/span&gt; Save to /mnt/data/runs/YYYY-MM-DD-HH.json
&lt;span class="p"&gt;3.&lt;/span&gt; If a previous hour's snapshot exists (check /mnt/data/runs/),
   compare and flag:
&lt;span class="p"&gt;   -&lt;/span&gt; Stories that appeared in both snapshots (persistent)
&lt;span class="p"&gt;   -&lt;/span&gt; HN stories with rising scores

&lt;span class="gu"&gt;### SUMMARY DIGEST&lt;/span&gt;
When the prompt says "generate summary":
&lt;span class="p"&gt;1.&lt;/span&gt; Read all snapshots from /mnt/data/runs/ for the last 6 hours
&lt;span class="p"&gt;2.&lt;/span&gt; Identify stories that appeared in multiple snapshots
   (persistent presence = trending)
&lt;span class="p"&gt;3.&lt;/span&gt; Identify stories with rising scores across snapshots
&lt;span class="p"&gt;4.&lt;/span&gt; Deduplicate and rank by persistence, score growth, and
   comment activity
&lt;span class="p"&gt;5.&lt;/span&gt; Write a markdown digest to
   /mnt/data/digests/YYYY-MM-DD-{morning|afternoon|evening|night}.md
&lt;span class="p"&gt;6.&lt;/span&gt; Include the top 10 trending stories with: title, source
   (HN/Reddit), peak score, trend direction, URL

&lt;span class="gu"&gt;### INTERACTIVE&lt;/span&gt;
When a user asks a question:
&lt;span class="p"&gt;-&lt;/span&gt; Read from /mnt/data/runs/ and /mnt/data/digests/ to answer
&lt;span class="p"&gt;-&lt;/span&gt; Use memory for conversational context

&lt;span class="gu"&gt;## Important&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; /mnt/data is persistent session storage. Use the same session ID
  across runs so data accumulates.
&lt;span class="p"&gt;-&lt;/span&gt; Always create directories before writing:
  &lt;span class="sb"&gt;`mkdir -p /mnt/data/runs /mnt/data/digests`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put the collection to be hourly and the snapshots to run every six hours. Retrospectively, this is a lot of data, and I could certainly have expanded that. I'd say collecting twice a day and having a snapshot created once a day would be fine as well. Feel free to tweak this prompt to your liking.&lt;/p&gt;

&lt;p&gt;One gotcha I'd like to pass on is that Reddit doesn't like agents scraping its data. The agent kept trying to open Reddit in the browser, getting 403'd, and burning 5-6 iterations before giving up. I sat there watching it retry over and over thinking "please just stop." The RSS feed at &lt;code&gt;reddit.com/r/MachineLearning/.rss&lt;/code&gt; works every time. Putting it explicitly in the system prompt saves you from that pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy
&lt;/h2&gt;



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

&lt;/div&gt;



&lt;p&gt;One command. It provisions the harness, memory, IAM role, session storage, and the underlying microVM infrastructure. This takes about 3-5 minutes because it's also spinning up AgentCore Memory and running CDK for the IAM roles.&lt;/p&gt;

&lt;p&gt;You can verify what got created:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test the Hourly Collection
&lt;/h2&gt;

&lt;p&gt;To test this out you'll need a stable session ID so &lt;code&gt;/mnt/data&lt;/code&gt; accumulates across runs, as I mentioned earlier. The session ID needs to be at least 33 characters (a UUID is 36, so that works). This is important because the same session ID means the same filesystem. That's how data builds up over time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SESSION_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"thepulse-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$SESSION_ID&lt;/span&gt;  &lt;span class="c"&gt;# Save this. You'll reuse it for every run.&lt;/span&gt;

agentcore invoke &lt;span class="nt"&gt;--harness&lt;/span&gt; thepulse &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--actor-id&lt;/span&gt; scheduler &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"Hourly run. Collect AI/ML stories from Hacker News and Reddit. Save the snapshot."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent will open a managed Chrome browser, navigate to &lt;code&gt;news.ycombinator.com&lt;/code&gt;, extract stories, fetch Reddit via RSS, and save everything to &lt;code&gt;/mnt/data/runs/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Verify it worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--exec&lt;/span&gt; &lt;span class="nt"&gt;--harness&lt;/span&gt; thepulse &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"ls -la /mnt/data/runs/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;--exec&lt;/code&gt; flag is one of my favorite things about the harness. It runs a shell command directly on the microVM. It's not connecting to any LLM so no tokens are used. You can use it to poke around the filesystem anytime without spending money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the Summary Digest
&lt;/h2&gt;

&lt;p&gt;Run the collection a couple times (or wait a few hours), then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--harness&lt;/span&gt; thepulse &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--actor-id&lt;/span&gt; scheduler &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"Generate summary. Read all snapshots from the last 6 hours and write a trending stories digest."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent reads all the hourly snapshots, identifies stories that appeared multiple times (trending), flags rising scores, and writes a markdown digest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask It Questions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--harness&lt;/span&gt; thepulse &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSION_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--actor-id&lt;/span&gt; demo-user &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"What's trending in AI right now?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent reads from &lt;code&gt;/mnt/data/runs/&lt;/code&gt; and &lt;code&gt;/mnt/data/digests/&lt;/code&gt; to answer with real data. Not hallucinated summaries, actual scores and URLs from the snapshots it collected. I really liked this part. The agent isn't guessing. It's reading files it wrote earlier that day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schedule It With EventBridge and Lambda
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;agentcore create&lt;/code&gt; command already scaffolded a CDK app at &lt;code&gt;agentcore/cdk/&lt;/code&gt;. We'll add a Lambda trigger and an EventBridge schedule to that existing project. EventBridge Scheduler is the AWS service that fires events on a cron schedule. We'll have it invoke a Lambda every hour, and that Lambda calls the harness. One &lt;code&gt;agentcore deploy&lt;/code&gt; handles the harness, one &lt;code&gt;cdk deploy&lt;/code&gt; handles the scheduler. I like keeping these separate because you iterate on the agent config way more often than the scheduling infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set Up the Lambda
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; agentcore/cdk/lambda/pulse-trigger
&lt;span class="nb"&gt;cd &lt;/span&gt;agentcore/cdk/lambda/pulse-trigger
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @aws-sdk/client-bedrock-agentcore @aws-sdk/client-secrets-manager
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; typescript @types/node esbuild
&lt;span class="nb"&gt;cd&lt;/span&gt; ../../../..
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;agentcore/cdk/lambda/pulse-trigger/index.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;BedrockAgentCoreClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;InvokeHarnessCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-bedrock-agentcore&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;SecretsManagerClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;GetSecretValueCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-secrets-manager&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TelegramSecret&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;botToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;defaultChatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agentCore&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;BedrockAgentCoreClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWS_REGION&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;secrets&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;SecretsManagerClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWS_REGION&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Cache the secret across warm invocations&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cachedSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TelegramSecret&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SECRET_TTL_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 5 minutes&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSecret&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TelegramSecret&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;cachedSecret&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cachedSecret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiry&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;cachedSecret&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretArn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TELEGRAM_SECRET_ARN&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;secretArn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing TELEGRAM_SECRET_ARN env var&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetSecretValueCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;SecretId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;secretArn&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="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TelegramSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SecretString&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cachedSecret&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="na"&gt;expiry&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;SECRET_TTL_MS&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;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendToTelegram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;botToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Telegram's message limit is 4096 chars. Split if needed.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&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;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\s\S]{1,4000}&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://api.telegram.org/bot&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;botToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/sendMessage`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;parse_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Markdown&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="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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Telegram send failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="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;hour&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;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getUTCHours&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;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STABLE_SESSION_ID&lt;/span&gt;&lt;span class="o"&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;harnessArn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HARNESS_ARN&lt;/span&gt;&lt;span class="o"&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;isSummaryRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;6&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isSummaryRun&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Generate summary. Read all snapshots from the last 6 hours and write a trending stories digest.&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;Hourly run. Collect AI/ML stories from Hacker News and Reddit. Save the snapshot.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&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;InvokeHarnessCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;harnessArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runtimeSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scheduler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agentCore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Collect the full response text from the stream&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;responseText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;chunkCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&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="k"&gt;await &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;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;chunkCount&lt;/span&gt;&lt;span class="o"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contentBlockDelta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delta&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="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;contentBlockDelta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;delta&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;delta&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;responseText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;delta&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delta&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="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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`\n[pulse-trigger] Stream complete — &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;chunkCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; chunks`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// On summary runs, send the digest to Telegram&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;isSummaryRun&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;responseText&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;botToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defaultChatId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSecret&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendToTelegram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;botToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defaultChatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;responseText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isSummaryRun&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;summary&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;collection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;digestSent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isSummaryRun&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;Let's break down what's happening here.&lt;/p&gt;

&lt;p&gt;The Lambda passes a stable session ID (from an environment variable) so every hourly run writes to the same &lt;code&gt;/mnt/data&lt;/code&gt;. It collects the full response text from the stream and logs it to &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/WhatIsCloudWatch.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;CloudWatch&lt;/a&gt;. On summary runs, it sends the digest to Telegram using a bot token stored in &lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Secrets Manager&lt;/a&gt;. Secrets Manager is the AWS service for storing sensitive values like API keys and tokens. You don't want a bot token sitting in an environment variable where anyone with console access can see it. The &lt;code&gt;sendToTelegram&lt;/code&gt; function handles Telegram's 4096 character limit by chunking the text. I found the CloudWatch logging really helpful for debugging early on when the agent would occasionally get stuck in a browser loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add the Scheduler Stack
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;agentcore/cdk/lib/scheduler-stack.ts&lt;/code&gt;. This is a CDK stack, which is basically a unit of deployment. Everything in this file gets deployed together as one &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;CloudFormation&lt;/a&gt; stack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TimeZone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NodejsFunction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-lambda-nodejs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Runtime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Secret&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-secretsmanager&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-iam&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;scheduler&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-scheduler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-scheduler-targets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;constructs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PulseSchedulerStackProps&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;StackProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;agentRuntimeArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;harnessArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;stableSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;telegramSecretArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PulseSchedulerStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PulseSchedulerStackProps&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;telegramSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromSecretCompleteArn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TelegramSecret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;telegramSecretArn&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;triggerFn&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;NodejsFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PulseTrigger&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_22_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../lambda/pulse-trigger/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;handler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;memorySize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;HARNESS_ARN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harnessArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;STABLE_SESSION_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stableSessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;TELEGRAM_SECRET_ARN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;telegramSecretArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;bundling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;externalModules&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;triggerFn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToRolePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock-agentcore:InvokeHarness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock-agentcore:InvokeAgentRuntime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;harnessArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agentRuntimeArn&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="c1"&gt;// Read access to the Telegram bot token secret&lt;/span&gt;
    &lt;span class="nx"&gt;telegramSecret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;triggerFn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HourlySchedule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ScheduleExpression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TimeZone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UTC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LambdaInvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;triggerFn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The Pulse — hourly AI/ML news collection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TriggerFunctionName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;triggerFn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;telegramSecret.grantRead(triggerFn)&lt;/code&gt; line gives the Lambda permission to read the bot token from Secrets Manager. The bot token lives there instead of in environment variables so it doesn't show up in CloudFormation outputs or the Lambda console.&lt;/p&gt;

&lt;p&gt;Wire it into &lt;code&gt;agentcore/cdk/bin/cdk.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PulseSchedulerStack&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../lib/scheduler-stack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ... existing AgentCore stack code ...&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PulseSchedulerStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ThePulseScheduler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;harnessArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;REPLACE_WITH_YOUR_HARNESS_ARN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;agentRuntimeArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;REPLACE_WITH_YOUR_RUNTIME_ARN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stableSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;REPLACE_WITH_YOUR_SESSION_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;telegramSecretArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;REPLACE_WITH_YOUR_SECRET_ARN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targets&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;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targets&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;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm showing placeholders here to make it clear what goes where. You can get your harness ARN and runtime ARN with &lt;code&gt;agentcore status --json&lt;/code&gt;, and use the same session ID you've been testing with. The Telegram secret ARN comes from Secrets Manager (store your bot token and chat ID as a JSON object with &lt;code&gt;botToken&lt;/code&gt; and &lt;code&gt;defaultChatId&lt;/code&gt; keys).&lt;/p&gt;

&lt;p&gt;That said, you shouldn't hardcode ARNs and secrets into source files you commit to git. The &lt;a href="https://github.com/aws-samples/sample-AgentCore-Managed-Harness-News" rel="noopener noreferrer"&gt;actual repo&lt;/a&gt; resolves the harness ARN and runtime ARN automatically from the deployed state file that &lt;code&gt;agentcore deploy&lt;/code&gt; creates, and reads the Telegram secret ARN and session ID from environment variables. That way nothing sensitive ends up in version control. Check the &lt;code&gt;cdk.ts&lt;/code&gt; in the repo for the full implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy and Test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install esbuild in the CDK project (needed for NodejsFunction)&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;agentcore/cdk &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;esbuild &lt;span class="nt"&gt;--save-dev&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ../..

&lt;span class="c"&gt;# Deploy the harness&lt;/span&gt;
agentcore deploy

&lt;span class="c"&gt;# Deploy the scheduler stack&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;agentcore/cdk &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx cdk deploy ThePulseScheduler &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ../..
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the Lambda directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda invoke &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--stack-name&lt;/span&gt; ThePulseScheduler &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Stacks[0].Outputs[?OutputKey==`TriggerFunctionName`].OutputValue'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cli-read-timeout&lt;/span&gt; 900 &lt;span class="se"&gt;\&lt;/span&gt;
  /tmp/lambda-response.json

&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/lambda-response.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--cli-read-timeout 900&lt;/code&gt; is important. The harness invocation takes a few minutes (the agent is browsing HN, fetching RSS, saving data), and the AWS CLI's default timeout is 60 seconds. Without it, you'll get a &lt;code&gt;Read timeout&lt;/code&gt; error even though the Lambda is still running fine. Don't make this mistake like I did.&lt;/p&gt;

&lt;p&gt;You should see &lt;code&gt;{"statusCode":200,"hour":...,"type":"collection"}&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;A few things I hit while building this. Hopefully they save you some time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;npm ci&lt;/code&gt; fails during CDK synth.&lt;/strong&gt; If you use &lt;code&gt;nodeModules&lt;/code&gt; in the bundling config instead of &lt;code&gt;externalModules: []&lt;/code&gt;, CDK copies your &lt;code&gt;package-lock.json&lt;/code&gt; into a temp directory and runs &lt;code&gt;npm ci&lt;/code&gt;. If the lock file is even slightly out of sync with &lt;code&gt;package.json&lt;/code&gt;, it fails. I went back and forth on this for a while before landing on the &lt;code&gt;externalModules: []&lt;/code&gt; approach that bundles everything with esbuild and skips &lt;code&gt;npm ci&lt;/code&gt; entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;AccessDeniedException&lt;/code&gt; on &lt;code&gt;ConverseStream&lt;/code&gt;.&lt;/strong&gt; This one got me. Bedrock now gives automatic access to all models in your region, so the old "Model Access" page is gone. But if your organization uses IAM (Identity and Access Management) policies or SCPs (Service Control Policies) to restrict which models can be invoked, you'll get this error. Check with your admin to make sure the model isn't blocked by a deny policy. I hit this when I tried to use Haiku for cheaper collection runs in an account that had an SCP limiting models to Sonnet only. The error message doesn't make it obvious that it's a permissions issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lambda times out but the agent is still running.&lt;/strong&gt; The Lambda timeout (15 min max) and the harness timeout (30 min) are separate. If the agent takes longer than the Lambda allows, the Lambda dies but the harness session keeps running in the background. Check CloudWatch logs for the Lambda, and use &lt;code&gt;agentcore logs --harness thepulse --since 1h&lt;/code&gt; for the harness side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reddit returns 403 in the browser.&lt;/strong&gt; Reddit blocks automated browser access. The system prompt tells the agent to use RSS instead, but if you're iterating on the prompt and forget to include that instruction, the agent will waste iterations trying to open Reddit in the browser. Ask me how I know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Estimate
&lt;/h2&gt;

&lt;p&gt;The main cost here is Bedrock model usage. Each hourly collection run uses around 10-15K tokens (browser interactions are chatty), and summary runs use more since they read all the accumulated data. Lambda and EventBridge are basically free at this scale.&lt;/p&gt;

&lt;p&gt;You can cut costs by enabling a cheaper model like Haiku in the Bedrock console and passing a model override for collection runs. Keep Sonnet for the summary runs that need more reasoning. The harness supports per-invocation model overrides so you don't need to redeploy. You can also just reduce the number of times it collects data.&lt;/p&gt;

&lt;p&gt;AgentCore Runtime and Browser have no separate charge during preview.&lt;/p&gt;

&lt;p&gt;Check the &lt;a href="https://aws.amazon.com/bedrock/pricing/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Bedrock pricing page&lt;/a&gt; for current model token costs and the &lt;a href="https://aws.amazon.com/bedrock/agentcore/pricing/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AgentCore pricing page&lt;/a&gt; for runtime and browser session rates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;Make sure you always clean up your stacks after trying out tutorials!&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;# Delete the scheduler stack&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;agentcore/cdk &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx cdk destroy ThePulseScheduler &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ../..

&lt;span class="c"&gt;# Delete the harness and all AgentCore resources&lt;/span&gt;
agentcore destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;agentcore destroy&lt;/code&gt; command removes the harness, memory, IAM role, and session storage. The CDK destroy removes the Lambda, EventBridge schedule, and any IAM roles CDK created.&lt;/p&gt;

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

&lt;p&gt;This is a starting point. I have thoughts on a few directions I could take my very own Jarvis 😊.&lt;/p&gt;

&lt;p&gt;Right now the Telegram integration is outbound only. I want to add two-way chat so I can message the bot and ask questions (without just using the command line). That means an &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;API Gateway&lt;/a&gt; webhook and a second Lambda that invokes the harness in interactive mode. The agent reads from &lt;code&gt;/mnt/data&lt;/code&gt; to answer with real data.&lt;/p&gt;

&lt;p&gt;More sources would be easy to add. The system prompt is just markdown. Add instructions for other news sources that a browser can reach. The nice thing is it would just take a prompt update and a new run of &lt;code&gt;agentcore deploy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I also want to try switching models per run. Enable Haiku in the Bedrock console and pass &lt;code&gt;--model-id&lt;/code&gt; on collection runs for cheaper hourly scrapes. Keep Sonnet for the summary runs that need more reasoning. The harness supports this per-invocation without a redeploy.&lt;/p&gt;

&lt;p&gt;And eventually, a dashboard. Have the agent generate an HTML page from the accumulated data and sync it to S3. Use &lt;code&gt;--exec&lt;/code&gt; to run &lt;code&gt;aws s3 sync&lt;/code&gt; from the microVM.&lt;/p&gt;

&lt;p&gt;After building this, I'm convinced the harness approach works really well for simpler use cases like my personal news Jarvis. If your agent needs persistent state, scheduled runs, and a browser, this is way less infrastructure than stitching it together yourself. If you just need a one-shot agent that answers a question, stick with Lambda. And of course if you need maximum flexibility, using AgentCore runtime is your best bet!&lt;/p&gt;

&lt;p&gt;Let me know in the comments what you think! Thanks!&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AgentCore Harness docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/browser-tool.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AgentCore Browser docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-tools.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Connect to tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness-memory.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Persist memory and filesystem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aws/agentcore-cli" rel="noopener noreferrer"&gt;AgentCore CLI @preview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/bedrock/agentcore/pricing/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AgentCore pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>agents</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Reaching for Python: Strands Agents TypeScript SDK Just Hit 1.0</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 04 May 2026 20:11:08 +0000</pubDate>
      <link>https://dev.to/aws/stop-reaching-for-python-strands-agents-typescript-sdk-just-hit-10-4lk6</link>
      <guid>https://dev.to/aws/stop-reaching-for-python-strands-agents-typescript-sdk-just-hit-10-4lk6</guid>
      <description>&lt;p&gt;A lot of production codebases are TypeScript. A lot of agent frameworks are Python. If yours is both, you either rewrite your stack or build a bridge between two languages. Strands Agents just shipped 1.0 of the TypeScript SDK, so now you don't have to. It's the full framework, native TypeScript types and all. And it does things Python can't, like running agents in the browser.&lt;/p&gt;

&lt;p&gt;The Python SDK has been in production since May 2025. This is the same model-driven approach, now with full TypeScript types and Zod-validated tools.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Full disclosure, I'm a Developer Advocate for AWS. Strands is an open source project from AWS. I've been building with the Python SDK for months, so I was curious how the TypeScript version compares. So far, it's been great.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this post we'll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Getting a basic agent running&lt;/li&gt;
&lt;li&gt;Defining type-safe tools with Zod&lt;/li&gt;
&lt;li&gt;Connecting MCP servers&lt;/li&gt;
&lt;li&gt;Streaming responses&lt;/li&gt;
&lt;li&gt;Running agents in the browser&lt;/li&gt;
&lt;li&gt;Multi-agent patterns: agent-as-tool, Graph, and Swarm&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @strands-agents/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Lines to a Working Agent
&lt;/h2&gt;

&lt;p&gt;The API is small on purpose. Create an agent, then invoke.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a helpful assistant.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;What makes TypeScript great for building agents?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bedrock is the default model provider. If you want OpenAI, Anthropic, Google, or anything that works with the Vercel AI SDK, you swap one import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;OpenAIModel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk/models/openai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAIModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-5.4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a helpful assistant.&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;You just swap the model import. The agent, tools, and invocation pattern don't change at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zod Tools
&lt;/h2&gt;

&lt;p&gt;TypeScript is a good fit here because you're defining the contract between your agent and your tools at the type level. You define a tool with a Zod schema and get runtime validation plus full type inference at compile time. The model can't pass garbage to your tool without Zod catching it, and your editor knows the exact shape of every input before you even run anything.&lt;/p&gt;

&lt;p&gt;Here's a GitHub lookup tool in about 30 lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;githubRepo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_github_repo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Get info about a GitHub repository.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Repository owner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Repository name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.github.com`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;`/repos/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&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;full_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — ⭐ &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stargazers_count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; stars`&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;githubRepo&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a developer assistant.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;input&lt;/code&gt; parameter in that callback is fully typed. Your editor knows &lt;code&gt;input.owner&lt;/code&gt; is a string. No &lt;code&gt;any&lt;/code&gt;, no type casting.&lt;/p&gt;

&lt;p&gt;The SDK also ships with built-in tools for bash, file editing, HTTP requests, and notebooks. Your agent can read files, hit APIs, and run shell commands without writing any tool code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk/vended-tools/bash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileEditor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk/vended-tools/file-editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;httpRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk/vended-tools/http-request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;notebook&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk/vended-tools/notebook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileEditor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;httpRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notebook&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a helpful coding assistant.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  MCP
&lt;/h2&gt;

&lt;p&gt;If you're already using MCP servers, they plug right in. I tested this with the filesystem MCP server and it worked well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;McpClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StdioClientTransport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/sdk/client/stdio.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mcp&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;McpClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StdioClientTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-y&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/server-filesystem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mcp&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;Any MCP-compatible server works the same way. One &lt;code&gt;McpClient&lt;/code&gt; import and you're done. I've used this with the filesystem server, a Postgres server, and a few custom ones, and the setup is identical every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming
&lt;/h2&gt;

&lt;p&gt;Async iterators let you generate chunks and stream them back to the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a creative storyteller.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;printer&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="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tell me a short story about a brave toaster.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;modelStreamUpdateEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;modelContentBlockDeltaEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textDelta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&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="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 event type checking is verbose. You're narrowing through three nested types to get to the actual text delta. That's just how TypeScript streaming works when the event union is this wide. Once you have the pattern, you copy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Runs in the Browser
&lt;/h2&gt;

&lt;p&gt;You can run agents in the browser. Most people don't think about this because the Python SDK can't do it natively.&lt;/p&gt;

&lt;p&gt;There's a demo in the &lt;a href="https://github.com/strands-agents/sdk-typescript/tree/main/strands-ts/examples/browser-agent" rel="noopener noreferrer"&gt;&lt;code&gt;strands-agents/sdk-typescript&lt;/code&gt;&lt;/a&gt; repo where you chat with an agent and it builds a live HTML canvas in real time. The agent runs entirely client-side.&lt;/p&gt;

&lt;p&gt;That opens up things that weren't possible before: local-first tools where the agent runs on the user's machine, interactive assistants embedded in your app without a server round-trip, or even running the model locally if you want to skip the round-trip entirely.&lt;/p&gt;

&lt;p&gt;I cloned the demo and had it running in a few minutes.&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/strands-agents/sdk-typescript.git
&lt;span class="nb"&gt;cd &lt;/span&gt;sdk-typescript/strands-ts/examples/browser-agent
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Multi-Agent Patterns
&lt;/h2&gt;

&lt;p&gt;The SDK ships with three ways to combine agents: agent-as-tool, Graph, and Swarm. Each one fits a different problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent-as-tool
&lt;/h3&gt;

&lt;p&gt;This is the one you'll reach for most. Pass a sub-agent directly into another agent's &lt;code&gt;tools&lt;/code&gt; array and it gets wrapped automatically. The outer agent decides when to call it, just like any other tool.&lt;/p&gt;

&lt;p&gt;Think of a writer agent that has a researcher agent as a tool. The writer decides when it needs facts, calls the researcher, gets the results back, and uses them to write. The code below is exactly that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;researcher&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;researcher&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Researcher&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Finds factual information and returns concise bullet-point findings.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a fact-finder. Return 3-5 concise bullet points. Be brief.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;printer&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;writer&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a prose writer. Use the Researcher tool to gather facts, then write one short paragraph.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;researcher&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// sub-agent auto-wrapped via asTool()&lt;/span&gt;
  &lt;span class="na"&gt;printer&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Write a paragraph about the deepest point in the ocean.&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;h3&gt;
  
  
  Graph
&lt;/h3&gt;

&lt;p&gt;Graph is for deterministic pipelines. You define agents as nodes and wire them together with edges. A node runs once all its upstream dependencies complete, so execution order is guaranteed. Nodes with no dependency between them can run in parallel.&lt;/p&gt;

&lt;p&gt;This pattern works well when you know the steps upfront and need them to always run in the same order. A research pipeline is a good example. Plan the questions, research each one, then synthesize the results. You wouldn't want the synthesizer running before the researcher finishes, and you don't want the model deciding to skip steps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Graph&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;planner&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;planner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Break the request into 2 short research questions.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;researcher&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;researcher&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Answer each research question in 1-2 sentences. Be concise.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;synthesiser&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;synthesiser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Write a 2-3 sentence summary of the research.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;graph&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;Graph&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;planner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;researcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;synthesiser&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;edges&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;planner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;researcher&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;researcher&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;synthesiser&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="c1"&gt;// MultiAgentResult.content holds the combined output from terminus nodes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;What are the main impacts of lithium mining?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;edges&lt;/code&gt; array defines the DAG (directed acyclic graph — basically a dependency map). &lt;code&gt;planner&lt;/code&gt; runs first, feeds into &lt;code&gt;researcher&lt;/code&gt;, which feeds into &lt;code&gt;synthesiser&lt;/code&gt;. The final output comes from the terminus node via &lt;code&gt;result.content&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Swarm
&lt;/h3&gt;

&lt;p&gt;Swarm is for when the path isn't known upfront. You define a set of agents with descriptions, pick a starting agent, and the model decides at runtime which agent handles the next step. Each agent either produces a final answer or hands off to another agent.&lt;/p&gt;

&lt;p&gt;I'd reach for this when building something like a customer support bot where you don't know which specialist is needed until you see what the user actually wrote. The routing logic lives in the model, not in your code. You're not writing &lt;code&gt;if request.includes('billing')&lt;/code&gt; anywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Swarm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;triage&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Classifies the user request and routes it to the right specialist.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Route to "billing" or "technical". Do not answer yourself.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;billing&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;billing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Handles billing and payment questions.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Resolve billing queries. Do not hand off further.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;technical&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;technical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Handles technical and product questions.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Resolve technical queries. Do not hand off further.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;swarm&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;Swarm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;technical&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxSteps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;swarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My invoice shows the wrong amount.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Heads up if you're on Bedrock:&lt;/strong&gt; Swarm uses &lt;a href="https://strandsagents.com/docs/api/typescript/Swarm/" rel="noopener noreferrer"&gt;structured output&lt;/a&gt; to drive handoffs. It forces a tool call to get the routing decision, and the current Claude Sonnet model on Bedrock throws a "does not support assistant message prefill" error when that happens. Swarm works fine with the Anthropic direct API or other providers that support forced tool choice. If you're on Bedrock and need dynamic routing, agent-as-tool is the safer option for now.&lt;/p&gt;

&lt;p&gt;I haven't tested the plugin system, session persistence, or OpenTelemetry tracing yet. The plugin system looks simple to use and I'll probably dig into it next.&lt;/p&gt;

&lt;p&gt;The SDK is open source. If you build something with it, I want to see it. The browser runtime especially. I'm curious where people take it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Covered
&lt;/h2&gt;

&lt;p&gt;The Strands Agents TypeScript SDK gives you a full agent framework without leaving your existing stack. Here's what we went through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A basic agent runs in two lines. Bedrock is the default, but swapping to OpenAI or Anthropic is one import.&lt;/li&gt;
&lt;li&gt;Zod tools give you runtime validation and compile-time type inference. The model can't pass bad input without Zod catching it.&lt;/li&gt;
&lt;li&gt;MCP servers connect with a single &lt;code&gt;McpClient&lt;/code&gt; import. Any MCP-compatible server works.&lt;/li&gt;
&lt;li&gt;Streaming uses async iterators. Verbose event types, but that's TypeScript being TypeScript.&lt;/li&gt;
&lt;li&gt;The browser runtime is real and working. Clone the demo and see it for yourself.&lt;/li&gt;
&lt;li&gt;For multi-agent work: agent-as-tool for orchestrator/worker setups, Graph for fixed pipelines, Swarm for dynamic routing (just not on Bedrock yet).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to go deeper, the &lt;a href="https://strandsagents.com/docs/user-guide/quickstart/typescript/" rel="noopener noreferrer"&gt;TypeScript quickstart&lt;/a&gt;, the &lt;a href="https://github.com/strands-agents/sdk-typescript" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;, and the &lt;a href="https://strandsagents.com/docs/api/typescript/" rel="noopener noreferrer"&gt;API docs&lt;/a&gt; are the best next stops.&lt;/p&gt;




&lt;h2&gt;
  
  
  Watch the Full Video
&lt;/h2&gt;

&lt;p&gt;If you prefer video format, here's a quick walkthrough of the TypeScript SDK and its key features:&lt;/p&gt;

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

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>aws</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
