<?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>Vibe Coding Forem</title>
    <description>The most recent home feed on Vibe Coding Forem.</description>
    <link>https://vibe.forem.com</link>
    <atom:link rel="self" type="application/rss+xml" href="https://vibe.forem.com/feed"/>
    <language>en</language>
    <item>
      <title>VoiceScribe</title>
      <dc:creator>Jan Klein</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:40:47 +0000</pubDate>
      <link>https://vibe.forem.com/jan-klein/voicescribe-4k99</link>
      <guid>https://vibe.forem.com/jan-klein/voicescribe-4k99</guid>
      <description>&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%2F5sd3f4jp1q1z5xfjpmad.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%2F5sd3f4jp1q1z5xfjpmad.png" alt="VoiceScribe" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  VoiceScribe
&lt;/h2&gt;

&lt;h3&gt;
  
  
  RealTime Speech To Text App Built with Google AI Studio
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Live app: &lt;a href="https://voice-scribe.netlify.app" rel="noopener noreferrer"&gt;voice-scribe.netlify.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;p&gt;VoiceScribe is a realtime speech to text web app that supports &lt;strong&gt;20 languages&lt;/strong&gt;. You speak, it writes. Then you copy or share the text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works on all browsers&lt;/strong&gt; Chrome, Firefox, Safari, Edge, and mobile browsers.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;Simple. Your browser captures your voice. Google's AI turns it into text. You see it instantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I built it
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;HTML&lt;/strong&gt; structure of the page, language dropdown, buttons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSS&lt;/strong&gt; styling, responsive design, works on phone and desktop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vanilla JavaScript&lt;/strong&gt; microphone access, sending audio to Google API, displaying text, copy and share functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google AI Studio&lt;/strong&gt; provides the API key for Google Cloud Speech-to-Text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Netlify&lt;/strong&gt; hosting, free.&lt;/p&gt;

&lt;p&gt;No frameworks. No backend. No database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Useful for education
&lt;/h3&gt;

&lt;p&gt;This app is a perfect teaching example for students who want to learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How browser APIs work (microphone, clipboard, sharing)&lt;/li&gt;
&lt;li&gt;How to integrate Google AI into a real project&lt;/li&gt;
&lt;li&gt;How to build a complete useful app with just HTML, CSS, and JavaScript&lt;/li&gt;
&lt;li&gt;How to handle permissions, errors, and different browsers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My experience with Google AI Studio
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It helped me save time with API access. But it has problems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It does not follow human language instructions well. I had to use &lt;strong&gt;custom instructions&lt;/strong&gt; which only developers know how to write.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It sometimes adds code without asking or makes failures. You should &lt;strong&gt;make a backup each time you create a new version&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;Open &lt;strong&gt;&lt;a href="https://voice-scribe.netlify.app" rel="noopener noreferrer"&gt;voice-scribe.netlify.app&lt;/a&gt;&lt;/strong&gt; in any browser, pick a language, and speak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For information &amp;amp; questions contact me at:&lt;/strong&gt; &lt;strong&gt;&lt;a href="https://bix.pages.dev" rel="noopener noreferrer"&gt;bix.pages.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>deved</category>
      <category>learngoogleaistudio</category>
      <category>gemini</category>
      <category>googleaistudio</category>
    </item>
    <item>
      <title>AI helps me code faster, but not always understand better</title>
      <dc:creator>Bohdan Chuprynka</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:39:44 +0000</pubDate>
      <link>https://vibe.forem.com/__6146a1dd7/ai-helps-me-code-faster-but-not-always-understand-better-oik</link>
      <guid>https://vibe.forem.com/__6146a1dd7/ai-helps-me-code-faster-but-not-always-understand-better-oik</guid>
      <description>&lt;p&gt;I’ve been thinking about this a lot lately.&lt;/p&gt;

&lt;p&gt;AI coding tools are obviously useful. They help you move faster, get unstuck, write boilerplate, debug, and try things quickly.&lt;/p&gt;

&lt;p&gt;But I keep noticing the same tension:&lt;/p&gt;

&lt;p&gt;Sometimes AI helps you finish something without helping you really understand it.&lt;/p&gt;

&lt;p&gt;The code works.&lt;br&gt;
The task is done.&lt;br&gt;
But your confidence in the code is not always there.&lt;/p&gt;

&lt;p&gt;I think that matters a lot, especially for:&lt;br&gt;
 people learning to code&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;students&lt;/li&gt;
&lt;li&gt;developers trying to stay sharp&lt;/li&gt;
&lt;li&gt;anyone using AI but still wanting to actually understand what they’re building&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the reason I’ve been exploring an idea for a tool that feels more like a mentor inside the editor, not just something that generates code.&lt;/p&gt;

&lt;p&gt;Something that helps you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;understand what the code is doing&lt;/li&gt;
&lt;li&gt;catch weak spots in your thinking&lt;/li&gt;
&lt;li&gt;notice when you’re relying on AI too quickly&lt;/li&gt;
&lt;li&gt;learn while building, not only finish faster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m trying to figure out whether this is a real problem other people feel too, or if it’s just something I’ve personally noticed.&lt;/p&gt;

&lt;p&gt;So I made a very short survey for anyone who has used AI for coding, debugging, or learning programming.&lt;/p&gt;

&lt;p&gt;It’s just 4 questions and takes about 1 minute:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://forms.gle/GynGGBWF46de9jz47" rel="noopener noreferrer"&gt;https://forms.gle/GynGGBWF46de9jz47&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’ve used AI for coding even once, I’d really value your input.&lt;/p&gt;

&lt;p&gt;Also curious what people here think:&lt;/p&gt;

&lt;p&gt;Has AI made you better at coding, or mostly just faster?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>coding</category>
      <category>productivity</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>Chaining MCP Tools: Build AI Workflows That Search, Read, Analyze, and Write</title>
      <dc:creator>NeuroLink AI</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:27:53 +0000</pubDate>
      <link>https://vibe.forem.com/neurolink/chaining-mcp-tools-build-ai-workflows-that-search-read-analyze-and-write-e40</link>
      <guid>https://vibe.forem.com/neurolink/chaining-mcp-tools-build-ai-workflows-that-search-read-analyze-and-write-e40</guid>
      <description>&lt;h2&gt;
  
  
  What is MCP Tool Chaining?
&lt;/h2&gt;

&lt;p&gt;Imagine an AI that can not only understand a request like "Analyze our codebase for security vulnerabilities and report them," but also execute that request end-to-end. This requires more than just a single AI model. It needs an orchestration layer that allows the AI to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Search&lt;/strong&gt; external systems (e.g., GitHub, a file system).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Read&lt;/strong&gt; and comprehend various data formats (code, documents, database records).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Analyze&lt;/strong&gt; the information using its inherent intelligence.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Act&lt;/strong&gt; on its findings by writing code, creating issues, generating reports, or sending messages.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MCP tool chaining is the mechanism that makes this possible. It's an architecture where AI models interact with a standardized set of tools (MCP servers) that expose real-world capabilities. When an AI needs to perform a task that requires external interaction, it invokes the appropriate tool, processes the output, and then uses another tool to continue the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Workflow 1: Code Analysis and Issue Creation
&lt;/h2&gt;

&lt;p&gt;Let's consider a practical example: an AI agent performing continuous code quality monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Search GitHub&lt;/strong&gt;: The AI uses a GitHub MCP tool to search for new pull requests or recently committed code in a specific repository.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Read Code&lt;/strong&gt;: Once new code is identified, the AI uses the GitHub MCP tool to read the relevant files.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Analyze Patterns&lt;/strong&gt;: The AI then analyzes the code for potential bugs, security vulnerabilities, or deviations from coding standards.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Create Issue&lt;/strong&gt;: If issues are found, the AI uses the GitHub MCP tool to automatically create a new GitHub issue, detailing the problem, suggesting a fix, and assigning it to the relevant team.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This entire process, from detection to reporting, can be fully automated through MCP tool chaining.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Workflow 2: Data-Driven Reporting
&lt;/h2&gt;

&lt;p&gt;Another powerful application lies in data analysis and reporting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Query Database&lt;/strong&gt;: An AI is tasked with generating a weekly sales report. It uses a PostgreSQL MCP tool to query the sales database, fetching relevant data like product sales, regional performance, and customer demographics.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Analyze Data&lt;/strong&gt;: The AI processes the raw data, identifying trends, anomalies, and key insights.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Generate Report&lt;/strong&gt;: Based on its analysis, the AI uses its generative capabilities to draft a comprehensive report, including summaries, visualizations (if integrated with a charting tool), and strategic recommendations.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Post to Slack&lt;/strong&gt;: Finally, the AI uses a Slack MCP tool to post the generated report (or a summary of it) to the relevant team's channel, ensuring stakeholders are promptly informed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  NeuroLink: Orchestrating the AI Nervous System
&lt;/h2&gt;

&lt;p&gt;NeuroLink is designed to be the "pipe layer for the AI nervous system," specifically built to facilitate these complex, multi-tool AI workflows. It unifies over 13 major AI providers and provides a seamless way to connect and chain MCP tools.&lt;/p&gt;

&lt;p&gt;One of NeuroLink's key features is its &lt;code&gt;addExternalMCPServer&lt;/code&gt; method, which allows you to integrate any external tool or system as an MCP server. Once registered, NeuroLink enables the AI to automatically discover and chain these tools based on the task at hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Examples with NeuroLink API
&lt;/h3&gt;

&lt;p&gt;Here's how you might configure NeuroLink to connect to GitHub, a PostgreSQL database, and Slack, and then leverage them in an AI workflow.&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;NeuroLink&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;@juspay/neurolink&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;neurolink&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;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Add GitHub as an MCP server&lt;/span&gt;
&lt;span class="c1"&gt;// This enables AI to interact with GitHub for searching, reading, and creating issues/PRs.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;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;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="s2"&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="s2"&gt;@modelcontextprotocol/server-github&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stdio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Using stdio for local execution&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;GITHUB_TOKEN&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;GITHUB_TOKEN&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// Securely pass credentials&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Add PostgreSQL as an MCP server&lt;/span&gt;
&lt;span class="c1"&gt;// This allows AI to query and manipulate database records.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgres&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;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="s2"&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="s2"&gt;@modelcontextprotocol/server-postgres&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stdio&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;DATABASE_URL&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;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Add Slack as an MCP server&lt;/span&gt;
&lt;span class="c1"&gt;// This empowers AI to post messages, summaries, or reports to Slack channels.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;slack&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;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Assuming a remote Slack MCP server&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-mcp-slack-server.com/api&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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &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;SLACK_BOT_TOKEN&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="c1"&gt;// Now, the AI can chain these tools automatically based on the prompt:&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;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;input&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="s2"&gt;`
      1. Find all recently closed GitHub issues in the 'neurolink' repository related to 'performance'.
      2. Analyze the resolution steps and any associated code changes.
      3. Query the 'production_metrics' PostgreSQL database for performance data during the resolution period.
      4. Based on the analysis, draft a summary report on the effectiveness of the fixes and post it to the #engineering-updates Slack channel.
    `&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="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;span class="c1"&gt;// The AI's generated content, after tool execution&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, NeuroLink acts as the central orchestrator, receiving the high-level request, breaking it down into sub-tasks, identifying the appropriate MCP tools (GitHub, Postgres, Slack), executing them in sequence, and synthesizing the results. The AI agent, powered by NeuroLink, intelligently decides which tool to use at each step, forming a dynamic "tool chain."&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Tool Chains
&lt;/h2&gt;

&lt;p&gt;Building complex AI workflows inevitably involves debugging. Here are some tips when working with NeuroLink and MCP tool chains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Verbose Logging&lt;/strong&gt;: Enable verbose logging in NeuroLink to see the exact tool calls the AI makes, their inputs, and their outputs. This is crucial for understanding the AI's decision-making process.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Isolate Tools&lt;/strong&gt;: Test each MCP server independently to ensure it functions correctly before integrating it into a complex chain.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Step-by-Step Execution&lt;/strong&gt;: For difficult cases, use NeuroLink's interactive CLI (&lt;code&gt;neurolink loop&lt;/code&gt;) or &lt;code&gt;prepareStep&lt;/code&gt; feature to step through the AI's thought process and tool invocations.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Validate Inputs/Outputs&lt;/strong&gt;: Ensure that the output of one tool call correctly matches the expected input format for the next tool in the chain. Discrepancies here are a common source of errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance: ToolCache and RequestBatcher for Optimization
&lt;/h2&gt;

&lt;p&gt;As AI agents perform more complex tasks, performance becomes critical. NeuroLink offers built-in mechanisms to optimize tool chain execution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ToolCache&lt;/code&gt;&lt;/strong&gt;: This module allows NeuroLink to cache the results of frequently called, idempotent tools. If an AI requests the same data (e.g., a file content from GitHub) multiple times within a short period, &lt;code&gt;ToolCache&lt;/code&gt; can serve the result from memory instead of re-executing the tool, significantly reducing latency and API costs. You can configure various caching strategies like LRU, FIFO, or LFU.&lt;br&gt;
&lt;/p&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;ToolCache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NeuroLink&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;@juspay/neurolink&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;cache&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;ToolCache&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lru&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Cache for 5 minutes&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;neurolink&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;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ... other config&lt;/span&gt;
  &lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;toolCache&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="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;RequestBatcher&lt;/code&gt;&lt;/strong&gt;: For tools that can process multiple requests efficiently in a single call (e.g., querying a database for several items), &lt;code&gt;RequestBatcher&lt;/code&gt; automatically groups concurrent tool calls into a single batch request. This reduces the overhead of individual API calls, improving throughput.&lt;br&gt;
&lt;/p&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;RequestBatcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NeuroLink&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;@juspay/neurolink&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;batcher&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;RequestBatcher&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;maxBatchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxWaitMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Batch up to 10 requests or wait 50ms&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;neurolink&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;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ... other config&lt;/span&gt;
  &lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;requestBatcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;batcher&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;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By intelligently applying caching and batching, NeuroLink ensures that your AI workflows remain performant and cost-effective, even when interacting with numerous external systems.&lt;/p&gt;

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

&lt;p&gt;MCP tool chaining with NeuroLink unlocks a new frontier in AI capabilities. By providing a robust framework for connecting and orchestrating diverse tools, NeuroLink empowers developers to build sophisticated AI agents that can search, read, analyze, and write across virtually any digital system. This ability to chain operations fundamentally transforms how AI can be integrated into real-world applications, paving the way for truly autonomous and intelligent workflows.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;NeuroLink — The Universal AI SDK for TypeScript&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/juspay/neurolink" rel="noopener noreferrer"&gt;github.com/juspay/neurolink&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install: &lt;code&gt;npm install @juspay/neurolink&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.neurolink.ink" rel="noopener noreferrer"&gt;docs.neurolink.ink&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Blog: &lt;a href="https://blog.neurolink.ink" rel="noopener noreferrer"&gt;blog.neurolink.ink&lt;/a&gt; — 150+ technical articles&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>I built a faster alternative to cp and rsync — here's how it works</title>
      <dc:creator>krit.k83 (ΚρητικόςIGB)</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:27:08 +0000</pubDate>
      <link>https://vibe.forem.com/krit83/i-built-a-faster-alternative-to-cp-and-rsync-heres-how-it-works-39fa</link>
      <guid>https://vibe.forem.com/krit83/i-built-a-faster-alternative-to-cp-and-rsync-heres-how-it-works-39fa</guid>
      <description>&lt;p&gt;I'm a systems engineer. I spend a lot of time copying files — backups to USB drives, transfers to NAS boxes, moving data between servers over SSH. And I kept running into the same frustrations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cp -r&lt;/code&gt; is painfully slow on HDDs when you have tens of thousands of small files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rsync&lt;/code&gt; is powerful but complex, and still slow for bulk copies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scp&lt;/code&gt; and SFTP top out at 1-2 MB/s on transfers that should be much faster&lt;/li&gt;
&lt;li&gt;No tool tells you upfront if the destination even has enough space&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;strong&gt;fast-copy&lt;/strong&gt; — a Python CLI that copies files at maximum sequential disk speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;cp -r&lt;/code&gt;, files are read in directory order — which is essentially random on disk. Every file seek on an HDD costs 5-10ms. Multiply that by 60,000 files and you're spending minutes just on head movement.&lt;/p&gt;

&lt;p&gt;fast-copy does something different: it resolves the physical disk offset of every file before copying. On Linux it uses &lt;code&gt;FIEMAP&lt;/code&gt;, on macOS &lt;code&gt;fcntl&lt;/code&gt;, on Windows &lt;code&gt;FSCTL&lt;/code&gt;. Then it sorts files by block position and reads them sequentially.&lt;/p&gt;

&lt;p&gt;That alone makes a big difference. But there's more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deduplication
&lt;/h2&gt;

&lt;p&gt;Many directories have duplicate files — node_modules across projects, cached downloads, backup copies. fast-copy hashes every file with xxHash-128 (or SHA-256 as fallback), copies each unique file once, and creates hard links for duplicates.&lt;/p&gt;

&lt;p&gt;In my test with 92K files, over half were duplicates — saving 379 MB and a lot of I/O time.&lt;/p&gt;

&lt;p&gt;It also keeps a SQLite database of hashes, so repeated copies to the same destination skip files that were already copied in previous runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSH tar streaming
&lt;/h2&gt;

&lt;p&gt;This is the part I'm most proud of. Instead of using SFTP (which has significant protocol overhead), fast-copy streams files as chunked ~100 MB tar batches over raw SSH channels.&lt;/p&gt;

&lt;p&gt;The remote side runs &lt;code&gt;tar xf -&lt;/code&gt; and files land directly on disk — no temp files, no SFTP overhead. This even works on servers that have SFTP disabled, like some Synology NAS configurations.&lt;/p&gt;

&lt;p&gt;Three modes are supported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local → Remote&lt;/li&gt;
&lt;li&gt;Remote → Local&lt;/li&gt;
&lt;li&gt;Remote → Remote (relay through your machine)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real benchmarks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Local copy — 92K files to USB:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;44,718 unique files copied + 47,146 hard-linked&lt;/li&gt;
&lt;li&gt;509.8 MB written, 378.9 MB saved by dedup&lt;/li&gt;
&lt;li&gt;17.9 seconds, 28.5 MB/s&lt;/li&gt;
&lt;li&gt;All files verified after copy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Remote to local — 92K files over LAN:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;509.8 MB downloaded in 14 minutes&lt;/li&gt;
&lt;li&gt;46,951 duplicates detected, saving 378.5 MB of transfer&lt;/li&gt;
&lt;li&gt;3x faster than SFTP&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;The simplest way — just run the Python script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python fast_copy.py /source /destination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or download a standalone binary (no Python needed) from the Releases page — available for Linux, macOS, and Windows.&lt;/p&gt;

&lt;p&gt;For SSH transfers, install paramiko:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;paramiko
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For faster hashing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;xxhash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/gekap/fast-copy" rel="noopener noreferrer"&gt;https://github.com/gekap/fast-copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Apache 2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love to hear feedback — especially from anyone dealing with large file transfers or backup workflows. What tools are you currently using? What's missing from them?&lt;/p&gt;




</description>
      <category>python</category>
      <category>linux</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Chaining MCP Tools: Search Read Analyze Write in TypeScript</title>
      <dc:creator>NeuroLink AI</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:26:55 +0000</pubDate>
      <link>https://vibe.forem.com/neurolink/chaining-mcp-tools-search-read-analyze-write-in-typescript-2gmb</link>
      <guid>https://vibe.forem.com/neurolink/chaining-mcp-tools-search-read-analyze-write-in-typescript-2gmb</guid>
      <description>&lt;h1&gt;
  
  
  Chaining MCP Tools: Search → Read → Analyze → Write in TypeScript
&lt;/h1&gt;

&lt;p&gt;Building sophisticated AI agents requires more than simple prompt-response interactions. Real-world automation demands multi-step workflows where AI performs sequential operations—gathering information, processing it, and taking action. This is where NeuroLink's Model Context Protocol (MCP) tool chaining capabilities transform what's possible with TypeScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anatomy of a Tool Chain
&lt;/h2&gt;

&lt;p&gt;Consider a typical developer workflow: searching GitHub for relevant code, reading files, analyzing patterns, and creating issues. Previously, this required orchestrating multiple API calls, managing state, and handling errors across different services. NeuroLink unifies these operations into a single, coherent AI workflow.&lt;/p&gt;

&lt;p&gt;Let's build a code review agent that chains MCP tools:&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;NeuroLink&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;@juspay/neurolink&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;neurolink&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;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Connect GitHub MCP server&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;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;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="s2"&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="s2"&gt;@modelcontextprotocol/server-github&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stdio&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;GITHUB_TOKEN&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;GITHUB_TOKEN&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Connect code analysis MCP server&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;code-analyzer&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;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.codeanalysis.tools/mcp&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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer YOUR_API_KEY&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="c1"&gt;// Execute a multi-step workflow&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;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;input&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="s2"&gt;`Search the "acme-corp/payments" repo for all files using "processPayment()"
           function, analyze the code for security vulnerabilities, and create a GitHub
           issue titled "Security Review: processPayment() usage" with findings.`&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-4-sonnet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// NeuroLink orchestrates the chain:&lt;/span&gt;
&lt;span class="c1"&gt;// 1. Calls github.search_code()&lt;/span&gt;
&lt;span class="c1"&gt;// 2. Calls github.read_file() for each result&lt;/span&gt;
&lt;span class="c1"&gt;// 3. Calls code-analyzer.analyze() on content&lt;/span&gt;
&lt;span class="c1"&gt;// 4. Calls github.create_issue() with findings&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Understanding the Chain
&lt;/h2&gt;

&lt;p&gt;The beauty of NeuroLink's approach is that the LLM decides the optimal sequence of tool calls. When given a complex task, the AI breaks it down into discrete steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1: Search
  └─ MCP Tool: github.search_code
  └─ Query: "processPayment repo:acme-corp/payments"
  └─ Result: 5 files found

Step 2: Read
  ├─ MCP Tool: github.read_file (file 1)
  ├─ MCP Tool: github.read_file (file 2)
  └─ ... (parallel execution)

Step 3: Analyze
  └─ MCP Tool: code-analyzer.analyze
  └─ Input: Combined file contents
  └─ Result: 3 security findings

Step 4: Write
  └─ MCP Tool: github.create_issue
  └─ Title: "Security Review: processPayment() usage"
  └─ Body: Formatted analysis with code snippets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building Intelligent Routers
&lt;/h2&gt;

&lt;p&gt;For production applications, you need more than basic tool execution. NeuroLink provides a &lt;code&gt;ToolRouter&lt;/code&gt; for intelligent routing across multiple MCP servers:&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;ToolRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ToolCache&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;@juspay/neurolink&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Route calls based on capability matching&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;capability-based&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;servers&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;primary-db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://mcp-primary.db.internal/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;capabilities&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;query&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;transaction&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="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="s2"&gt;analytics-db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://mcp-analytics.db.internal/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;capabilities&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;aggregate&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;report&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="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="s2"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://mcp-github.tools.internal/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;capabilities&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;repo&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;issue&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;pr&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="c1"&gt;// Cache expensive operations&lt;/span&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="nc"&gt;ToolCache&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lru&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Use in your NeuroLink instance&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configureMCP&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="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;batcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxBatchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxWaitMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&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;
  
  
  Human-in-the-Loop for Critical Actions
&lt;/h2&gt;

&lt;p&gt;Not all tool chains should execute autonomously. NeuroLink's HITL (Human-in-the-Loop) system ensures human approval for sensitive operations:&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;neurolink&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;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;hitl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;enabled&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;requireApproval&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;github.create_issue&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;github.merge_pr&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;db.execute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;confidenceThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reviewCallback&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;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&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="c1"&gt;// Send to Slack for approval&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;approval&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;slack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendApprovalRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;action&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="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversationSummary&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;approval&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;approved&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;// The AI will pause before creating issues&lt;/span&gt;
&lt;span class="c1"&gt;// and wait for human approval&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;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;input&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create a critical bug report for the auth module&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;
  
  
  Real-World Example: Documentation Sync Agent
&lt;/h2&gt;

&lt;p&gt;Here's a complete implementation that keeps documentation synchronized with 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;NeuroLink&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;@juspay/neurolink&lt;/span&gt;&lt;span class="dl"&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;createDocSyncAgent&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;neurolink&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;NeuroLink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Add required MCP servers&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;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;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="s2"&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="s2"&gt;@modelcontextprotocol/server-github&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stdio&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;GITHUB_TOKEN&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;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;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addExternalMCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notion&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;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.notion.com/mcp&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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &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;NOTION_TOKEN&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="c1"&gt;// The agent workflow&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;syncDocumentation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&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;notionPage&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;neurolink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;input&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="s2"&gt;`Sync API documentation between GitHub repo "&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;" and Notion page "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;notionPage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;".

               Steps:
               1. Search &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; for all files ending in "API.ts" or containing "@api" JSDoc
               2. Read each API file and extract endpoint definitions
               3. Compare with existing content in Notion page &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;notionPage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
               4. Update Notion with any new endpoints or changes
               5. Create a summary of updates made`&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-4-sonnet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hitl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&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;requireApproval&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;notion.update_page&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;syncDocumentation&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&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;await&lt;/span&gt; &lt;span class="nf"&gt;createDocSyncAgent&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;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;syncDocumentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acme-corp/payments-api&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;api-documentation-2024&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;h2&gt;
  
  
  Best Practices for Tool Chaining
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Design for composability&lt;/strong&gt;: Build small, focused MCP servers that do one thing well&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use result caching&lt;/strong&gt;: Expensive operations like code analysis should be cached&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement HITL for mutations&lt;/strong&gt;: Always require approval for state-changing operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle failures gracefully&lt;/strong&gt;: Design chains that can retry or skip failed steps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor execution&lt;/strong&gt;: Use NeuroLink's telemetry to track tool usage and latency&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;MCP tool chaining with NeuroLink transforms AI agents from chatbots into autonomous workers capable of complex, multi-step workflows. By combining search, read, analyze, and write operations through a unified interface, you can automate sophisticated developer workflows while maintaining control and visibility.&lt;/p&gt;

&lt;p&gt;The future of AI isn't just smarter models—it's smarter orchestration of tools that work together seamlessly.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;NeuroLink — The Universal AI SDK for TypeScript&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/juspay/neurolink" rel="noopener noreferrer"&gt;github.com/juspay/neurolink&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install: &lt;code&gt;npm install @juspay/neurolink&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.neurolink.ink" rel="noopener noreferrer"&gt;docs.neurolink.ink&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Blog: &lt;a href="https://blog.neurolink.ink" rel="noopener noreferrer"&gt;blog.neurolink.ink&lt;/a&gt; — 150+ technical articles&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Service Layer: Where Separate Components Become a System</title>
      <dc:creator>Ozioma Ochin</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:24:51 +0000</pubDate>
      <link>https://vibe.forem.com/oozioma/the-service-layer-where-separate-components-become-a-system-4oeh</link>
      <guid>https://vibe.forem.com/oozioma/the-service-layer-where-separate-components-become-a-system-4oeh</guid>
      <description>&lt;p&gt;This is Part 4 of a series building a production-ready semantic search API with Java, Spring Boot, and pgvector.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/oozioma/building-a-semantic-search-api-with-spring-boot-and-pgvector-part-1-architecture-58b9"&gt;Part 1&lt;/a&gt; covered the architecture. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/oozioma/building-a-semantic-search-api-with-spring-boot-and-pgvector-part-2-designing-the-postgresql-2jlb"&gt;Part 2&lt;/a&gt; defined the schema. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/oozioma/building-a-semantic-search-api-with-spring-boot-and-pgvector-part-3-the-embedding-layer-1pj0"&gt;Part 3&lt;/a&gt; handled the embeddings — how text becomes vectors.&lt;/p&gt;

&lt;p&gt;Each piece worked in isolation. &lt;/p&gt;

&lt;p&gt;But systems don't fail in isolation — they fail at the boundaries.&lt;/p&gt;

&lt;p&gt;If you've ever built a feature that worked perfectly on its own but broke the moment you connected it to everything else — this article is about preventing that.&lt;/p&gt;

&lt;p&gt;At this point, we have a schema that can store documents and an embedding layer that can generate vectors. &lt;/p&gt;

&lt;p&gt;But nothing connects them. A document has nowhere to go. A query has no pipeline.&lt;/p&gt;

&lt;p&gt;This is where the service layer comes in.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;This is a production-style implementation — not a demo. The full project structure, tests, and configuration are available on &lt;a href="https://github.com/buenas/-semantic-search-service" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What Does the Service Layer Actually Do?
&lt;/h3&gt;

&lt;p&gt;The database stores state, but it doesn't understand it. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;PENDING&lt;/code&gt;, &lt;code&gt;READY&lt;/code&gt;, and &lt;code&gt;FAILED&lt;/code&gt; only become meaningful once the service layer defines when those transitions happen and what triggers them.&lt;/p&gt;

&lt;p&gt;When a document arrives, the service decides the order of operations — save first, embed second, update on success, record failure explicitly if something goes wrong.&lt;/p&gt;

&lt;p&gt;Search follows the same pattern. A query doesn't go straight to the database. It's first converted into an embedding, then passed through a query that applies lifecycle constraints, metadata filters, and scoring thresholds. &lt;/p&gt;

&lt;p&gt;The service layer controls that entire pipeline.&lt;/p&gt;

&lt;p&gt;The service layer owns one thing: &lt;em&gt;&lt;strong&gt;the rules that make the system predictable.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Without it, the system is just a collection of correct but disconnected components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP Request
     │
     ▼
Controller Layer       ← validates input, delegates to service
     │
     ▼
Service Layer          ← all decisions happen here
     │                    │
     ▼                    ▼
Repository Layer      Embedding Layer
(JPA + JdbcTemplate)  (EmbeddingClient interface)
     │                    │
     ▼                    ▼
PostgreSQL + pgvector  OpenAI API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Interface That Keeps Everything Clean
&lt;/h3&gt;

&lt;p&gt;The service layer exposes one interface to the rest of the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;DocumentService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;CreateDocumentResponse&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateDocumentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;DocumentResponse&lt;/span&gt; &lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;SearchResponse&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Controllers depend on the interface, not the implementation. &lt;/p&gt;

&lt;p&gt;Defining the contract as an interface and hiding the implementation behind it is what makes the system testable and changeable without cascading updates across the codebase.&lt;/p&gt;

&lt;p&gt;The more important detail is what does not cross this boundary.&lt;/p&gt;

&lt;p&gt;The Document entity never crosses this boundary — by design. Controllers receive &lt;code&gt;DTOs&lt;/code&gt;, not persistence objects. &lt;/p&gt;

&lt;p&gt;That separation means the database schema and the API contract can evolve independently. The schema can change without breaking clients. The API can change without rewriting persistence logic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Why this matters to you:&lt;/strong&gt; If you've ever had a database change break your API — or an API change force a database rewrite — this boundary is what prevents that. Define it early and hold it firmly.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What Happens When Embedding Fails?
&lt;/h3&gt;

&lt;p&gt;From the outside, creating a document looks simple. Send a document, get an ID back.&lt;/p&gt;

&lt;p&gt;Inside the service, everything is built around one assumption: the second step might fail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CreateDocumentResponse&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateDocumentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Document&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;saveAsPending&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;embedAndPersist&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTitle&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateDocumentResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;DocumentStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;READY&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines, two distinct operations.&lt;/p&gt;

&lt;p&gt;The first saves the document immediately with a status of &lt;code&gt;PENDING&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The document exists in the database before any embedding call is made. &lt;/p&gt;

&lt;p&gt;If the application crashes at this point, the document is already there with a recoverable state.&lt;/p&gt;

&lt;p&gt;The second calls the OpenAI API, generates the embedding, and updates the document to &lt;code&gt;READY&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;If this step fails, the document moves to &lt;code&gt;FAILED&lt;/code&gt; instead, and the error is stored directly in the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /documents
      │
      ▼
saveAsPending()
status = PENDING ← document is safe in the database
      │
      ▼
embedAndPersist()
      │
   ┌──┴──────────────┐
   │                 │
   ▼                 ▼
status = READY   status = FAILED
searchable       error stored in DB
                 excluded from search
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's an alternative that looks simpler — embed first, then save. &lt;/p&gt;

&lt;p&gt;It removes a step but removes visibility. If embedding fails in that model, the document never exists. There's no record, no state, nothing to debug. &lt;/p&gt;

&lt;p&gt;By saving first, every attempt leaves a trace. &lt;/p&gt;

&lt;p&gt;Failures don't disappear. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They become data.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This pattern — save first, embed second — is the difference between a failure you can debug and one that just disappears.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's how the failure handling actually works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;embedAndPersist&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddingClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"\n\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SQL_UPDATE_EMBEDDING&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;toPgVectorLiteral&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Unexpected row count updating embedding for document id="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IllegalStateException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;markFailed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Embedding failed for document id="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three decisions here worth understanding:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Title and content are concatenated for embedding. &lt;code&gt;title + "\n\n" + content&lt;/code&gt; gives the model full context. A document titled "Payment Failure Handling Policy" with content about retry logic produces a richer embedding than the content alone.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;IllegalStateException&lt;/code&gt; is re-thrown unchanged. If the update affects zero or more than one row, something is wrong with the database state — not the embedding call. That error should propagate as-is rather than being wrapped as an embedding failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Everything else triggers &lt;code&gt;markFailed&lt;/code&gt;. Network timeouts, rate limits, malformed responses — any exception that isn't an &lt;code&gt;IllegalStateException&lt;/code&gt; records the failure and re-throws. The caller sees the failure. The database gets a record of what went wrong.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Most API integration failures are silent. This makes them loud.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Search — The Pipeline That Ties Everything Together
&lt;/h3&gt;

&lt;p&gt;Search is the most complex operation in the service. It touches the embedding layer, the repository, and the database — and it has to coordinate all three correctly.&lt;/p&gt;

&lt;p&gt;What makes it manageable is not reducing that complexity, but containing it deliberately.&lt;/p&gt;

&lt;p&gt;The orchestration method is deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SearchResponse&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;qVector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getQuery&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchResultItem&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetchResults&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;qVector&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;countResults&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;qVector&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFilters&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMinScore&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SearchResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPage&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSize&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;items&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four lines. Each delegates to a private method with a clear name. &lt;/p&gt;

&lt;p&gt;The method reads like a description of the search process — embed the query, fetch the results, count the total, return the response. &lt;/p&gt;

&lt;p&gt;The how is pushed down into methods that can be reasoned about in isolation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;embedQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; 
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;toPgVectorLiteral&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddingClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; 
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query goes through the same embedding client used for documents. &lt;/p&gt;

&lt;p&gt;That symmetry matters — the query and the stored documents exist in the same vector space. Without it, similarity search would be meaningless.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;SQL&lt;/code&gt; is constructed in two layers: the inner query selects candidates and computes similarity, while the outer query applies score thresholds and pagination.&lt;/p&gt;

&lt;p&gt;The split isn't stylistic. PostgreSQL cannot reference a &lt;code&gt;SELECT&lt;/code&gt; alias in a &lt;code&gt;WHERE&lt;/code&gt; clause at the same query level — which is why &lt;code&gt;cosine_distance&lt;/code&gt; must be resolved in a subquery before the score threshold can filter on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cosine_distance&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'READY'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cosine_distance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cosine_distance&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;If you've ever wondered why your JPA queries feel limiting for complex use cases — this is where you cross that line deliberately.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why JPA Isn’t Enough for Vector Search
&lt;/h3&gt;

&lt;p&gt;The search query isn't static. &lt;/p&gt;

&lt;p&gt;Metadata filters, score thresholds, and pagination all change the SQL at runtime. &lt;/p&gt;

&lt;p&gt;At that point the abstraction provided by JPA starts to break down — you're no longer mapping objects, you're constructing a query.&lt;/p&gt;

&lt;p&gt;That's where QueryBuilder comes in:&lt;br&gt;
&lt;/p&gt;

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

   &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;StringBuilder&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
   &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

   &lt;span class="nc"&gt;QueryBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;baseSql&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;firstParam&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sql&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;StringBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseSql&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
       &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firstParam&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;

   &lt;span class="nc"&gt;QueryBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;baseSql&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;QueryBuilder&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sql&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;StringBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseSql&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
       &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two constructors mirror the structure of the query – inner and outer. &lt;/p&gt;

&lt;p&gt;The first builds the inner query. &lt;/p&gt;

&lt;p&gt;The second builds the outer query, inheriting parameters from the inner one without tracking them manually. &lt;/p&gt;

&lt;p&gt;Where injection risk actually lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;applyFilters&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&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="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Entry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;entrySet&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getKey&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"^[a-zA-Z0-9_-]{1,64}$"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
           &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid metadata filter key: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
       &lt;span class="o"&gt;}&lt;/span&gt;

       &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  AND (metadata-&amp;gt;&amp;gt;'"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"') = ?\n"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
       &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filter key is appended directly into the &lt;code&gt;SQL&lt;/code&gt; string. &lt;code&gt;SQL&lt;/code&gt; doesn't allow placeholders for column names or &lt;code&gt;JSON&lt;/code&gt; path expressions — which means this is where injection risk enters the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The regex is not a convenience. It is the only control point between user input and the database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;^[a-zA-Z0-9_-]{1,64}$&lt;/code&gt; — only alphanumeric characters, underscores, and hyphens. &lt;/p&gt;

&lt;p&gt;Anything else is rejected before it reaches the database. Filter values, on the other hand, always go through JDBC parameters and are safe regardless of input. &lt;/p&gt;

&lt;p&gt;This split — validated keys, parameterised values — is what makes the query both flexible and secure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This is one of those cases where the 'boring' regex is doing serious security work. Don't skip it.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Key validation handles injection risk. The other challenge in query construction is where to apply the score threshold.&lt;/p&gt;

&lt;p&gt;Score filtering is applied on the outer query — not the inner one. &lt;code&gt;cosine_distance&lt;/code&gt; is defined in the inner query's &lt;code&gt;SELECT&lt;/code&gt; clause. &lt;/p&gt;

&lt;p&gt;PostgreSQL cannot reference that alias in a &lt;code&gt;WHERE&lt;/code&gt; clause at the same level. Wrapping it as a subquery makes it a real column in the outer scope — which is what allows &lt;code&gt;minScore&lt;/code&gt; to work at all.&lt;/p&gt;

&lt;p&gt;This is the point where you stop “using an ORM” and start designing queries deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating a Document means Updating Its Embedding Too
&lt;/h3&gt;

&lt;p&gt;Updating a document is not the same as updating a database row.&lt;/p&gt;

&lt;p&gt;When content changes, the stored embedding becomes stale. A document about "payment retry logic" gets updated to "refund processing." &lt;/p&gt;

&lt;p&gt;But the embedding still points toward payment retries. Searches for "refund policy" would miss it. Searches for "payment retries" would still find it — incorrectly.&lt;/p&gt;

&lt;p&gt;The update operation handles this explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;applyUpdates&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpdateDocumentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTitle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTitle&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setContent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMetadata&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMetadata&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DocumentStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PENDING&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEmbeddingError&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="n"&gt;documentRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment content changes, the embedding becomes invalid. &lt;/p&gt;

&lt;p&gt;The system makes that explicit by resetting the document to &lt;code&gt;PENDING&lt;/code&gt;, removing it from search until a new embedding is generated.&lt;/p&gt;

&lt;p&gt;This trades availability for correctness — a document disappearing briefly is preferable to returning incorrect results.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;findOrThrow&lt;/code&gt; is called again after &lt;code&gt;embedAndPersist&lt;/code&gt; so the response reflects the document's final state — including the updated status and &lt;code&gt;embeddingUpdatedAt&lt;/code&gt; timestamp — not the state before the embedding ran.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This is easy to miss when you first build it. If a document update doesn't trigger a re-embed, your search results will silently drift out of sync with your content.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  One Place for All Your Errors
&lt;/h3&gt;

&lt;p&gt;Errors in this system fall into two categories — errors the caller caused and errors the system encountered. &lt;/p&gt;

&lt;p&gt;Those two cases should not look the same.&lt;/p&gt;

&lt;p&gt;A missing document returns a &lt;code&gt;404&lt;/code&gt;. Invalid input returns a &lt;code&gt;400&lt;/code&gt;. An embedding failure returns a &lt;code&gt;500&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What matters more than the distinction is consistency — every error, regardless of where it originates, returns the same shape:&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Document not found: 42"&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;That consistency is enforced in one place — &lt;code&gt;GlobalExceptionHandler&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

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

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ResourceNotFoundException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleNotFound&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;ResourceNotFoundException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&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;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                        &lt;span class="s"&gt;"NOT_FOUND"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MethodArgumentNotValidException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleValidation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;MethodArgumentNotValidException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBindingResult&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFieldErrors&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getField&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;": "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultMessage&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;joining&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&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;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                        &lt;span class="s"&gt;"VALIDATION_ERROR"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;message&lt;/span&gt;
                &lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleGeneral&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&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;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                        &lt;span class="s"&gt;"INTERNAL_ERROR"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                        &lt;span class="s"&gt;"An unexpected error occurred"&lt;/span&gt;
                &lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@RestControllerAdvice&lt;/code&gt; annotation makes it active across all controllers without being wired into any of them. &lt;/p&gt;

&lt;p&gt;The service layer throws exceptions. The handler translates them. The controllers never see error handling code.&lt;/p&gt;

&lt;p&gt;A client that always receives code and message can handle all errors with one piece of logic. &lt;/p&gt;

&lt;p&gt;A client that receives different shapes from different endpoints has to handle each one separately.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;One handler, consistent responses everywhere — your frontend team will thank you.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How the LifecycleKeeps Bad Data Out of Search
&lt;/h3&gt;

&lt;p&gt;The document lifecycle isn't just about tracking failures. It's what keeps invalid data out of search results entirely.&lt;/p&gt;

&lt;p&gt;Every search query filters on two conditions before any similarity calculation runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'READY'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;PENDING&lt;/code&gt; document is excluded. A &lt;code&gt;FAILED&lt;/code&gt; document is excluded.&lt;/p&gt;

&lt;p&gt;This is where the schema design from Part 2 pays off — the composite index on &lt;code&gt;(status, created_at DESC)&lt;/code&gt; exists specifically to support this filtering pattern. &lt;/p&gt;

&lt;p&gt;Without it, every search scans the full table and discards non-ready documents. With it, PostgreSQL jumps directly to the relevant subset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING ──────────────────────────────┐
   │                                  │
   ▼                                  │
embedAndPersist()                     │
   │                                  │
┌──┴──────────────┐                   │
│                 │                   │
▼                 ▼                   ▼
READY          FAILED            not searchable
searchable     error in DB
               not searchable

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

&lt;/div&gt;



&lt;p&gt;The lifecycle isn't just about correctness. It's a performance optimization.&lt;/p&gt;

&lt;p&gt;If you've ever had stale or incomplete data show up in search results with no explanation — a lifecycle model like this is what prevents it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The System Now Works
&lt;/h3&gt;

&lt;p&gt;With the service layer in place, the system finally behaves like a system.&lt;/p&gt;

&lt;p&gt;A document arrives at &lt;code&gt;POST /documents&lt;/code&gt;. The controller validates the request and delegates to the service. &lt;/p&gt;

&lt;p&gt;The service saves the document as &lt;code&gt;PENDING&lt;/code&gt;, calls the embedding client, and updates the status to &lt;code&gt;READY&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The document is now stored with a valid embedding and visible to search.&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%2Fmo1qdgrtffcgltmbhjky.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%2Fmo1qdgrtffcgltmbhjky.png" alt="Search and Post"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A search query arrives at &lt;code&gt;POST /search&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The service embeds the query, builds the SQL dynamically through &lt;code&gt;QueryBuilder&lt;/code&gt;, applies filters and score thresholds, and returns ranked results with three score fields — &lt;code&gt;cosineDistance&lt;/code&gt;, &lt;code&gt;cosineSimilarity&lt;/code&gt;, and &lt;code&gt;score&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every layer has exactly one job. Every failure is visible. Every response has a consistent shape.&lt;/p&gt;

&lt;p&gt;The system that started as a schema and an embedding client in &lt;a href="https://dev.to/oozioma/building-a-semantic-search-api-with-spring-boot-and-pgvector-part-1-architecture-58b9"&gt;Part 1&lt;/a&gt; is now a complete, working API.&lt;/p&gt;

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

&lt;p&gt;The service layer completes the system. Everything now works end to end.&lt;/p&gt;

&lt;p&gt;But working systems still have flaws.&lt;/p&gt;

&lt;p&gt;In the next article, I’ll step back from the implementation and break down what this system gets right, what it gets wrong, and what I would change if I were to build it again.&lt;/p&gt;

&lt;p&gt;See you there.&lt;/p&gt;

</description>
      <category>java</category>
      <category>vectordatabase</category>
      <category>springboot</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Understanding Data Modelling in Power BI: Joins, Relationships, and Schemas Explained</title>
      <dc:creator>Dalton Imbiru</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:22:55 +0000</pubDate>
      <link>https://vibe.forem.com/dalton_imbiru_82680ef8a50/understanding-data-modelling-in-power-bi-joins-relationships-and-schemas-explained-51gf</link>
      <guid>https://vibe.forem.com/dalton_imbiru_82680ef8a50/understanding-data-modelling-in-power-bi-joins-relationships-and-schemas-explained-51gf</guid>
      <description>&lt;p&gt;Data modelling is the foundation of effective data analysis in Power BI. A well-structured model ensures faster performance, accurate calculations, and easier report building. This article breaks down everything you need to know—from SQL joins and Power BI relationships to schemas and practical implementation steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Data Modelling?
&lt;/h2&gt;

&lt;p&gt;Data modelling is the process of organising data into tables and defining how those tables relate to each other so that analysis becomes meaningful and efficient.&lt;/p&gt;

&lt;p&gt;In Power BI, data modeling involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Structuring tables (Fact and Dimension)&lt;/li&gt;
&lt;li&gt; Defining relationships between tables&lt;/li&gt;
&lt;li&gt; Optimizing performance and usability&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  SQL Joins Explained (With Examples)
&lt;/h2&gt;

&lt;p&gt;Joins combine data from two or more tables based on a common column.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customers&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;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Alex&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Orders&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;OrderID&lt;/th&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  INNER JOIN
&lt;/h2&gt;

&lt;p&gt;Returns only matching records from both tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&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;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; When you only want valid matches (e.g., customers who made purchases).&lt;/p&gt;

&lt;h2&gt;
  
  
  LEFT JOIN
&lt;/h2&gt;

&lt;p&gt;Returns all records from the left table + matching from the right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&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;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Alex&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Show all customers, even those without orders.&lt;/p&gt;

&lt;h2&gt;
  
  
  RIGHT JOIN
&lt;/h2&gt;

&lt;p&gt;Returns all records from the right table + matching from the left.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&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;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Show all orders, even if customer info is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  FULL OUTER JOIN
&lt;/h2&gt;

&lt;p&gt;Returns all records from both tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&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;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;OrderID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Mary&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Alex&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Complete data reconciliation.&lt;/p&gt;

&lt;h2&gt;
  
  
  LEFT ANTI JOIN
&lt;/h2&gt;

&lt;p&gt;Returns rows from left table that have &lt;strong&gt;no match&lt;/strong&gt; in right table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&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;CustomerID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Alex&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Identify customers who never purchased.&lt;/p&gt;

&lt;h2&gt;
  
  
  RIGHT ANTI JOIN
&lt;/h2&gt;

&lt;p&gt;Returns rows from right table with &lt;strong&gt;no match&lt;/strong&gt; in left table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&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;OrderID&lt;/th&gt;
&lt;th&gt;CustomerID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Identify orphan records (e.g., invalid orders).&lt;/p&gt;

&lt;h2&gt;
  
  
  Joins in Power BI (Power Query)
&lt;/h2&gt;

&lt;p&gt;Power BI implements joins in &lt;strong&gt;Power Query&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Home → Transform Data&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select a table&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Merge Queries&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select second table&lt;/li&gt;
&lt;li&gt;Choose matching column(s)&lt;/li&gt;
&lt;li&gt;Select join type:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Inner&lt;/li&gt;
&lt;li&gt;Left Outer&lt;/li&gt;
&lt;li&gt;Right Outer&lt;/li&gt;
&lt;li&gt;Full Outer&lt;/li&gt;
&lt;li&gt;Left Anti&lt;/li&gt;
&lt;li&gt;Right Anti

&lt;ol&gt;
&lt;li&gt;Expand columns to finalize&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Relationships in Power BI
&lt;/h2&gt;

&lt;p&gt;Unlike SQL joins (which combine tables physically), Power BI relationships connect tables logically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of Relationships
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. One-to-Many (1:M)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;One record in Table A → Many in Table B&lt;/li&gt;
&lt;li&gt;Example: Customers → Orders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most common relationship&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Many-to-Many (M:M)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Many records in both tables&lt;/li&gt;
&lt;li&gt;Example: Students ↔ Courses&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. One-to-One (1:1)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;One record matches exactly one record&lt;/li&gt;
&lt;li&gt;Example: Employee ↔ Employee Details&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cardinality
&lt;/h2&gt;

&lt;p&gt;Defines how tables relate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-to-Many&lt;/li&gt;
&lt;li&gt;Many-to-One&lt;/li&gt;
&lt;li&gt;Many-to-Many&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cross-Filter Direction
&lt;/h2&gt;

&lt;p&gt;Controls how filters flow between tables.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single direction&lt;/strong&gt; → One way (recommended)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both directions&lt;/strong&gt; → Two-way filtering (use carefully)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Active vs Inactive Relationships
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Active relationship&lt;/strong&gt; → Default used in visuals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inactive relationship&lt;/strong&gt; → Exists but is not used unless activated via DAX (&lt;code&gt;USERELATIONSHIP&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Order Date (Active)&lt;/li&gt;
&lt;li&gt;Ship Date (Inactive)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Creating Relationships in Power BI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method 1: Model View
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Model View&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Drag one column to another&lt;/li&gt;
&lt;li&gt;Relationship is created automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Method 2: Manage Relationships
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Home → Manage Relationships&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Tables&lt;/li&gt;
&lt;li&gt;Columns&lt;/li&gt;
&lt;li&gt;Cardinality&lt;/li&gt;
&lt;li&gt;Cross-filter direction

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;OK&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Joins vs Relationships (Key Difference)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Joins&lt;/th&gt;
&lt;th&gt;Relationships&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where used&lt;/td&gt;
&lt;td&gt;Power Query&lt;/td&gt;
&lt;td&gt;Data Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;Combines tables&lt;/td&gt;
&lt;td&gt;Keeps tables separate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Can increase size&lt;/td&gt;
&lt;td&gt;More efficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flexibility&lt;/td&gt;
&lt;td&gt;Static&lt;/td&gt;
&lt;td&gt;Dynamic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Fact vs Dimension Tables
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Fact Table
&lt;/h2&gt;

&lt;p&gt;Contains measurable data (numbers)&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Sales&lt;/li&gt;
&lt;li&gt;Revenue&lt;/li&gt;
&lt;li&gt;Quantity&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dimension Table
&lt;/h2&gt;

&lt;p&gt;Contains descriptive attributes&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Customer Name&lt;/li&gt;
&lt;li&gt;Product Category&lt;/li&gt;
&lt;li&gt;Date&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example Model
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;FactSales&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OrderID&lt;/li&gt;
&lt;li&gt;CustomerID&lt;/li&gt;
&lt;li&gt;ProductID&lt;/li&gt;
&lt;li&gt;SalesAmount&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DimCustomer&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CustomerID&lt;/li&gt;
&lt;li&gt;Name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DimProduct&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ProductID&lt;/li&gt;
&lt;li&gt;Category&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data Modeling Schemas
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Star Schema
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One central fact table&lt;/li&gt;
&lt;li&gt;Connected to multiple dimension tables&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Snowflake Schema
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Dimensions are normalized (split into multiple tables)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Product → Category → Department&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Flat Table (Denormalized / DLAT)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All data in one table&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Schema&lt;/th&gt;
&lt;th&gt;When to Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Star&lt;/td&gt;
&lt;td&gt;Most Power BI reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;Complex hierarchical data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat Table&lt;/td&gt;
&lt;td&gt;Small datasets or quick analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Role-Playing Dimensions
&lt;/h2&gt;

&lt;p&gt;A role-playing dimension is a table used multiple times for different purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Date Table&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Order Date&lt;/li&gt;
&lt;li&gt;Ship Date&lt;/li&gt;
&lt;li&gt;Delivery Date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Power BI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Duplicate the Date table&lt;/li&gt;
&lt;li&gt;Create separate relationships&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Data Modeling Issues
&lt;/h2&gt;

&lt;p&gt;Ambiguous relationships&lt;br&gt;
Happens with multiple paths between tables&lt;/p&gt;

&lt;p&gt;Many-to-many confusion&lt;br&gt;
Can lead to incorrect aggregations&lt;/p&gt;

&lt;p&gt;Circular relationships&lt;br&gt;
Causes errors&lt;/p&gt;

&lt;p&gt;Poor performance&lt;br&gt;
Caused by flat tables or too many joins&lt;/p&gt;

&lt;p&gt;Solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use star schema&lt;/li&gt;
&lt;li&gt;Avoid unnecessary bi-directional filters&lt;/li&gt;
&lt;li&gt;Keep relationships simple&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-Step: Building a Model in Power BI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Load Data
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Home → Get Data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Clean Data (Power Query)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Remove duplicates&lt;/li&gt;
&lt;li&gt;Handle nulls&lt;/li&gt;
&lt;li&gt;Merge tables if needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Create Relationships
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Go to Model View&lt;/li&gt;
&lt;li&gt;Drag and connect tables&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Validate Model
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Check cardinality&lt;/li&gt;
&lt;li&gt;Ensure no ambiguous paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: Optimize
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use star schema&lt;/li&gt;
&lt;li&gt;Reduce columns&lt;/li&gt;
&lt;li&gt;Avoid many-to-many unless necessary&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Data modeling in Power BI is not just technical, it’s strategic. Understanding joins helps you prepare data, while relationships allow you to analyze it efficiently. By structuring your data into fact and dimension tables and choosing the right schema (preferably star), you create a model that is both powerful and scalable.&lt;/p&gt;

&lt;p&gt;Mastering these concepts transforms Power BI from a simple visualization tool into a robust analytics engine and ultimately changes how you interpret and interact with data.&lt;/p&gt;

</description>
      <category>data</category>
      <category>microsoft</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I passed 13 AWS certifications. Here's what I actually use at work (and what collects dust).</title>
      <dc:creator>Vaibhav Kaushik</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:20:38 +0000</pubDate>
      <link>https://vibe.forem.com/some_tech_stuff_/i-passed-13-aws-certifications-heres-what-i-actually-use-at-work-and-what-collects-dust-148</link>
      <guid>https://vibe.forem.com/some_tech_stuff_/i-passed-13-aws-certifications-heres-what-i-actually-use-at-work-and-what-collects-dust-148</guid>
      <description>&lt;p&gt;It was a random Tuesday. No warning, no gradual degradation. RDS just... stopped accepting connections. ECS tasks were crashing in a cascade, CloudWatch alarms were firing, and my Slack was lighting up with "is prod down?" messages from three different people simultaneously.&lt;/p&gt;

&lt;p&gt;I didn't open a study guide. I opened CloudWatch Logs, pulled up the VPC flow logs, and started working backwards. Five years of late nights debugging security groups, IAM policies that were almost right, and routing tables that made sense on paper but not in practice that's what I was actually reaching for.&lt;/p&gt;

&lt;p&gt;That incident got me thinking: I've spent a significant chunk of my career chasing AWS certifications 13 of them, including Professional and Specialty levels. But which ones actually matter when the pressure is on?&lt;/p&gt;

&lt;p&gt;Here's my honest breakdown.&lt;/p&gt;

&lt;p&gt;I work as an AWS Solutions Architect at Wipro, designing multi-account architectures, building Terraform modules, and managing CI/CD pipelines for enterprise clients. My day-to-day stack is AWS, Terraform, Kubernetes, Docker, Python, and GitLab CI.&lt;/p&gt;

&lt;p&gt;The three that actually changed how I work&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Solutions Architect – Professional&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This one rewired how I think. Not because of the exam content, but because preparing for it forces you to reason about tradeoffs at scale cost vs performance vs reliability vs operational overhead. I now approach every architecture decision with that mental framework, even when no one asks me to.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DevOps Engineer – Professional&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you work with CI/CD, IaC, or anything that touches deployment pipelines, this cert is the closest to real work. CodePipeline, CloudFormation, Systems Manager, Auto Scaling these come up constantly. Studying for this gave me a structured mental model for a domain I was already working in.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Security – Specialty&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Enterprise clients don't trust you until they trust your security posture. This cert gave me the vocabulary and depth to have those conversations confidently SCPs, permission boundaries, GuardDuty findings, encryption at rest vs in transit. In my experience, security knowledge is the #1 differentiator between a junior and senior cloud architect.&lt;/p&gt;

&lt;p&gt;The honest truth about certification chasing&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Certifications are a map. They are not the territory. The territory is production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've met engineers with 2 certs who could debug a broken VPC faster than people with 10. I've also met certified professionals who couldn't explain why their Terraform plan was destroying a resource it shouldn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Certs gave me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A structured way to learn services I wouldn't have touched otherwise&lt;/li&gt;
&lt;li&gt;Credibility in client conversations and job applications&lt;/li&gt;
&lt;li&gt;A forcing function to fill gaps in my knowledge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What certs didn't give me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ability to stay calm at 2am when prod is down&lt;/li&gt;
&lt;li&gt;Intuition for when &lt;em&gt;not&lt;/em&gt; to use a managed service&lt;/li&gt;
&lt;li&gt;Experience debugging a Kubernetes pod that won't start because of a misconfigured IAM role for service accounts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My recommendation if you're starting out&lt;/p&gt;

&lt;p&gt;Don't do 13. Do these four, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Practitioner&lt;/strong&gt; — only if you're new to cloud entirely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solutions Architect – Associate&lt;/strong&gt; — your real starting point&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevOps Engineer – Professional&lt;/strong&gt; — if you work in infra/platform/DevOps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security – Specialty&lt;/strong&gt; — regardless of your role, this pays off&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then go build something. Break it. Fix it. That's the cert that matters most.&lt;/p&gt;

&lt;p&gt;I'm a Solutions Architect at Wipro and AWS Community Builder based in London. I write about real-world AWS, Terraform, and cloud architecture. Follow along if that's useful to you.&lt;/p&gt;

&lt;p&gt;What cert has been most useful in your day-to-day work? Drop it in the comments.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
      <category>career</category>
    </item>
    <item>
      <title>RDLC Without BC: When the Feedback Loop Is Longer Than the Workday</title>
      <dc:creator>Stack Collider</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:18:29 +0000</pubDate>
      <link>https://vibe.forem.com/stackcollider/rdlc-without-bc-when-the-feedback-loop-is-longer-than-the-workday-19im</link>
      <guid>https://vibe.forem.com/stackcollider/rdlc-without-bc-when-the-feedback-loop-is-longer-than-the-workday-19im</guid>
      <description>&lt;h2&gt;
  
  
  RDLC Without BC: When the Feedback Loop Is Longer Than the Workday
&lt;/h2&gt;

&lt;h1&gt;
  
  
  csharp #dotnet #businesscentral #rdlc #wpf
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Nexus Claude: Every time a developer says "let me check how the report looks" — somewhere in an office, Business Central starts deploying.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;Developing RDLC reports for Business Central looks straightforward: edit the &lt;code&gt;.rdlc&lt;/code&gt; file in Visual Studio, deploy the extension to BC, run the report, see the result.&lt;/p&gt;

&lt;p&gt;The problem is in the word "deploy".&lt;/p&gt;

&lt;p&gt;Every change — even nudging a field by two pixels — requires a full cycle: build, deploy, launch BC, navigate to the report, enter parameters. Five minutes at best. Multiply by twenty iterations a day and the workday becomes waiting.&lt;/p&gt;

&lt;p&gt;The goal is simple:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Open RDLC and XML data → see the result. No BC. No deploy. Now.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Act One: Dead End in Visual Studio
&lt;/h2&gt;

&lt;p&gt;The first thought — Visual Studio Report Designer has a Preview mode. So you can connect XML as a DataSource and view the report right there.&lt;/p&gt;

&lt;p&gt;Create a Report Server Project. Open Designer. Try to add a DataSource — Designer won't allow it. Try connecting XML directly through Connection Properties — and get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The XmlDP query is invalid.
Unexpected end of file has occurred.
The following elements are not closed: Column, Columns, DataItem, DataItems, ReportDataSet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the &lt;code&gt;.rdlc&lt;/code&gt; file manually. Inside — a hard lock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;DataSources&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;DataSource&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"DataSource"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ConnectionProperties&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;DataProvider&amp;gt;&lt;/span&gt;SQL&lt;span class="nt"&gt;&amp;lt;/DataProvider&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ConnectString&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ConnectionProperties&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/DataSource&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/DataSources&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DataProvider=SQL&lt;/code&gt;. Can't change it through the UI. Designer won't allow it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nexus Claude: Microsoft isn't hiding the solution. Microsoft considers this scenario non-existent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Official dead end. Moving on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Act Two: Build It Yourself
&lt;/h2&gt;

&lt;p&gt;If the tool doesn't exist — write it.&lt;/p&gt;

&lt;p&gt;The architecture is simple: a WPF application takes two files — &lt;code&gt;.rdlc&lt;/code&gt; and &lt;code&gt;.xml&lt;/code&gt; with BC data — renders a PDF via &lt;code&gt;ReportViewerCore.NETCore&lt;/code&gt; and displays the result through WebView2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"ReportViewerCore.NETCore"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Web.WebView2"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"*"&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 rendering pipeline is transparent: XML is parsed into a &lt;code&gt;DataTable&lt;/code&gt;, &lt;code&gt;DataTable&lt;/code&gt; is passed to &lt;code&gt;LocalReport&lt;/code&gt;, &lt;code&gt;LocalReport&lt;/code&gt; renders PDF into a &lt;code&gt;MemoryStream&lt;/code&gt;, WebView2 displays the bytes.&lt;/p&gt;

&lt;p&gt;The only non-trivial part — column type inference. BC delivers everything as strings, but &lt;code&gt;LocalReport&lt;/code&gt; needs proper types — otherwise numeric fields don't calculate, dates don't sort.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;InferColumnType&lt;/code&gt; logic walks through all non-empty values in a column:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;int → decimal → DateTime → string&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Dates deserve special attention. BC uses the &lt;code&gt;dd/MM/yyyy&lt;/code&gt; format. Standard &lt;code&gt;DateTime.TryParse&lt;/code&gt; with &lt;code&gt;InvariantCulture&lt;/code&gt; won't recognize it. Explicit formats are required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;DateFormats&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"dd/MM/yyyy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dd/MM/yyyy HH:mm:ss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"yyyy-MM-dd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"yyyy-MM-ddTHH:mm:ss"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryParseExact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateFormats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvariantCulture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DateTimeStyles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&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 this, &lt;code&gt;19/02/2026&lt;/code&gt; silently falls through to &lt;code&gt;string&lt;/code&gt; — the report works, but date sorting breaks. Silently.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nexus Claude: A system that stays quiet when something goes wrong isn't more reliable than one that crashes. It's just harder to diagnose.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Act Three: The Installer Is Its Own Story
&lt;/h2&gt;

&lt;p&gt;A tool for yourself is one thing. A tool for the team requires an installer.&lt;/p&gt;

&lt;p&gt;The choice is obvious — Inno Setup. Free, proven, no dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem one: the preprocessor
&lt;/h3&gt;

&lt;p&gt;The first version of the script used &lt;code&gt;#define&lt;/code&gt; for variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight pascal"&gt;&lt;code&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;AppName&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;RDLC&lt;/span&gt; &lt;span class="n"&gt;Report&lt;/span&gt; &lt;span class="n"&gt;Tester&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="n"&gt;define&lt;/span&gt; &lt;span class="n"&gt;AppVersion&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="m"&gt;1.01&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inno Setup Compiler compiles without errors, but the &lt;code&gt;[Setup]&lt;/code&gt; section is empty — values weren't substituted. Cause: file encoding. &lt;code&gt;#define&lt;/code&gt; directives aren't read when saved as UTF-8 with BOM in certain configurations.&lt;/p&gt;

&lt;p&gt;Fix — remove the preprocessor entirely. Hardcode values directly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Result:&lt;/em&gt; ❌ ~20 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem two: DownloadTemporaryFile
&lt;/h3&gt;

&lt;p&gt;The installer needs to check for WebView2 Runtime and download it if missing. First version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight pascal"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;DownloadTemporaryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="n"&gt;DownloadProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c1"&gt;// error
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compiler: type mismatch. &lt;code&gt;DownloadTemporaryFile&lt;/code&gt; returns &lt;code&gt;Int64&lt;/code&gt; (byte count), not &lt;code&gt;Boolean&lt;/code&gt;. On error — it throws an exception. Need &lt;code&gt;try/except&lt;/code&gt;, not a return value check.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Result:&lt;/em&gt; ❌ ~15 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem three: PublishSingleFile
&lt;/h3&gt;

&lt;p&gt;The app was built as &lt;code&gt;PublishSingleFile&lt;/code&gt; — one &lt;code&gt;.exe&lt;/code&gt;, no dependencies alongside. Logical for distribution.&lt;/p&gt;

&lt;p&gt;After installing via the installer, launch from &lt;code&gt;Program Files&lt;/code&gt; — silence. Event Viewer says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.DllNotFoundException: Dll was not found.
at MS.Internal.WindowsBase.NativeMethodsSetLastError.SetWindowLongPtrWndProc
at MS.Win32.HwndSubclass.SubclassWndProc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PublishSingleFile&lt;/code&gt; extracts native DLLs into a temp folder next to the exe at runtime. In &lt;code&gt;Program Files&lt;/code&gt; — no write permissions. WPF crashes before the window initializes. Silently.&lt;/p&gt;

&lt;p&gt;Fix — disable &lt;code&gt;PublishSingleFile&lt;/code&gt; in &lt;code&gt;.csproj&lt;/code&gt;. The installer packages the entire &lt;code&gt;publish\&lt;/code&gt; folder into one Setup.exe. The end result is the same — one file to download — but native DLLs sit alongside the exe where they belong.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Result:&lt;/em&gt; ❌ ~30 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem four: WebView2 and permissions
&lt;/h3&gt;

&lt;p&gt;After the DLL fix the app launches, but crashes again — now with a different error code. WebView2 creates a user data folder next to the exe during initialization. In &lt;code&gt;Program Files&lt;/code&gt; — no write permissions again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: WebView2 creates folder next to exe&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureCoreWebView2Async&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// After: explicit path in AppData&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;CoreWebView2Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFolderPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SpecialFolder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApplicationData&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"RdlcReportTester"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"WebView2"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureCoreWebView2Async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Result:&lt;/em&gt; ✅&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nexus Claude: Two crashes in a row from the same root cause — no write permissions. Both silent. A good reminder: "installing in Program Files" isn't just a path — it's a contract with Windows about access rights.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BC XML DataSource → RdlcRendererWpf → PDF Preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the app, drag and drop &lt;code&gt;.rdlc&lt;/code&gt; and &lt;code&gt;.xml&lt;/code&gt;, see the report. No deployments, no BC sessions.&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%2Fv6p1dlw8wi90rk8bv9w1.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%2Fv6p1dlw8wi90rk8bv9w1.png" alt="RDLC Report Tester v1.01 — PDF preview with test data"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A careful reader will spot &lt;code&gt;StackCollider Latvia SIA&lt;/code&gt; in the vendor list. An easter egg. Test data deserves character too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"ReportViewerCore.NETCore"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Web.WebView2"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"*"&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;Full source → &lt;a href="https://github.com/stackcollider/rdlc-report-tester" rel="noopener noreferrer"&gt;github.com/stackcollider/rdlc-report-tester&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nexus Claude: A tool that speeds up development is code too. Sometimes it's more important to write it than the next report.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>csharp</category>
    </item>
    <item>
      <title>AI Tools Build Fast. Here Is What They Miss</title>
      <dc:creator>Quality Bridge Consulting</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:13:32 +0000</pubDate>
      <link>https://vibe.forem.com/qualitybridgeconsulting/ai-tools-build-fast-here-is-what-they-miss-3igo</link>
      <guid>https://vibe.forem.com/qualitybridgeconsulting/ai-tools-build-fast-here-is-what-they-miss-3igo</guid>
      <description>&lt;p&gt;We are not going to tell you AI development tools are overhyped. They are not. We used them on a real client project and an internal tool, and the speed was everything people claim it is.&lt;/p&gt;

&lt;p&gt;What nobody talks about is what happens after the first working version appears on your screen.&lt;/p&gt;

&lt;p&gt;That is the part worth writing about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What We Built&lt;/strong&gt;&lt;br&gt;
The first project was an internal tracking and project management tool for our own delivery work at &lt;a href="https://qualitybridgeconsulting.com" rel="noopener noreferrer"&gt;QualityBridge Consulting&lt;/a&gt;. The kind of thing that would sit in a backlog for months waiting for development time. Using an AI-powered builder, we had a working MVP in one to two weeks. That timeline would have been six to eight weeks through traditional development.&lt;/p&gt;

&lt;p&gt;The second was a website prototype for a restaurant client. They needed something functional and modern to put in front of stakeholders before committing to a full build. We delivered a clickable, working prototype in days. The client could react to something real rather than read through a specification document.&lt;/p&gt;

&lt;p&gt;Both builds were successful. Both also required more rigour than the tools suggest you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the Tools Earn Their Reputation&lt;/strong&gt;&lt;br&gt;
The speed on frontend delivery is real. Clean, modern interfaces built on React and Tailwind CSS that would take a developer several days to produce came together in a fraction of that time.&lt;/p&gt;

&lt;p&gt;For prototyping specifically, the value is obvious. Stakeholders give better feedback on something they can interact with. Getting to that stage in days rather than weeks changes the entire dynamic of early project conversations.&lt;/p&gt;

&lt;p&gt;For internal tools, the case is just as strong. Teams carry backlogs full of tools they need but cannot justify the development cost to build. AI builders change that calculation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the Tools Do Not Tell You&lt;/strong&gt;&lt;br&gt;
This is the part that matters for anyone considering these tools seriously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You still need to test properly&lt;/strong&gt;. AI-generated code looks right. In controlled conditions it usually works right. But real users do not use software in controlled conditions. They enter unexpected inputs, navigate in unexpected sequences, and find the edge cases that a visual check will never catch. On both our builds, structured testing found issues before they reached anyone outside our team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code review is not optional&lt;/strong&gt;. These tools generate code fast, but they do not always generate it consistently across a longer build. We found instances where iterating on a feature caused the AI to introduce changes that conflicted with earlier decisions. Without someone reviewing what was being generated at each step, those conflicts accumulate quietly until they become a real problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change tracking requires deliberate effort&lt;/strong&gt;. Traditional development has version control and pull request reviews built into the process. AI-assisted development moves fast enough that it is easy to lose track of what changed, when, and why. On our internal tool, keeping a clear log of every prompt, every iteration, and every decision was not a nice-to-have. It was the difference between a product we could maintain and a prototype nobody could safely modify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Broader Point&lt;/strong&gt;&lt;br&gt;
AI development tools lower the barrier to building. That is a good thing for lean teams and scaling businesses who cannot justify a full engineering team for every internal tool or early-stage product.&lt;br&gt;
But there is a difference between lowering the barrier to building and lowering the standard of what gets shipped.&lt;/p&gt;

&lt;p&gt;The teams that get the most from these tools treat them as a fast starting point, not a finished product. They use the speed to move quickly through early iterations, then apply proper quality practices before anything reaches real users or real data.&lt;/p&gt;

&lt;p&gt;Thorough testing. Code review. Tracked changes. Clear acceptance criteria before anything is called done.&lt;/p&gt;

&lt;p&gt;The tools have changed how fast a build can start. They have not changed what done actually means.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Honest Take&lt;/strong&gt;&lt;br&gt;
We will keep using these tools. The speed advantage on prototypes and internal builds is too useful to set aside, and the output quality continues to improve with each passing month.&lt;/p&gt;

&lt;p&gt;But every build we do with AI assistance gets the same quality treatment as every other build. The same testing standards. The same review process. The same expectation that what ships works correctly and can be maintained by the team inheriting it.&lt;/p&gt;

&lt;p&gt;If you are exploring AI-assisted development for your business, the question is not whether the tools are good. They are. The question is whether your delivery process is ready to work alongside them properly.&lt;br&gt;
Most are not. That is where the real work is.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://qualitybridgeconsulting.com/" rel="noopener noreferrer"&gt;QualityBridge Consulting&lt;/a&gt; helps SMEs and scaling teams deliver digital products with structure, transparency, and no surprises. If you are building with AI tools and want to make sure what ships is actually production-ready, we would be glad to talk.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>softwaredevelopment</category>
      <category>ai</category>
    </item>
    <item>
      <title>Parsing LLM Responses in DataWeave: 3 Layers of Defense Against Markdown Fences</title>
      <dc:creator>ThaSha</dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:12:23 +0000</pubDate>
      <link>https://vibe.forem.com/thasha/parsing-llm-responses-in-dataweave-3-layers-of-defense-against-markdown-fences-4ch5</link>
      <guid>https://vibe.forem.com/thasha/parsing-llm-responses-in-dataweave-3-layers-of-defense-against-markdown-fences-4ch5</guid>
      <description>&lt;p&gt;I connected MuleSoft to GPT-4o last quarter for a support ticket classifier. The prompt builder worked (I covered that in a previous post). The LLM call worked. Then the response came back and my parser crashed.&lt;/p&gt;

&lt;p&gt;The LLM was supposed to return clean JSON. Instead it returned: "Here is my analysis:\n&lt;br&gt;
&lt;br&gt;
&lt;code&gt;json\n{\"ranking\": [\"TK-101\"]}\n&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
\nLet me know if you need anything else."&lt;/p&gt;

&lt;p&gt;Markdown fences. Preamble text. Trailing conversation. My &lt;code&gt;read()&lt;/code&gt; call choked on "Here is my analysis" — not valid JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLMs wrap JSON responses in markdown fences — your parser must extract the JSON first&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;try()&lt;/code&gt; from &lt;code&gt;dw::Runtime&lt;/code&gt; catches parse failures gracefully — without it, one malformed response crashes your flow&lt;/li&gt;
&lt;li&gt;Always validate required keys after parsing — LLMs hallucinate extra fields and omit required ones&lt;/li&gt;
&lt;li&gt;The regex for fence extraction: &lt;code&gt;raw match /(?s)&lt;/code&gt;&lt;code&gt;(?:json)?\s*(\{.*?\})\s*&lt;/code&gt;&lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test with 5 response variants: clean JSON, fenced JSON, fenced without language tag, no fences, broken JSON&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The Problem: LLMs Don't Return Clean JSON
&lt;/h2&gt;

&lt;p&gt;You ask GPT-4o to return JSON. Sometimes it does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ranking"&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;"TK-101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TK-098"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Timeout is critical."&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;But often it adds commentary:&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;Here&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;analysis:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
json&lt;br&gt;
{"ranking": ["TK-101"], "summary": "Timeout is critical."}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Let me know if you need anything else.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
plaintext&lt;/p&gt;

&lt;p&gt;That's not valid JSON. &lt;code&gt;read(raw, "application/json")&lt;/code&gt; throws. Your Mule flow crashes. 50,000 LLM responses per day = 50,000 potential crashes.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Solution: 3-Layer Defense in DataWeave
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%dw 2.0
import try from dw::Runtime
output application/json
var raw = payload.rawResponse
var fenceMatch = raw match /(?s)```

(?:json)?\s*(\{.*?\})\s*

```/
var jsonStr = if (fenceMatch[1]?) fenceMatch[1] else raw
var parsed = try(() -&amp;gt; read(jsonStr, "application/json"))
var keys = if (parsed.success) (parsed.result pluck $$) else []
var missing = payload.requiredKeys filter (k) -&amp;gt; !(keys contains k)
---
{
  parsed: if (parsed.success) parsed.result else null,
  valid: parsed.success and isEmpty(missing),
  missingKeys: missing
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;100 production-ready DataWeave patterns with tests: &lt;a href="https://github.com/shakarbisetty/mulesoft-cookbook" rel="noopener noreferrer"&gt;mulesoft-cookbook on GitHub&lt;/a&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Layer 1: Regex Fence Extraction
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var fenceMatch = raw match /(?s)```

(?:json)?\s*(\{.*?\})\s*

```/
var jsonStr = if (fenceMatch[1]?) fenceMatch[1] else raw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The regex matches content between markdown fences. &lt;code&gt;(?s)&lt;/code&gt; enables dotall mode so &lt;code&gt;.&lt;/code&gt; matches newlines. &lt;code&gt;(?:json)?&lt;/code&gt; handles both&lt;br&gt;
&lt;br&gt;
 `&lt;code&gt;json` and bare&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
``&lt;code&gt;. The captured group&lt;/code&gt;({.*?})` extracts just the JSON object.&lt;/p&gt;

&lt;p&gt;If no fences found (&lt;code&gt;fenceMatch[1]?&lt;/code&gt; is false), fall back to parsing the raw string directly. This handles the case where the LLM returns clean JSON without fences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: try() Wraps the Parse
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
dataweave
var parsed = try(() -&amp;gt; read(jsonStr, "application/json"))


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

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;try()&lt;/code&gt; from &lt;code&gt;dw::Runtime&lt;/code&gt; is the critical piece. Without it, &lt;code&gt;read()&lt;/code&gt; on invalid JSON throws an exception and crashes the flow. With &lt;code&gt;try()&lt;/code&gt;, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Success: &lt;code&gt;{success: true, result: {...parsed object...}}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Failure: &lt;code&gt;{success: false, error: {...error details...}}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your flow continues either way. Log the error, route to a dead-letter queue, retry with a different prompt — your choice. But the flow never crashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trap:&lt;/strong&gt; If you don't check &lt;code&gt;parsed.success&lt;/code&gt; before accessing &lt;code&gt;parsed.result&lt;/code&gt;, you get &lt;code&gt;null&lt;/code&gt; on failure. Downstream code that expects an object gets null instead. Always check the success flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Required Key Validation
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
dataweave
var keys = if (parsed.success) (parsed.result pluck $$) else []
var missing = payload.requiredKeys filter (k) -&amp;gt; !(keys contains k)


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

&lt;/div&gt;

&lt;p&gt;LLMs hallucinate. You asked for &lt;code&gt;ranking&lt;/code&gt; and &lt;code&gt;summary&lt;/code&gt;. The LLM returned &lt;code&gt;ranking&lt;/code&gt;, &lt;code&gt;analysis&lt;/code&gt;, and &lt;code&gt;confidence&lt;/code&gt; — but not &lt;code&gt;summary&lt;/code&gt;. The JSON is valid but missing a required field.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pluck $$&lt;/code&gt; extracts all keys from the parsed object. &lt;code&gt;filter&lt;/code&gt; checks each required key against the actual keys. &lt;code&gt;missing&lt;/code&gt; tells you exactly which keys the LLM omitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 Response Variants to Test
&lt;/h2&gt;

&lt;p&gt;I test every LLM parser against these variants:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;What Breaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean JSON&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"ranking": [...], "summary": "..."}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nothing — baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fenced with language&lt;/td&gt;
&lt;td&gt;```&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
&lt;code&gt;json\n{...}\n&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
&lt;code&gt;| `read()` without fence extraction |&lt;br&gt;
| Fenced without language |&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
&lt;code&gt;\n{...}\n&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
``&lt;code&gt;| Regex that requires&lt;/code&gt;json&lt;code&gt;after fence |&lt;br&gt;
| No fences, with preamble |&lt;/code&gt;Here is my analysis: {"ranking": [...]}&lt;code&gt;| Naive&lt;/code&gt;read()&lt;code&gt;on full string |&lt;br&gt;
| Broken JSON |&lt;/code&gt;{"ranking": ["TK-101"&lt;code&gt;(truncated) | Any parser without&lt;/code&gt;try()` |&lt;/p&gt;

&lt;p&gt;I hit all 5 in production within the first week. The same model, same prompt, same temperature — 5 different formats across 50,000 responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap: The Regex Doesn't Handle Nested Objects Well
&lt;/h2&gt;

&lt;p&gt;The regex &lt;code&gt;\{.*?\}&lt;/code&gt; uses non-greedy matching. It works for flat JSON objects. But if your LLM returns nested objects:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
json
{"analyses": [{"ticketId": "TK-101", "action": "increase pool"}], "summary": "..."}


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

&lt;/div&gt;



&lt;p&gt;The non-greedy &lt;code&gt;.*?&lt;/code&gt; stops at the first &lt;code&gt;}&lt;/code&gt; — inside the nested array. You get a partial JSON parse.&lt;/p&gt;

&lt;p&gt;For production, I switched to finding the matching closing brace by counting open/close braces, not regex. But the regex works for 90% of LLM responses where the top-level structure is flat.&lt;/p&gt;

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

&lt;p&gt;The parser handles 50,000 LLM responses per day. Average parse time: 2ms per response. The regex is the slowest part — &lt;code&gt;try()&lt;/code&gt; and &lt;code&gt;read()&lt;/code&gt; are fast because they're native DataWeave functions.&lt;/p&gt;

&lt;p&gt;Zero flow crashes since deploying the 3-layer approach. Before it, we had 3-5 crashes per day from malformed LLM responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Do Now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Every LLM integration gets this parser as the first step after the API call&lt;/li&gt;
&lt;li&gt;Failed parses go to a dead-letter queue with the raw response for debugging&lt;/li&gt;
&lt;li&gt;Missing key reports go to a monitoring dashboard — tracks LLM reliability over time&lt;/li&gt;
&lt;li&gt;I test with all 5 variants before any production deployment&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;100 patterns with MUnit tests: &lt;a href="https://github.com/shakarbisetty/mulesoft-cookbook" rel="noopener noreferrer"&gt;github.com/shakarbisetty/mulesoft-cookbook&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;60-second video walkthroughs: &lt;a href="https://www.youtube.com/@SanThaParv?sub_confirmation=1" rel="noopener noreferrer"&gt;youtube.com/@SanThaParv&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataweave</category>
      <category>mulesoft</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>🚀Playwright vs Selenium in 2026: The Ultimate Guide for Modern Test Automation</title>
      <dc:creator>Ankit Aloni </dc:creator>
      <pubDate>Sun, 05 Apr 2026 20:11:17 +0000</pubDate>
      <link>https://vibe.forem.com/ankitaloni369/playwright-vs-selenium-in-2026-the-ultimate-guide-for-modern-test-automation-1bc6</link>
      <guid>https://vibe.forem.com/ankitaloni369/playwright-vs-selenium-in-2026-the-ultimate-guide-for-modern-test-automation-1bc6</guid>
      <description>&lt;h2&gt;
  
  
  ⚡ Why This Decision Matters More Than You Think
&lt;/h2&gt;

&lt;p&gt;Most engineers don’t struggle with writing tests.&lt;/p&gt;

&lt;p&gt;They struggle with &lt;strong&gt;trusting them&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;❌ Test passes locally → fails in CI&lt;br&gt;&lt;br&gt;
❌ Pipeline fails → rerun → magically passes&lt;br&gt;&lt;br&gt;
❌ &lt;code&gt;Thread.sleep()&lt;/code&gt; becomes your best friend  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s not bad luck.&lt;/p&gt;

&lt;p&gt;👉 That’s a &lt;strong&gt;tooling mismatch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In 2026, choosing between &lt;strong&gt;Playwright vs Selenium&lt;/strong&gt; is not just technical.&lt;/p&gt;

&lt;p&gt;It’s a decision about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚡ Speed
&lt;/li&gt;
&lt;li&gt;🧠 Reliability
&lt;/li&gt;
&lt;li&gt;🏗️ Engineering culture
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧭 Two Generations of Testing Philosophy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🧱 Selenium: The Foundation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Born in 2004
&lt;/li&gt;
&lt;li&gt;W3C WebDriver standard
&lt;/li&gt;
&lt;li&gt;Enterprise adoption
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server-rendered apps
&lt;/li&gt;
&lt;li&gt;Predictable DOM
&lt;/li&gt;
&lt;li&gt;Simpler JS
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ⚡ Playwright: The Modern Approach
&lt;/h3&gt;

&lt;p&gt;Built for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SPA apps
&lt;/li&gt;
&lt;li&gt;Async-heavy UI
&lt;/li&gt;
&lt;li&gt;Dynamic DOM
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Don’t just act on the DOM — understand its state.”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  ⚡ Core Difference (This Changes Everything)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Selenium&lt;/th&gt;
&lt;th&gt;Playwright&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Command-based&lt;/td&gt;
&lt;td&gt;State-aware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External control&lt;/td&gt;
&lt;td&gt;Internal awareness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual waits&lt;/td&gt;
&lt;td&gt;Auto sync&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;👉 This is why Playwright feels faster, cleaner, and more reliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Architecture (Where Tests Actually Break)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Selenium
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Test] → HTTP → WebDriver → Browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Every action = network call
&lt;/li&gt;
&lt;li&gt;Slower
&lt;/li&gt;
&lt;li&gt;More fragile
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebElement&lt;/span&gt; &lt;span class="n"&gt;btn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;btn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;click&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Playwright
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Test] → WebSocket/CDP → Browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Persistent connection
&lt;/li&gt;
&lt;li&gt;Event-driven
&lt;/li&gt;
&lt;li&gt;Faster
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔍 Why This Matters in Real Life
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ Selenium Approach
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ Playwright Approach
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright automatically waits for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✔️ Visibility
&lt;/li&gt;
&lt;li&gt;✔️ Stability
&lt;/li&gt;
&lt;li&gt;✔️ Interactivity
&lt;/li&gt;
&lt;/ul&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Pro Tip:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If your tests need retries to pass, your architecture is broken.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧑‍💻 Developer Experience (DX)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ⚡ Setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Playwright → Ready in minutes
&lt;/li&gt;
&lt;li&gt;Selenium → Setup heavy
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🧪 Assertions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Playwright&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.alert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toHaveText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Success&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Selenium&lt;/span&gt;
&lt;span class="nc"&gt;WebElement&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"alert"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Success"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getText&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Playwright = cleaner + smarter assertions&lt;/p&gt;




&lt;h3&gt;
  
  
  ⏳ Wait Handling
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Selenium&lt;/th&gt;
&lt;th&gt;Playwright&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Explicit waits&lt;/td&gt;
&lt;td&gt;Built-in waits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual logic&lt;/td&gt;
&lt;td&gt;Smart defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  🐞 Debugging
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Playwright&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trace viewer
&lt;/li&gt;
&lt;li&gt;Video
&lt;/li&gt;
&lt;li&gt;Network logs
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Selenium&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logs
&lt;/li&gt;
&lt;li&gt;Screenshots
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📊 Feature Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Playwright&lt;/th&gt;
&lt;th&gt;Selenium&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auto-waiting&lt;/td&gt;
&lt;td&gt;✅ Smart&lt;/td&gt;
&lt;td&gt;❌ Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel execution&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;td&gt;⚠️ Grid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network mocking&lt;/td&gt;
&lt;td&gt;✅ Native&lt;/td&gt;
&lt;td&gt;❌ Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shadow DOM&lt;/td&gt;
&lt;td&gt;✅ Easy&lt;/td&gt;
&lt;td&gt;⚠️ Complex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual testing&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;td&gt;❌ External&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🔬 Performance Reality
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Playwright&lt;/th&gt;
&lt;th&gt;Selenium&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;⚡ Fast&lt;/td&gt;
&lt;td&gt;🐢 Slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flakiness&lt;/td&gt;
&lt;td&gt;Low (~3%)&lt;/td&gt;
&lt;td&gt;Higher (~15%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium–High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🚀 CI/CD Simplicity
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Playwright
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright install&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Selenium
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selenium&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;selenium/standalone-chrome&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mvn test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 More infra = more problems&lt;/p&gt;




&lt;h2&gt;
  
  
  🐞 Debugging Difference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--trace&lt;/span&gt; on
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Replay entire test (DOM + network + screenshots)&lt;/p&gt;




&lt;h2&gt;
  
  
  🔁 Real Code Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Playwright
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&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&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;admin&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#pass&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;1234&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#login&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveURL&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Selenium
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/login"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;sendKeys&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"admin"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pass"&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;sendKeys&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1234"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"login"&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;click&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WebDriverWait&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;until&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExpectedConditions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;urlContains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/dashboard"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎯 When to Use What
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Choose Playwright if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;New project
&lt;/li&gt;
&lt;li&gt;Need speed + stability
&lt;/li&gt;
&lt;li&gt;CI-first setup
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ Choose Selenium if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Large existing suite
&lt;/li&gt;
&lt;li&gt;Java ecosystem
&lt;/li&gt;
&lt;li&gt;Migration cost is high
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧠 Decision Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New project?
 ├── Yes → Playwright
 └── No
      ├── Existing Selenium?
      │     ├── Yes → Stay / gradual migration
      │     └── No → Playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🏁 Final Verdict
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🚀 Playwright → Modern, fast, reliable
&lt;/li&gt;
&lt;li&gt;🧱 Selenium → Stable, enterprise-ready
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 No universal winner — only the right context.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 What You Should Do Today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Install Playwright
&lt;/li&gt;
&lt;li&gt;Convert 2–3 tests
&lt;/li&gt;
&lt;li&gt;Compare:

&lt;ul&gt;
&lt;li&gt;Speed
&lt;/li&gt;
&lt;li&gt;Stability
&lt;/li&gt;
&lt;li&gt;Debugging
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  💬 Final Thought
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A test suite is not valuable because it exists.&lt;br&gt;&lt;br&gt;
It’s valuable because you &lt;strong&gt;trust it&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you don’t trust your tests…&lt;br&gt;&lt;br&gt;
you don’t have automation.&lt;/p&gt;

&lt;p&gt;You have noise.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>testing</category>
      <category>playwright</category>
      <category>selenium</category>
    </item>
  </channel>
</rss>
