<?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>Forem: Arham Mirkar</title>
    <description>The latest articles on Forem by Arham Mirkar (@arham_mirkar).</description>
    <link>https://forem.com/arham_mirkar</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3847393%2Fb738c1a2-914e-4cb3-aa21-958ca9ae95e2.png</url>
      <title>Forem: Arham Mirkar</title>
      <link>https://forem.com/arham_mirkar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/arham_mirkar"/>
    <language>en</language>
    <item>
      <title>I Killed Our 23-Zap Stack and Built One Database Instead — Here's Why Middleware Always Fails</title>
      <dc:creator>Arham Mirkar</dc:creator>
      <pubDate>Sun, 05 Apr 2026 14:35:43 +0000</pubDate>
      <link>https://forem.com/arham_mirkar/i-killed-our-23-zap-stack-and-built-one-database-instead-heres-why-middleware-always-fails-20lj</link>
      <guid>https://forem.com/arham_mirkar/i-killed-our-23-zap-stack-and-built-one-database-instead-heres-why-middleware-always-fails-20lj</guid>
      <description>&lt;h2&gt;
  
  
  We Used to Run 23 Zaps
&lt;/h2&gt;

&lt;p&gt;Slack → Asana (task from message), Asana → Notion (status sync), &lt;br&gt;
Notion → HubSpot (client updates), HubSpot → Slack (deal alerts).&lt;/p&gt;

&lt;p&gt;Every week: at least 2 silently broken. We'd find out 48+ hours &lt;br&gt;
later when a client asked why nothing had moved.&lt;/p&gt;

&lt;p&gt;The problem wasn't our Zaps. It was the architecture.&lt;/p&gt;
&lt;h2&gt;
  
  
  The 5 Technical Reasons Zapier Always Breaks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. API Schema Drift&lt;/strong&gt;&lt;br&gt;
When Notion migrated to v2, thousands of Zaps broke overnight. &lt;br&gt;
No warning. Silent failure until a trigger runs and errors out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Rate Limiting&lt;/strong&gt;&lt;br&gt;
Slack, Asana, Notion each have API rate limits. Under heavy use, &lt;br&gt;
Zapier hits them and silently drops automation runs. From your end: &lt;br&gt;
nothing looks wrong. Tasks are just never created.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. OAuth Token Expiry&lt;/strong&gt;&lt;br&gt;
Stored credentials expire. Zapier handles most renewals — but &lt;br&gt;
aggressive token rotation on the tool side silently breaks auth &lt;br&gt;
until a Zap runs and fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Webhook Timeouts&lt;/strong&gt;&lt;br&gt;
Zapier must acknowledge webhook receipt within 30s. Any network &lt;br&gt;
blip = event lost. No retry. No trace. No task in Asana.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Missing Context (The Unfixable One)&lt;/strong&gt;&lt;br&gt;
Even when Zapier works perfectly, it copies fields between &lt;br&gt;
isolated databases. A task created from a Slack message has &lt;br&gt;
no reference to the conversation. The Asana AI has no idea &lt;br&gt;
what the client actually said. This isn't fixable by better Zaps.&lt;/p&gt;
&lt;h2&gt;
  
  
  What We Built Instead
&lt;/h2&gt;

&lt;p&gt;At Kobin, every module — inbox, tasks, CRM, vault, calendar — &lt;br&gt;
shares one Supabase database with the same foreign keys.&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;// Every entity shares project_id + client_id&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt; &lt;span class="o"&gt;=&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;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;    &lt;span class="o"&gt;=&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;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client_id&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="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;    &lt;span class="o"&gt;=&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;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drive_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a client message arrives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It already has &lt;code&gt;project_id&lt;/code&gt; and &lt;code&gt;client_id&lt;/code&gt; — no sync needed&lt;/li&gt;
&lt;li&gt;Converting it to a task takes one click, context included&lt;/li&gt;
&lt;li&gt;The task appears in the client portal immediately (same FK)&lt;/li&gt;
&lt;li&gt;The AI sees messages + tasks + files simultaneously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero Zaps. Zero webhooks. Zero silent failures at 2am.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost Comparison
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Zapier stack (5 people):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slack + Asana + Notion + Zapier = $231/month&lt;/li&gt;
&lt;li&gt;+ HubSpot = $281–321/month
&lt;/li&gt;
&lt;li&gt;+ ~8hrs/month maintenance @ $75/hr = ~$900/month total&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Kobin:&lt;/strong&gt; $49/month. All of the above, natively connected.&lt;/p&gt;

&lt;p&gt;Full post with the architecture breakdown and FAQ:&lt;br&gt;
&lt;a href="https://www.kobin.team/blog/zapier-slack-asana-notion-alternative" rel="noopener noreferrer"&gt;https://www.kobin.team/blog/zapier-slack-asana-notion-alternative&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to discuss the real-time WebSocket approach we use for &lt;br&gt;
cross-module updates vs polling in the comments — it's an &lt;br&gt;
interesting tradeoff for unified workspaces.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building Kobin in public — agency OS replacing the Zapier stack.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Closed beta: kobin.team&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>webdev</category>
      <category>startup</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I Compared 10 Productivity Tools for Agency Founders — Here's the Full Stack Analysis</title>
      <dc:creator>Arham Mirkar</dc:creator>
      <pubDate>Fri, 03 Apr 2026 12:59:17 +0000</pubDate>
      <link>https://forem.com/arham_mirkar/i-compared-10-productivity-tools-for-agency-founders-heres-the-full-stack-analysis-4bdb</link>
      <guid>https://forem.com/arham_mirkar/i-compared-10-productivity-tools-for-agency-founders-heres-the-full-stack-analysis-4bdb</guid>
      <description>&lt;h2&gt;
  
  
  The problem I was trying to solve
&lt;/h2&gt;

&lt;p&gt;After running a small agency for a year on the standard tool stack (Slack + Notion + HubSpot + Linear + Google Drive + Calendly), I was spending &lt;strong&gt;$310/month&lt;/strong&gt; on tools that couldn't talk to each other.&lt;/p&gt;

&lt;p&gt;The worst part wasn't the cost. It was what happened when I tried to use AI on top of a fragmented stack.&lt;/p&gt;

&lt;p&gt;Slack AI saw messages.&lt;br&gt;
Notion AI saw documents.&lt;br&gt;
Linear AI saw issues.&lt;/p&gt;

&lt;p&gt;When I asked any of them &lt;em&gt;"what should I focus on today?"&lt;/em&gt; — none of them could give me a real answer. Because the real answer required data from all three tabs simultaneously.&lt;/p&gt;

&lt;p&gt;So I built Kobin. And then I wrote this comparison.&lt;/p&gt;


&lt;h2&gt;
  
  
  How I approached the ranking
&lt;/h2&gt;

&lt;p&gt;I evaluated 10 tools across five criteria:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Coverage&lt;/strong&gt; — how many agency workflows does it handle natively?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context&lt;/strong&gt; — when the AI responds, how much of your actual operation does it see?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt; — total monthly spend including all the tools you still need alongside it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client experience&lt;/strong&gt; — does it include a client portal, or do you need to build one separately?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI scope&lt;/strong&gt; — is the AI siloed to one module, or does it see the full workspace?&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  The Quick Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Price (5 seats)&lt;/th&gt;
&lt;th&gt;Client Portal&lt;/th&gt;
&lt;th&gt;AI Scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kobin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$49/mo&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;td&gt;Full workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion&lt;/td&gt;
&lt;td&gt;$16/mo&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Notion only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack&lt;/td&gt;
&lt;td&gt;$87/mo&lt;/td&gt;
&lt;td&gt;❌ (guest only)&lt;/td&gt;
&lt;td&gt;Messages only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asana&lt;/td&gt;
&lt;td&gt;$55–125/mo&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Asana only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClickUp&lt;/td&gt;
&lt;td&gt;$0–95/mo&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;ClickUp only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HubSpot&lt;/td&gt;
&lt;td&gt;$50–90/mo&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;HubSpot only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linear&lt;/td&gt;
&lt;td&gt;$0–80/mo&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Issues only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monday.com&lt;/td&gt;
&lt;td&gt;$60–120/mo&lt;/td&gt;
&lt;td&gt;Enterprise only&lt;/td&gt;
&lt;td&gt;Monday only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basecamp&lt;/td&gt;
&lt;td&gt;$299/mo flat&lt;/td&gt;
&lt;td&gt;~Basic&lt;/td&gt;
&lt;td&gt;❌ No AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Todoist&lt;/td&gt;
&lt;td&gt;$6/mo&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Tasks only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  The AI Context Problem
&lt;/h2&gt;

&lt;p&gt;This is the thing nobody writes about in these comparisons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fragmented tools = fragmented AI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model is only as useful as the context it receives. When you ask Asana Intelligence "what should I focus on today?", it answers from Asana data only. It has no idea your biggest client hasn't replied in 4 days (that's in Slack). It doesn't know the $22,000 deal in your pipeline is stale (that's in HubSpot). It can't see that your calendar is empty this afternoon (that's in Google Calendar).&lt;/p&gt;

&lt;p&gt;Kobin AI, before responding to anything, assembles a live briefing from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All active tasks (with overdue and blocked flags)&lt;/li&gt;
&lt;li&gt;Full CRM pipeline (deal values, win probabilities, stage)&lt;/li&gt;
&lt;li&gt;Team workload (live, across all members)&lt;/li&gt;
&lt;li&gt;Calendar events (upcoming and recent)&lt;/li&gt;
&lt;li&gt;Vault files (titles, types, folder)&lt;/li&gt;
&lt;li&gt;Last 20 inbox messages&lt;/li&gt;
&lt;li&gt;Contact profiles from CRM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a real exchange from our workspace:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt; "What's the single most important thing I should focus on today based on my tasks, pipeline, and calendar?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kobin AI:&lt;/strong&gt; "Today's top priority: Unblock and close out the overdue task 'Testing Deliverable Uploads' (Ahmed). It's the only blocked and overdue item across all active projects. Clearing it frees the pipeline for the next sprint and removes the single critical bottleneck on the Reelix project."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That answer pulled from tasks, team workload, and project context — simultaneously. No other tool on this list can do this because no other tool has a unified data model.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Total Cost Math
&lt;/h2&gt;

&lt;p&gt;Running the popular "best-in-class" stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Slack Pro (5 seats):      $87/month
Notion Team (5 seats):    $40/month
Asana Premium (5 seats):  $55/month
HubSpot Starter:          $50/month
Buffer Essentials:        $18/month
Zapier (to connect all):  $29/month
─────────────────────────────────────
Total:                    $279–$350/month
Annual:                   $3,348–$4,200/year
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus 51 minutes/person/week in context-switching overhead (Lokalise 2026 research).&lt;br&gt;
At $75/hr blended rate for a 5-person team: &lt;strong&gt;$15,938/year in lost productivity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Kobin replaces all of this at $49/month. The math is not close.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full write-up
&lt;/h2&gt;

&lt;p&gt;I published the complete ranked guide with individual tool reviews, pros/cons, and verdict for each:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.kobin.team/blog/best-productivity-tools-for-agencies" rel="noopener noreferrer"&gt;https://www.kobin.team/blog/best-productivity-tools-for-agencies&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're building something and want to see the AI context angle specifically:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.kobin.team/blog/kobin-ai-vs-notion-clickup" rel="noopener noreferrer"&gt;https://www.kobin.team/blog/kobin-ai-vs-notion-clickup&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Kobin is in closed beta. Join the waitlist at kobin.team — no credit card required.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>webdev</category>
      <category>saas</category>
      <category>agency</category>
    </item>
    <item>
      <title>How We Built an AI Layer That Understands an Entire Agency Workspace (Not Just One Module)</title>
      <dc:creator>Arham Mirkar</dc:creator>
      <pubDate>Mon, 30 Mar 2026 20:31:07 +0000</pubDate>
      <link>https://forem.com/arham_mirkar/how-we-built-an-ai-layer-that-understands-an-entire-agency-workspace-not-just-one-module-noc</link>
      <guid>https://forem.com/arham_mirkar/how-we-built-an-ai-layer-that-understands-an-entire-agency-workspace-not-just-one-module-noc</guid>
      <description>&lt;p&gt;We shipped the AI layer for &lt;a href="https://kobin.team" rel="noopener noreferrer"&gt;Kobin&lt;/a&gt; today — an agency operating system that replaces Slack, Notion, HubSpot, Linear, and Buffer. This is the technical story of how we built it and the specific decisions that made it work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with every AI feature shipped in the last two years
&lt;/h2&gt;

&lt;p&gt;Every productivity tool adds AI. The result is always the same: a model that knows about the host tool and nothing else. Slack AI knows about messages. Asana AI knows about tasks. HubSpot AI knows about contacts.&lt;/p&gt;

&lt;p&gt;None of them can answer the question &lt;em&gt;"which of my clients has gone quiet, and what's blocking their project?"&lt;/em&gt; — because the answer requires tasks, messages, and CRM data to exist in the same context.&lt;/p&gt;

&lt;p&gt;We decided to solve this before writing AI code. We spent the first year building the data model: tasks, projects, clients, CRM pipeline, vault files, calendar events, and real-time inbox — all in Supabase, all linked by foreign keys. Only then did we build the AI layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: MCP-style tools, not context dumps
&lt;/h2&gt;

&lt;p&gt;The naive approach is to dump your entire workspace into a system prompt and let the model figure it out. This fails for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Token bloat&lt;/strong&gt;: A workspace with 50 tasks, 10 projects, 30 CRM contacts, and 100 recent messages easily exceeds 8,000 tokens before you ask a single question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hallucination risk&lt;/strong&gt;: The more unstructured data you put in a prompt, the more the model interpolates and invents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staleness&lt;/strong&gt;: Workspace data changes constantly. A context dump taken at request time is already stale for any response that takes more than a few seconds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead, we built a tool-calling architecture inspired by the Model Context Protocol (MCP). The model starts with a minimal workspace summary (~100 tokens) and calls specific read tools to fetch exactly what it needs.&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;// Mini context — ~100 tokens, always fresh&lt;/span&gt;
&lt;span class="k"&gt;export&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;buildMiniContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;founderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;profileRes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tasksRes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projectsRes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;supabaseAdmin&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;profiles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;full_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;founderId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;supabaseAdmin&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;tasks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id, status, due_date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&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_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;founderId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is_completed&lt;/span&gt;&lt;span class="dl"&gt;"&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="c1"&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="s2"&gt;`Founder: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;profile&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="s2"&gt;`Tasks: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; active, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;overdueCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; overdue`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`Projects: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;activeProjects&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; active`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`Calendar: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;eventsCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; events this week`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the model calls tools like &lt;code&gt;get_tasks&lt;/code&gt;, &lt;code&gt;get_team_workload&lt;/code&gt;, or &lt;code&gt;search_contacts&lt;/code&gt; to drill into exactly what it needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 8 read tools
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;READ_TOOLS&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="s1"&gt;get_workspace_overview&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// High-level stats across all modules&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// Tasks with filter presets (overdue, blocked, due today...)&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_projects&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// Projects with task completion counts&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_team_workload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Team members with active task counts + workload labels&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_crm_pipeline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Pipeline by stage with deal values and stale flags&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_calendar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// Events with flexible range presets&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_vault_files&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// Vault documents filterable by project name&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search_contacts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// Full contact profile by name (fuzzy match)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tool returns compact, structured text rather than full JSON objects. This keeps tool results under 500 tokens while still providing all the detail the model needs.&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;// Example: get_tasks returns compact text, not full objects&lt;/span&gt;
&lt;span class="nx"&gt;lines&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="s2"&gt;`- [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PRI&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="nx"&gt;priority&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;t&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="s2"&gt; | &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;STAT&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="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="nf"&gt;shortDate&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="nx"&gt;due_date&lt;/span&gt;&lt;span class="p"&gt;)}${&lt;/span&gt;&lt;span class="nx"&gt;overdue&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;assignee&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;project&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="c1"&gt;// → "- [U] Finish homepage redesign | ip | 3/28 [OD] | →Ahmed | →Reelix"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The 5 action tools
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ACTION_TOOLS&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="s1"&gt;create_task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// Full task creation with name resolution&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update_task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// Partial updates by fuzzy title match&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete_task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// Returns confirmation request (never auto-deletes)&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Project creation&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update_project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Partial updates by fuzzy name match&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Name resolution
&lt;/h3&gt;

&lt;p&gt;The hardest part of action tools is resolving human names to database IDs. The user says "assign to Sarah" — the model needs &lt;code&gt;user_id: "uuid-here"&lt;/code&gt;. We built a three-tier fuzzy matcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fuzzyMatch&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="nl"&gt;match&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;index&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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="c1"&gt;// 1. Exact match&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exactIdx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;q&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;exactIdx&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;exactIdx&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;exactIdx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Starts with&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startsIdx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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;startsIdx&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;startsIdx&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startsIdx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Contains&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;containsIdx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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;containsIdx&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;containsIdx&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;containsIdx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. First name match&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstNameIdx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;q&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;firstNameIdx&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;firstNameIdx&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firstNameIdx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the match fails, the action returns an error message with the available names — so the model can correct itself on the next attempt rather than hallucinating an ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-fetching missing context
&lt;/h3&gt;

&lt;p&gt;The model gets richer context when read tools are called first — but sometimes it tries to take an action without calling the relevant read tool first. Rather than fail, action tools auto-fetch what they need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveTeamMember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TeamMemberContext&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;founderId&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;// If team context wasn't pre-loaded by a read tool, fetch it now&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;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;members&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;supabaseAdmin&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="s1"&gt;team_members&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;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id, position, profile:profiles!...(full_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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;founder_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;founderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is_active&lt;/span&gt;&lt;span class="dl"&gt;'&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;// ... populate team array&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fuzzyMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The multi-step reasoning loop
&lt;/h2&gt;

&lt;p&gt;Some requests require multiple tool calls in sequence. "Assign the most urgent overdue task to whoever has the lightest workload" requires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call &lt;code&gt;get_tasks&lt;/code&gt; with &lt;code&gt;filter: "overdue"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;get_team_workload&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Resolve name from workload results&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;update_task&lt;/code&gt; with the resolved assignee&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We run a loop of up to 4 steps. Each iteration appends the tool call and its result to the conversation before calling the model again:&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;for &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;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="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;groq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GROQ_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;messages&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="nx"&gt;ALL_TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tool_choice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt; &lt;span class="o"&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;choices&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;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;tool_calls&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;toolCalls&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No more tool calls — stream the final response&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;streamFinalResponse&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;actionEvents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Execute tools, append results, continue loop&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;toolCall&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&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="nf"&gt;isReadTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeReadTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;founderId&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;executeAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;messages&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="nf"&gt;toolCallResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolCall&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;result&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;h2&gt;
  
  
  The mixed batch problem and how we solved it
&lt;/h2&gt;

&lt;p&gt;Groq's Llama models sometimes try to call read tools and action tools in the same step — before the read tool results are available to inform the action. This causes the action to run with incomplete context.&lt;/p&gt;

&lt;p&gt;We detect and defer:&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;hasReadCalls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tc&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;READ_TOOL_NAMES&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="nx"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasActionCalls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tc&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;READ_TOOL_NAMES&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="nx"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isMixedBatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hasReadCalls&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;hasActionCalls&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;isMixedBatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Execute read tools, skip action tools with a "try again" message&lt;/span&gt;
  &lt;span class="c1"&gt;// The model re-calls action tools in the next step with actual data&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Deduplication guard for create actions
&lt;/h2&gt;

&lt;p&gt;One reliability issue with AI action tools: the model sometimes tries to call &lt;code&gt;create_task&lt;/code&gt; twice for the same request. We use a Set to track which create actions have fired:&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;createActionsExecuted&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;Set&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="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;toolName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;toolName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_project&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;createActionsExecuted&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="nx"&gt;toolName&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;success&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;message&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;toolName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; already executed — task already exists.`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;createActionsExecuted&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="nx"&gt;toolName&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;
  
  
  Streaming SSE responses
&lt;/h2&gt;

&lt;p&gt;The command bar streams responses using Server-Sent Events from a Next.js API route. Action events (task created, etc.) are emitted before the text stream so the UI can show success cards immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createStreamSSEResponse&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="nx"&gt;AsyncIterable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&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;actionEvents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&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;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encoder&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;TextEncoder&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;readable&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;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Emit action events first (for immediate UI feedback)&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;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;actionEvents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`data: &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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;action_executed&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;event&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// Then stream text response&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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&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="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="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;`data: &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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delta&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="nx"&gt;delta&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;
          &lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readable&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="nx"&gt;SSE_HEADERS&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;
  
  
  Why Groq + Llama 4 Scout
&lt;/h2&gt;

&lt;p&gt;We evaluated several options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI GPT-4o&lt;/strong&gt;: Great quality, but 3-5 second response times for tool-heavy requests killed the UX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic Claude Haiku&lt;/strong&gt;: Fast, but tool-calling reliability was inconsistent on multi-step sequences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groq + Llama 4 Scout (17B)&lt;/strong&gt;: Sub-2-second responses even for 3-step tool chains. Reliable function calling. Free for users at our current scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Groq's inference speed is genuinely a product differentiator here. When the AI responds in under 2 seconds, it feels like a feature. When it takes 6 seconds, it feels like a bug.&lt;/p&gt;




&lt;h2&gt;
  
  
  The delete confirmation pattern
&lt;/h2&gt;

&lt;p&gt;Destructive actions need a human in the loop. When the model calls &lt;code&gt;delete_task&lt;/code&gt;, it never deletes immediately. Instead it returns a &lt;code&gt;needs_confirmation: true&lt;/code&gt; flag with a &lt;code&gt;confirmation_action&lt;/code&gt; object. The frontend renders a "Confirm Delete" button:&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;// action-executor.ts&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;success&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;needs_confirmation&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;confirmation_action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete_task_confirmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resolved_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&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="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Delete task "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;task&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="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Frontend: shows "Confirm Delete" button&lt;/span&gt;
&lt;span class="c1"&gt;// On click → DELETE /api/ai/command with { task_id }&lt;/span&gt;
&lt;span class="c1"&gt;// Server executes the actual deletion&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Start with the data model.&lt;/strong&gt; The AI is only as good as the structure underneath it. We could not have built this if tasks, projects, clients, and vault files were in separate siloed tables without foreign key relationships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Read tools beat context dumps.&lt;/strong&gt; Structured tool calls with compact return values outperform pasting raw JSON into the system prompt. The model makes better decisions with less noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Auto-fetch gracefully.&lt;/strong&gt; If the model skips a read tool and goes straight to an action, the action should auto-fetch what it needs rather than return an error. Failing gracefully is better than a strict execution order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Name resolution is 30% of the work.&lt;/strong&gt; Users say "assign to Sarah." The model needs a UUID. Building reliable fuzzy matching (with fallback error messages that list available options) took more work than the actual tool implementations.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Daily morning brief (push notification, 8am, 6 sections)&lt;/li&gt;
&lt;li&gt;Pre-meeting brief (10 minutes before every calendar event)&lt;/li&gt;
&lt;li&gt;Client silence detection (background scan every 6 hours)&lt;/li&gt;
&lt;li&gt;Weekly client report auto-draft&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture supports all of these as new read tools + a background cron calling the AI with specific prompts.&lt;/p&gt;




&lt;p&gt;If you're building something similar or have questions about the tool-calling architecture, I'm happy to discuss in the comments.&lt;/p&gt;

&lt;p&gt;— Arham&lt;br&gt;&lt;br&gt;
Founder, &lt;a href="https://kobin.team" rel="noopener noreferrer"&gt;Kobin&lt;/a&gt; — Agency Operating System&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Full documentation: &lt;a href="https://kobin.team/docs" rel="noopener noreferrer"&gt;kobin.team/docs&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>supabase</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Slack, Notion, Asana, HubSpot — Why I Replaced All Four With One Tool for My Agency</title>
      <dc:creator>Arham Mirkar</dc:creator>
      <pubDate>Sun, 29 Mar 2026 14:26:37 +0000</pubDate>
      <link>https://forem.com/arham_mirkar/slack-notion-asana-hubspot-why-i-replaced-all-four-with-one-tool-for-my-agency-58k</link>
      <guid>https://forem.com/arham_mirkar/slack-notion-asana-hubspot-why-i-replaced-all-four-with-one-tool-for-my-agency-58k</guid>
      <description>&lt;p&gt;If you run a dev agency or freelance team, your stack probably looks something like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slack&lt;/strong&gt; for team chat and client messages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notion&lt;/strong&gt; for docs, briefs, and project wikis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asana or Linear&lt;/strong&gt; for task tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HubSpot&lt;/strong&gt; for CRM and leads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each tool is good at its job. The problem is they were never built to talk to each other.&lt;/p&gt;

&lt;p&gt;A client sends a message in Slack requesting a change. You need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a task in Asana&lt;/li&gt;
&lt;li&gt;Update the relevant file in Notion&lt;/li&gt;
&lt;li&gt;Schedule a review in Google Calendar&lt;/li&gt;
&lt;li&gt;Log the interaction in HubSpot&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s four manual steps across four tabs for one client message. Multiply that by every project you’re running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cost
&lt;/h2&gt;

&lt;p&gt;Research from UC Irvine shows it takes &lt;strong&gt;23 minutes&lt;/strong&gt; to regain full focus after switching context between apps. Harvard Business Review puts daily app toggles at &lt;strong&gt;1,200+ per knowledge worker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a 5-person team, that’s roughly 44 hours per year burned on tool-switching alone — before you even count the $200+ monthly in subscriptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built instead
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://www.kobin.team" rel="noopener noreferrer"&gt;Kobin&lt;/a&gt; — an agency operating system where the inbox, tasks, files, CRM, calendar, and client portal all live in one place and share the same data layer.&lt;/p&gt;

&lt;p&gt;When a client message comes in, converting it to a task is one click. The task links to the project. The project links to a Google Drive folder structure. The client sees their slice in a scoped portal. No Zapier. No manual bridges.&lt;/p&gt;

&lt;p&gt;I wrote a full breakdown of the tool-by-tool comparison here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.kobin.team/blog/slack-notion-asana-hubspot-alternatives" rel="noopener noreferrer"&gt;Full article → kobin.team/blog/slack-notion-asana-hubspot-alternatives&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where Slack fails for client-facing teams&lt;/li&gt;
&lt;li&gt;Why Notion becomes a junk drawer at scale&lt;/li&gt;
&lt;li&gt;Why Asana is overkill for a 5-person agency&lt;/li&gt;
&lt;li&gt;What a lightweight HubSpot alternative looks like&lt;/li&gt;
&lt;li&gt;The actual subscription + productivity math&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to discuss in the comments — curious what stacks others are running in 2026.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>webdev</category>
      <category>startup</category>
      <category>tools</category>
    </item>
    <item>
      <title>I built a real-time inbox with Supabase Realtime to replace Slack for agencies</title>
      <dc:creator>Arham Mirkar</dc:creator>
      <pubDate>Sat, 28 Mar 2026 09:45:08 +0000</pubDate>
      <link>https://forem.com/arham_mirkar/i-built-a-real-time-inbox-with-supabase-realtime-to-replace-slack-for-agencies-3gcf</link>
      <guid>https://forem.com/arham_mirkar/i-built-a-real-time-inbox-with-supabase-realtime-to-replace-slack-for-agencies-3gcf</guid>
      <description>&lt;p&gt;I spent a year building Kobin — an agency operating system that replaces Slack, Notion, HubSpot, Linear, and Buffer. The inbox was the hardest part to build. Here’s how it works technically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The inbox uses Supabase Realtime WebSocket channels. Each room (project room, group chat, DM) gets its own channel subscription. Switching rooms tears down the old channel and creates a new one — this keeps memory clean and avoids stale message states.&lt;/p&gt;

&lt;p&gt;Messages load the last 20 first. Older messages load on scroll-up demand with no flash to top. An animated skeleton UI shows while rooms are loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The features that were harder than expected
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Replies&lt;/strong&gt; — storing the quoted message as a nested reference without blowing up query complexity. We denormalize the preview text at write time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unread badges&lt;/strong&gt; — tracked via &lt;code&gt;last_read_at&lt;/code&gt; timestamp per user per room. Simple in theory, surprisingly tricky when you have real-time updates coming in while the user is actively reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/ai"&gt;@ai&lt;/a&gt; mentions&lt;/strong&gt; — when a message contains &lt;code&gt;@AI&lt;/code&gt;, the server assembles a full context payload (project status, last 20 messages, open tasks, upcoming meetings, vault items) and sends it to the model. The response streams back as a message in the thread with a purple avatar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File attachments&lt;/strong&gt; — images open in a full-screen lightbox with download. Office docs and PDFs get a preview card. ZIPs show a file icon with size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it instead of using Slack
&lt;/h2&gt;

&lt;p&gt;Slack’s AI sees messages. Asana’s AI sees tasks. Notion’s AI sees documents. None of them see the full picture.&lt;/p&gt;

&lt;p&gt;When everything — messages, tasks, files, meetings, CRM contacts — lives in the same schema, the AI actually has something to work with. That’s the whole point of building the inbox as part of a unified workspace rather than integrating with Slack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;Room list loads with 5 total queries regardless of how many rooms exist. This took several iterations to get right with Supabase RLS policies in play.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full product
&lt;/h2&gt;

&lt;p&gt;The inbox is one of 8 modules in Kobin. The others: Vault (Google Drive-backed), Tasks, CRM, Client Portal, Calendar, LinkedIn Studio, and an AI layer in development.&lt;/p&gt;

&lt;p&gt;Full writeup on the problem it solves (with data): &lt;a href="https://www.kobin.team/blog/the-283-problem" rel="noopener noreferrer"&gt;https://www.kobin.team/blog/the-283-problem&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Waitlist if you run an agency: &lt;a href="https://www.kobin.team" rel="noopener noreferrer"&gt;https://www.kobin.team&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to go deeper on any of the technical decisions in the comments.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>nextjs</category>
      <category>buildinpublic</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
