<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Manish</title>
    <description>The latest articles on DEV Community by Manish (@reclusivecoder).</description>
    <link>https://dev.to/reclusivecoder</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F577523%2F2292b0e5-687d-4a8f-a057-5dd34922d236.jpg</url>
      <title>DEV Community: Manish</title>
      <link>https://dev.to/reclusivecoder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/reclusivecoder"/>
    <language>en</language>
    <item>
      <title>Build a custom multi-step AI workflow graph with openJiuwen Agent Core</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Tue, 27 Oct 2026 17:00:16 +0000</pubDate>
      <link>https://dev.to/reclusivecoder/build-a-custom-multi-step-ai-workflow-graph-with-openjiuwen-agent-core-35oi</link>
      <guid>https://dev.to/reclusivecoder/build-a-custom-multi-step-ai-workflow-graph-with-openjiuwen-agent-core-35oi</guid>
      <description>&lt;p&gt;A multi-step LLM pipeline usually breaks at a handoff. The triage step returns &lt;code&gt;"high"&lt;/code&gt;, but the routing step expects the integer &lt;code&gt;4&lt;/code&gt;. Nothing raises an error. The bad value keeps moving downstream until a customer gets the wrong reply or a dashboard shows misleading figures. By then you can't say where the data went wrong, so you can't fix it quickly.&lt;/p&gt;

&lt;p&gt;This tutorial builds a support-ticket workflow on &lt;a href="https://github.com/openJiuwen-ai/agent-core" rel="noopener noreferrer"&gt;openJiuwen Agent Core&lt;/a&gt;, an open-source Python SDK for agent workflows. The graph has five parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;Start&lt;/code&gt; node.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;LLMComponent&lt;/code&gt; that triages the ticket into a typed record (named fields with fixed types).&lt;/li&gt;
&lt;li&gt;A human approval step.&lt;/li&gt;
&lt;li&gt;A second &lt;code&gt;LLMComponent&lt;/code&gt; that drafts a reply.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;End&lt;/code&gt; node.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;WorkflowAgent&lt;/code&gt; runs the graph.&lt;/p&gt;

&lt;p&gt;You'll also see where Agent Core enforces data contracts (rules for what each step may accept and return). The evidence comes from a proof suite that ran every scenario against the real SDK and a real model, three times in a row. Agent Core checks two boundaries at run time and leaves one unchecked. The proof suite shows that gap too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment setup
&lt;/h2&gt;

&lt;p&gt;You will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.11, 3.12, or 3.13 (&lt;a href="https://pypi.org/project/openjiuwen/" rel="noopener noreferrer"&gt;openjiuwen on PyPI&lt;/a&gt; declares &lt;code&gt;&amp;gt;=3.11,&amp;lt;3.14&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;An OpenAI API key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;openjiuwen==0.1.19&lt;/code&gt;, which matches the &lt;a href="https://github.com/openJiuwen-ai/agent-core/releases" rel="noopener noreferrer"&gt;&lt;code&gt;v0.1.19&lt;/code&gt; release&lt;/a&gt; of Agent Core
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/manishh/openjiuwen--custom-multi-step-ai-workflow-agent-core-proof-first
&lt;span class="nb"&gt;cd &lt;/span&gt;openjiuwen--custom-multi-step-ai-workflow-agent-core-proof-first
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env     &lt;span class="c"&gt;# then set OPENAI_API_KEY=sk-... (no quotes, no spaces)&lt;/span&gt;
./prove &lt;span class="nt"&gt;--repeat&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also export the key in your shell (&lt;code&gt;export OPENAI_API_KEY=sk-...&lt;/code&gt;) instead of using &lt;code&gt;.env&lt;/code&gt;. The &lt;code&gt;.env&lt;/code&gt; file is git-ignored.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;./prove&lt;/code&gt; to start the proof  (demo). It creates a virtual environment (&lt;code&gt;.venv&lt;/code&gt;), installs the pinned dependencies from &lt;code&gt;requirements.txt&lt;/code&gt; and &lt;code&gt;constraints.txt&lt;/code&gt;, and runs every scenario. Each repeat runs in a new Python process and a new working directory, because Agent Core keeps its checkpointer (the store for saved workflow state) and workflow registry in process memory. A repeat makes about seven small model calls and takes about 25 seconds. &lt;code&gt;./prove&lt;/code&gt; prints PASS or FAIL for every check. It exits with 0 only if every check passed in every repeat.&lt;/p&gt;

&lt;p&gt;To install the SDK into your own project, run &lt;code&gt;pip install openjiuwen==0.1.19&lt;/code&gt;. The repo README covers the full prerequisites.&lt;/p&gt;

&lt;p&gt;The imports this tutorial needs come from three modules. Two of them are shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openjiuwen.core.foundation.llm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ModelClientConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ModelRequestConfig&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openjiuwen.core.workflow&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;End&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LLMCompConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LLMComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Workflow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;WorkflowCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;WorkflowComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WorkflowAgent&lt;/code&gt; comes from &lt;code&gt;openjiuwen.core.application.workflow_agent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two side effects appear on the first run. The SDK writes INFO logs to stdout, and it creates a &lt;code&gt;logs/&lt;/code&gt; folder in your working directory. The proof script gives each repeat its own directory and sends stdout to &lt;code&gt;product.log&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  WorkflowCard and workflow initialization
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;WorkflowCard&lt;/code&gt; holds the workflow's identity and its input contract. Its &lt;code&gt;input_params&lt;/code&gt; field takes a JSON schema. Agent Core checks every incoming input dict against that schema before any node runs.&lt;/p&gt;

&lt;p&gt;The tutorial schema requires a &lt;code&gt;query&lt;/code&gt; string and allows an optional integer &lt;code&gt;priority_hint&lt;/code&gt;. The &lt;code&gt;priority_hint&lt;/code&gt; field exists only so the proof has a typed field to break (for simulating an error).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;TICKET_INPUT_SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s ticket text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority_hint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;integer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Optional 1-5 hint from the help desk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workflow_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;WorkflowCard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;WorkflowCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workflow_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workflow_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;input_params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TICKET_INPUT_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass the card with the &lt;code&gt;card=&lt;/code&gt; keyword:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;flow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support_ticket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Triage a ticket, get approval, draft a reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The official docs show &lt;code&gt;Workflow(workflow_config=WorkflowConfig(card=...))&lt;/code&gt;. In 0.1.19, that form runs without an error but drops the card. The workflow then gets a random ID and no input schema. Use &lt;code&gt;card=&lt;/code&gt; until the docs catch up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Give each graph variant its own &lt;code&gt;id&lt;/code&gt; as well. &lt;code&gt;WorkflowAgent.add_workflows&lt;/code&gt; registers workflows in a process-wide registry, keyed by ID and version. If you register a different graph under an existing key, the SDK logs an ERROR and the agent keeps running the first graph.&lt;/p&gt;

&lt;p&gt;The input contract holds up in the proof. When &lt;code&gt;priority_hint&lt;/code&gt; is &lt;code&gt;"urgent"&lt;/code&gt;, the agent reports error &lt;code&gt;100016&lt;/code&gt; and no node starts. That means no model call happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"error_code": 100016,
"message": "workflow execute input is invalid, ... reason='input validation failed against schema: [189001] validate data with schema failed, error='1 validation error for DynamicModel\npriority_hint\n  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='urgent', input_type=str]...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schema names the required field &lt;code&gt;query&lt;/code&gt; to match the key &lt;code&gt;WorkflowAgent&lt;/code&gt; uses when it passes the user's message to the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start and End node configuration
&lt;/h2&gt;

&lt;p&gt;In the 0.1.19 SDK, typed inputs live on &lt;code&gt;WorkflowCard.input_params&lt;/code&gt;. &lt;code&gt;Start()&lt;/code&gt; accepts no arguments. Passing it a config dict raises &lt;code&gt;TypeError&lt;/code&gt;. To connect the Start node to the workflow input, use &lt;code&gt;inputs_schema&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_start_comp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;inputs_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${query}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;${...}&lt;/code&gt; strings are references. &lt;code&gt;${query}&lt;/code&gt; reads the workflow input. &lt;code&gt;${start.query}&lt;/code&gt; reads the &lt;code&gt;query&lt;/code&gt; output of the node with ID &lt;code&gt;start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The End node has two return modes, and each one produces a different result shape. If you give &lt;code&gt;End&lt;/code&gt; a &lt;code&gt;responseTemplate&lt;/code&gt;, it runs in text mode. It renders a string and returns &lt;code&gt;{"response": "..."}&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_end_comp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;End&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;responseTemplate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[{{category}}/{{urgency}}] escalated={{approved}} :: {{reply}}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;  &lt;span class="c1"&gt;# text mode
&lt;/span&gt;    &lt;span class="n"&gt;inputs_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${triage.category}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urgency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${triage.urgency}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${approve.approved}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${reply.text}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you leave out the template, &lt;code&gt;End()&lt;/code&gt; runs in variable mode. It returns the values as a dict under &lt;code&gt;output&lt;/code&gt;. In the proof, the variable-mode graph returned &lt;code&gt;{"output": {"category": "bug", "urgency": 5}}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use text mode when a person reads the result. Use variable mode when code consumes it. A downstream service can read &lt;code&gt;output["urgency"]&lt;/code&gt; directly and never has to parse a sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLM component node setup
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;LLMComponent&lt;/code&gt; takes one &lt;code&gt;LLMCompConfig&lt;/code&gt;. The config bundles three things: the model connection, the prompt, and the output contract.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ModelClientConfig&lt;/code&gt; holds the connection to the provider. &lt;code&gt;ModelRequestConfig&lt;/code&gt; picks the model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;model_client_config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ModelClientConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;How to reach the model provider. The key is read from the environment.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ModelClientConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;client_provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OpenAI&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                     &lt;span class="c1"&gt;# Agent Core's OpenAI-compatible client
&lt;/span&gt;        &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;         &lt;span class="c1"&gt;# never hard-code or write to disk
&lt;/span&gt;        &lt;span class="n"&gt;api_base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.openai.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;verify_ssl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                              &lt;span class="c1"&gt;# keep TLS verification on
&lt;/span&gt;        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                   &lt;span class="c1"&gt;# seconds per model call
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;model_request_config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ModelRequestConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Which model to call (and, optionally, sampling settings).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ModelRequestConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MODEL_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proof used &lt;code&gt;gpt-5.4-mini&lt;/code&gt; (&lt;code&gt;DEFAULT_MODEL&lt;/code&gt;) through the &lt;code&gt;OpenAI&lt;/code&gt; provider, with TLS verification on. To use a different model or endpoint, set &lt;code&gt;MODEL_NAME&lt;/code&gt; or &lt;code&gt;OPENAI_BASE_URL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The triage node combines the remaining fields. It defines the output contract and the prompts, then passes everything to &lt;code&gt;LLMCompConfig&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;TRIAGE_OUTPUT_CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing | bug | other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urgency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;integer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1 (low) to 5 (critical)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;TRIAGE_SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You triage customer support tickets.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;TRIAGE_USER_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ticket: {{query}}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# {{query}} is filled from the node's inputs
&lt;/span&gt;&lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;triage_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TRIAGE_USER_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TRIAGE_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LLMComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The triage node: classify a ticket into a typed {category, urgency} record.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;output_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TRIAGE_OUTPUT_CONFIG&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# text/markdown mode allows exactly one output field: the raw reply string.
&lt;/span&gt;        &lt;span class="n"&gt;output_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;triage_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;LLMComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LLMCompConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model_client_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;model_client_config&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;model_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;model_request_config&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;template_content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;                             &lt;span class="c1"&gt;# chat template, rendered per run
&lt;/span&gt;            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;      &lt;span class="c1"&gt;# "json" | "text" | "markdown"
&lt;/span&gt;        &lt;span class="n"&gt;output_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;output_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;# the node's output contract
&lt;/span&gt;    &lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three fields shape what the node sends and returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;template_content&lt;/code&gt; is the chat template. Placeholders such as &lt;code&gt;{{query}}&lt;/code&gt; are filled from the node's inputs. The graph supplies those inputs through &lt;code&gt;inputs_schema={"query": "${start.query}"}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;response_format&lt;/code&gt; takes &lt;code&gt;{"type": "json"}&lt;/code&gt;, &lt;code&gt;{"type": "text"}&lt;/code&gt; or &lt;code&gt;{"type": "markdown"}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;output_config&lt;/code&gt; declares the fields this node promises to downstream nodes. In JSON mode, Agent Core adds the schema to the prompt. It then parses the reply and validates it against the schema. Text and markdown modes accept exactly one output field, which holds the raw reply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The proof tests &lt;code&gt;output_config&lt;/code&gt; with a prompt that contradicts the schema. The schema says &lt;code&gt;urgency&lt;/code&gt; is an integer. The prompt tells the model to report urgency as a word. This simulates someone changing the &lt;em&gt;"house style"&lt;/em&gt; of the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"user": "Ticket: {{query}}\n\nReport urgency as one of the words low, medium or high. Never use a number for urgency."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The drift is deliberate, and &lt;code&gt;gpt-5.4-mini&lt;/code&gt; followed the prompt in every repeat. The proof then runs the same prompt twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text mode.&lt;/strong&gt; The run completes, and &lt;code&gt;"triage_text": "high"&lt;/code&gt; reaches &lt;code&gt;End&lt;/code&gt; as untyped text. The exact wording varies from run to run (for example &lt;code&gt;high&lt;/code&gt; or &lt;code&gt;Urgency: high&lt;/code&gt;), because text mode puts no constraint on the shape of the reply. In every case, a word reaches &lt;code&gt;End&lt;/code&gt; where the schema expects an integer, and nothing checks it.&lt;/p&gt;

&lt;p&gt;If the model happens to answer with an integer anyway, the proof re-runs scenario C, up to three attempts. It records each retry in &lt;code&gt;report.json&lt;/code&gt;. The checks themselves never loosen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON mode.&lt;/strong&gt; Switch triage to JSON mode with &lt;code&gt;urgency: integer&lt;/code&gt;, and the run stops at the triage node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"error_code": 101004,
"message": "component llm_execution process error, reason: component llm_config is invalid, reason: expected type integer but got &amp;lt;class 'str'&amp;gt;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The node list for that run is &lt;code&gt;["start", "triage"]&lt;/code&gt;. &lt;code&gt;End&lt;/code&gt; never starts, so the workflow returns no final output for a consumer to misread.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to connect the graph and run the WorkflowAgent
&lt;/h2&gt;

&lt;p&gt;The full graph adds a human approval step between triage and reply.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    IN([Agent input: query, priority_hint]) --&amp;gt;|checked against WorkflowCard.input_params| start[Start]
    start --&amp;gt;|start.query| triage[LLMComponent triage, JSON mode, output_config]
    triage --&amp;gt;|triage.category, triage.urgency| approve[HumanApproval, session.interact]
    approve --&amp;gt;|approve.approved| reply[LLMComponent reply, text mode]
    reply --&amp;gt;|reply.text| endn[End, text mode]
    endn --&amp;gt; OUT([workflow_final])&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;The approval node&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The approval node is a six-line custom &lt;code&gt;WorkflowComponent&lt;/code&gt;. When it calls &lt;code&gt;session.interact()&lt;/code&gt;, the run pauses and Agent Core saves a checkpoint (a snapshot of the workflow's state).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HumanApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WorkflowComponent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Escalate this &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ticket (urgency &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;urgency&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)? yes/no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Registering nodes and edges&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Register each node together with its input references. Then add the edges.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_workflow_comp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;triage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;triage_llm&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;inputs_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${start.query}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_workflow_comp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HumanApproval&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                       &lt;span class="n"&gt;inputs_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${triage.category}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urgency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${triage.urgency}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_workflow_comp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;reply_llm&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                       &lt;span class="n"&gt;inputs_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${triage.category}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${approve.approved}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dst&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;triage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;triage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt;
    &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The edge check in version 0.1.19 is shallow. &lt;code&gt;add_connection&lt;/code&gt; only confirms that the node IDs are non-empty strings. Compilation only confirms that both endpoints are registered nodes. Neither step looks at the &lt;code&gt;${...}&lt;/code&gt; references.&lt;/p&gt;

&lt;p&gt;Scenario D in the proof shows the effect. It uses the smaller start → triage → end graph and changes one reference in &lt;code&gt;End&lt;/code&gt; to &lt;code&gt;${triage.urgncy}&lt;/code&gt;. The run returns &lt;code&gt;COMPLETED&lt;/code&gt; with &lt;code&gt;{"output": {"category": "bug"}}&lt;/code&gt;. The &lt;code&gt;urgency&lt;/code&gt; field disappears and no error appears. &lt;code&gt;End&lt;/code&gt; drops any value that resolves to &lt;code&gt;None&lt;/code&gt;. To cover your own references, add a unit test that asserts on the keys of the final output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running the WorkflowAgent&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wrap the workflow in a &lt;code&gt;WorkflowAgent&lt;/code&gt; and stream it. The proof suite calls &lt;code&gt;await Runner.start()&lt;/code&gt; before the agent calls and &lt;code&gt;await Runner.stop()&lt;/code&gt; after them. The SDK's own tests use the same pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WorkflowAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WorkflowAgentConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Support-ticket agent driven by an Agent Core workflow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;workflows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_workflows&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agent.stream()&lt;/code&gt; yields four kinds of chunks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tracer_workflow&lt;/code&gt; chunks, one for each node.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tracer_agent&lt;/code&gt; chunks, which carry errors.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;__interaction__&lt;/code&gt; chunk, when a node pauses.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;workflow_final&lt;/code&gt; chunk, which holds the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A failed run doesn't raise an exception from &lt;code&gt;stream()&lt;/code&gt;, so read the error from the tracer chunks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn 1: the run pauses&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In turn 1, the nodes &lt;code&gt;["start", "triage", "approve"]&lt;/code&gt; started, and then the run paused. The output shows the triage result and the question the approval node asked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"triage": {
  "category": "billing",
  "urgency": 4
}
...
"id": "approve",
"value": "Escalate this billing ticket (urgency 4)? yes/no"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the proof, the harness answers this question itself. It resumes the run with a scripted &lt;code&gt;yes&lt;/code&gt; (&lt;code&gt;resume.update("approve", "yes")&lt;/code&gt; in &lt;code&gt;harness/run_proof.py&lt;/code&gt;), so &lt;code&gt;./prove&lt;/code&gt; never waits for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn 2: the run resumes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To resume, send an &lt;code&gt;InteractiveInput&lt;/code&gt; on the same &lt;code&gt;conversation_id&lt;/code&gt;. In &lt;code&gt;run_example.py&lt;/code&gt;, &lt;code&gt;answer&lt;/code&gt; is whatever you type at the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InteractiveInput&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# resume that node from its checkpoint
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk2&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;}):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turn 2 started &lt;code&gt;["approve", "reply", "end"]&lt;/code&gt;. It skipped &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;triage&lt;/code&gt;, and the final response reused turn 1's values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"response": "[billing/4] escalated=True :: Your billing ticket has been escalated to a specialist, who will review it and follow up with you as soon as possible."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What the checkpoint covers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The checkpoint covers runs that pause for input. The checkpointer lives in memory and clears when a run completes. It handles pauses inside a live process and offers no crash recovery. If you send ordinary input to a session that still holds saved state, the SDK raises an error or clears the state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it yourself&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To answer the approval yourself, run the interactive example after &lt;code&gt;./prove&lt;/code&gt; has created &lt;code&gt;.venv&lt;/code&gt;. The script doesn't load &lt;code&gt;.env&lt;/code&gt; on its own, so load the key into your shell first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .env&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a
.venv/bin/python run_example.py &lt;span class="s2"&gt;"I was charged twice this month"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script prints each node's output and asks &lt;code&gt;Escalate this billing ticket (urgency N)? yes/no&lt;/code&gt;. It then resumes the workflow with your answer and prints the final response. The SDK's INFO log lines appear mixed in with the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error reasons and error codes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The proof suite sets one option you should think about before copying it into production. &lt;code&gt;UserConfig.set_is_sensitive(False)&lt;/code&gt; makes the SDK show error reasons. By default, the SDK hides them and reports only &lt;code&gt;"invoke llm failed"&lt;/code&gt;. The proof suite needs the reasons to tell a model API failure (&lt;code&gt;101003&lt;/code&gt;) from a contract failure (&lt;code&gt;101004&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;These are the error codes you'll see in the proof transcripts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;100016&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The workflow input failed the &lt;code&gt;WorkflowCard.input_params&lt;/code&gt; schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;101004&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An LLM node's JSON reply failed its &lt;code&gt;output_config&lt;/code&gt;, for example &lt;code&gt;expected type integer&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;101003&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The model API call failed (network, auth or rate limit), which is unrelated to your contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;You now have a &lt;code&gt;WorkflowAgent&lt;/code&gt; that rejects malformed input with error &lt;code&gt;100016&lt;/code&gt; before it spends a model call. It stops a drifted LLM reply at the node with error &lt;code&gt;101004&lt;/code&gt;, before the reply reaches downstream consumers. It also pauses for human approval and resumes without re-running triage. Agent Core does not check node references, so you have to cover them yourself. Add a test that asserts on the keys of the final output.&lt;/p&gt;

&lt;p&gt;From here, try &lt;a href="https://github.com/openJiuwen-ai/agent-core/blob/develop/docs/en/2.Development%20Guide/API%20Docs/README.md" rel="noopener noreferrer"&gt;registering several workflows on one &lt;code&gt;WorkflowAgent&lt;/code&gt;&lt;/a&gt; or branching the graph so independent nodes run in parallel. Clone &lt;a href="https://github.com/openJiuwen-ai/agent-core" rel="noopener noreferrer"&gt;Agent Core on GitHub&lt;/a&gt; to explore the SDK, and run this tutorial's example from &lt;a href="https://github.com/manishh/openjiuwen--custom-multi-step-ai-workflow-agent-core-proof-first" rel="noopener noreferrer"&gt;its companion repo&lt;/a&gt;. If your team prefers a drag-and-drop canvas, see Agent Studio on &lt;a href="https://www.openjiuwen.com/en/" rel="noopener noreferrer"&gt;openjiuwen.com&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does openJiuwen Agent Core do in a multi-step AI workflow?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;openJiuwen Agent Core is an open-source Python SDK that lets you define agent workflows as graphs of nodes. You declare the workflow's input schema on a &lt;code&gt;WorkflowCard&lt;/code&gt; and each LLM node's output schema in &lt;code&gt;output_config&lt;/code&gt;. As of v0.1.19, the SDK checks the workflow input and the output of JSON-mode LLM nodes at run time. It doesn't check the references between nodes, so cover those with a test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does Agent Core reject input before any node runs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agent Core validates every incoming input dict against the JSON schema you provide in &lt;code&gt;WorkflowCard.input_params&lt;/code&gt; before the first node executes. If the input fails validation, the agent returns error &lt;code&gt;100016&lt;/code&gt; and no model call is made, which means you don't spend tokens on a request that was already broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between text mode and variable mode in the End node?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you pass a &lt;code&gt;responseTemplate&lt;/code&gt; to &lt;code&gt;End&lt;/code&gt;, it renders a formatted string and returns &lt;code&gt;{"response": "..."}&lt;/code&gt;. Without a template, &lt;code&gt;End&lt;/code&gt; returns the raw values as a dict under &lt;code&gt;output&lt;/code&gt;. Text mode suits human-readable results and variable mode suits downstream code that reads specific keys directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does Agent Core catch a drifted LLM output?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a node runs in JSON mode with an &lt;code&gt;output_config&lt;/code&gt; schema, Agent Core adds that schema to the prompt, parses the model's reply, and validates the parsed result against the declared types. If the model returns a string where an integer is required, the run stops at that node with error &lt;code&gt;101004&lt;/code&gt; and the downstream nodes never execute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when a node reference contains a typo?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In version 0.1.19, Agent Core's edge check is shallow. A typo such as &lt;code&gt;${triage.urgncy}&lt;/code&gt; resolves to &lt;code&gt;None&lt;/code&gt; at run time, and &lt;code&gt;End&lt;/code&gt; silently drops any value that resolves to &lt;code&gt;None&lt;/code&gt;. The run still returns &lt;code&gt;COMPLETED&lt;/code&gt; but the missing field is absent from the output. A unit test that asserts on the final output keys is the reliable way to catch this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does the human approval step pause and resume a workflow?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A custom &lt;code&gt;WorkflowComponent&lt;/code&gt; calls &lt;code&gt;await session.interact(...)&lt;/code&gt;, which pauses the run and saves a checkpoint in memory. To resume, you send an &lt;code&gt;InteractiveInput&lt;/code&gt; carrying the same &lt;code&gt;conversation_id&lt;/code&gt; back to &lt;code&gt;agent.stream()&lt;/code&gt;. The resumed run picks up from the approval node and skips the nodes that have already completed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why doesn't &lt;code&gt;./prove&lt;/code&gt; ask me to approve the escalation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The proof answers the approval for you. To prove the same behavior on every repeat, &lt;code&gt;harness/run_proof.py&lt;/code&gt; resumes the paused run with a scripted &lt;code&gt;yes&lt;/code&gt;. The run still pauses at the &lt;code&gt;approve&lt;/code&gt; node, and &lt;code&gt;reply&lt;/code&gt; and &lt;code&gt;end&lt;/code&gt; don't start until the answer arrives. To type the answer yourself, run &lt;code&gt;run_example.py&lt;/code&gt; after loading your key into the shell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Agent Core's checkpoint survive a process restart?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The checkpointer lives in memory and clears when a run completes or the process exits. It covers pauses inside a live process but offers no crash recovery across restarts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which Python versions does openJiuwen Agent Core support?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/openjiuwen/" rel="noopener noreferrer"&gt;openjiuwen on PyPI&lt;/a&gt; declares &lt;code&gt;&amp;gt;=3.11,&amp;lt;3.14&lt;/code&gt;, so Agent Core supports Python 3.11, 3.12, and 3.13.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why should &lt;code&gt;UserConfig.set_is_sensitive(False)&lt;/code&gt; stay out of production?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By default, Agent Core hides detailed error reasons and returns a generic message such as &lt;code&gt;"invoke llm failed"&lt;/code&gt;. Setting &lt;code&gt;is_sensitive&lt;/code&gt; to &lt;code&gt;False&lt;/code&gt; exposes the full reason string, which is useful in a proof suite but may leak internal implementation details in a production environment.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>agents</category>
      <category>openjiuwen</category>
    </item>
    <item>
      <title>How to keep an AI workflow predictable using WorkSwarm's SwarmFlow</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Tue, 27 Oct 2026 16:35:54 +0000</pubDate>
      <link>https://dev.to/reclusivecoder/how-to-keep-an-ai-workflow-predictable-using-workswarms-swarmflow-155l</link>
      <guid>https://dev.to/reclusivecoder/how-to-keep-an-ai-workflow-predictable-using-workswarms-swarmflow-155l</guid>
      <description>&lt;p&gt;Have you ever watched one early mistake spread through a whole team of AI agents? SwarmFlow is built to stop that. It is WorkSwarm's scripting layer for Cluster mode (‘team’ mode). You fix the order of steps in a Python file, hold the run at a human checkpoint, and cap token spend with two separate limits. Every run then follows the same steps in the same order, and leaves a record you can audit.&lt;/p&gt;

&lt;p&gt;Here is the problem it solves. When a team of LLM agents works through a multi-step job, a bad output early in the chain gets passed to every step after it. Say a code-generation agent writes the wrong function. A documentation agent then documents that wrong function. You pay for both calls before anyone looks at the result. There is a second problem. The Leader agent (the one that coordinates the worker agents) decides the order of work at run time. Two runs of the same request can take different paths.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm" rel="noopener noreferrer"&gt;WorkSwarm&lt;/a&gt; (formerly JiuwenSwarm) tackles this with SwarmFlow. A Python file fixes the order of stages. A &lt;code&gt;human()&lt;/code&gt; call holds the run until a person answers. Two token ceilings stop a run and send the Leader a diagnostic message it can act on.&lt;/p&gt;

&lt;p&gt;In this tutorial, you will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build a four-stage pipeline: spec, codegen, human review, and docs.&lt;/li&gt;
&lt;li&gt;Send an edit request at the review checkpoint. Then watch SwarmFlow re-run only the changed stage and replay the unchanged one from its journal (a saved record of each finished step).&lt;/li&gt;
&lt;li&gt;Trip both token ceilings on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every output shown here comes from a proof test that ran all four scenarios three times against OpenAI &lt;code&gt;gpt-5.5&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites and setup
&lt;/h2&gt;

&lt;p&gt;You will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux or macOS with Python 3.11 to 3.13 (the proof ran Python 3.12 on Ubuntu)&lt;/li&gt;
&lt;li&gt;An OpenAI API key with access to &lt;code&gt;gpt-5.5&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ports 19000, 19001 and 18092 free (WorkSwarm's web channel, gateway and agent server)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WorkSwarm is published on PyPI as &lt;a href="https://pypi.org/project/workswarm/" rel="noopener noreferrer"&gt;&lt;code&gt;workswarm&lt;/code&gt;&lt;/a&gt;, because the &lt;code&gt;jiuwenswarm&lt;/code&gt; PyPI name stopped at 0.2.3. The &lt;code&gt;workswarm&lt;/code&gt; package installs the &lt;code&gt;jiuwenswarm-*&lt;/code&gt; commands and pulls in &lt;a href="https://github.com/openJiuwen-ai/agent-core/releases" rel="noopener noreferrer"&gt;openJiuwen Agent Core&lt;/a&gt; (&lt;code&gt;openjiuwen&lt;/code&gt;), which contains the SwarmFlow engine. Pin both versions in &lt;code&gt;requirements.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;workswarm==0.2.7
openjiuwen==0.1.19
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clone the repo and run the proof:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/manishh/openjiuwen--swarmflow-predictable-workflows-proof-first
&lt;span class="nb"&gt;cd &lt;/span&gt;openjiuwen--swarmflow-predictable-workflows-proof-first
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env        &lt;span class="c"&gt;# put your key in OPENAI_API_KEY (no quotes, no spaces), or export it instead&lt;/span&gt;
./prove &lt;span class="nt"&gt;--repeat&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;prove&lt;/code&gt; script handles the entire setup for you. It creates a virtual environment (&lt;code&gt;.venv&lt;/code&gt;) and installs the pinned packages. For each repeat, it then creates a fresh WorkSwarm home in a temp directory with &lt;code&gt;jiuwenswarm-init&lt;/code&gt;, so your local &lt;code&gt;~/.jiuwenswarm&lt;/code&gt; is never touched.&lt;/p&gt;

&lt;p&gt;Next, it starts &lt;code&gt;jiuwenswarm-start app&lt;/code&gt;. Your API key goes only into the server's process environment. The script then drives the Leader over the same WebSocket API the web UI uses (&lt;code&gt;ws://127.0.0.1:19000/ws&lt;/code&gt;). It uses WebSocket because the headless process SDK has no Cluster mode, and SwarmFlow runs only in Cluster mode.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;./prove&lt;/code&gt; also prints a masked form of the key it found: &lt;code&gt;Using OPENAI_API_KEY=&lt;/code&gt; followed by the key's first 8 and last 6 characters. This allows you to check that it picked up the right key.&lt;/p&gt;

&lt;p&gt;Each repeat takes about 7 minutes (~395 seconds in the verified runs) and uses roughly 1.4M tokens, mostly cached prompt reads. The human-checkpoint scenario uses about 750k of those. Each of the three budget scenarios uses between 110k and 290k.&lt;/p&gt;

&lt;p&gt;The diagram below shows one repeat (a &lt;code&gt;./prove&lt;/code&gt; script run). The proof harness plays two parts. It acts as the chat user who asks the Leader to run each script. It also acts as the reviewer who answers the checkpoint. This way, a repeat runs from start to finish without waiting for you (the &lt;code&gt;User&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    autonumber
    actor Dev as User
    participant H as Proof harness
    participant L as Team Leader
    participant SF as SwarmFlow
    participant E as Evidence on disk

    Dev-&amp;gt;&amp;gt;H: ./prove
    H-&amp;gt;&amp;gt;L: Start server, ask it to run slugify_pipeline.py
    L-&amp;gt;&amp;gt;SF: Launch the pipeline

    rect rgba(76,175,80,0.10)
    Note over H,E: Scenario A: human checkpoint and partial re-run
    SF-&amp;gt;&amp;gt;E: Run spec and codegen, save both to the journal
    SF-&amp;gt;&amp;gt;H: Ask the reviewer to approve
    H-&amp;gt;&amp;gt;SF: Reply "add max_len, cap at 40"
    SF-&amp;gt;&amp;gt;L: Send edit points
    L-&amp;gt;&amp;gt;SF: Edit the script, relaunch with the same run_id
    E-&amp;gt;&amp;gt;SF: spec is replayed from the journal (CACHE_HIT)
    SF-&amp;gt;&amp;gt;E: codegen runs again (CACHE_MISS)
    SF-&amp;gt;&amp;gt;H: Ask the reviewer again
    H-&amp;gt;&amp;gt;SF: Reply "approve"
    SF-&amp;gt;&amp;gt;H: docs runs, status is completed
    end

    rect rgba(156,39,176,0.10)
    Note over H,E: Scenarios B, C, D: token ceilings
    H-&amp;gt;&amp;gt;SF: Run the budget demo with no limit, a per-run limit, and a session limit
    SF-&amp;gt;&amp;gt;H: Status is completed, failed, or stopped
    end

    H-&amp;gt;&amp;gt;E: Check the journal, logs, and trace
    H-&amp;gt;&amp;gt;Dev: Report with pass or fail for each check&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;SwarmFlow is off by default. The test switches it on in the home's &lt;code&gt;config/config.yaml&lt;/code&gt; with these lines from &lt;code&gt;harness/setup_home.py&lt;/code&gt;, and you'd set the same keys in your own install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preferred_language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;              &lt;span class="c1"&gt;# init defaults to zh when it cannot prompt
&lt;/span&gt;    &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;setup_guide&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;         &lt;span class="c1"&gt;# first-run onboarding distracts the Leader
&lt;/span&gt;    &lt;span class="n"&gt;team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;modes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;team&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jiuwen_team&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enable_swarmflow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;               &lt;span class="c1"&gt;# SwarmFlow is off by default
&lt;/span&gt;    &lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;swarmflow_budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3_000_000&lt;/span&gt;          &lt;span class="c1"&gt;# default team ceiling (chat.send may override per session)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model settings go in the home's &lt;code&gt;config/.env&lt;/code&gt;, with no &lt;code&gt;API_KEY&lt;/code&gt; line on disk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MODEL_PROVIDER=OpenAI&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ENDPOINT_PROFILE=openai&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;API_BASE=https://api.openai.com/v1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MODEL_NAME=gpt-5.5&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For platform installers and the web UI, see the &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm" rel="noopener noreferrer"&gt;WorkSwarm README&lt;/a&gt; and the &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm/blob/develop/docs/README_EN.md" rel="noopener noreferrer"&gt;English docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to define workflow stages in Python
&lt;/h2&gt;

&lt;p&gt;A SwarmFlow pipeline is a Python file. It imports &lt;code&gt;agent&lt;/code&gt;, &lt;code&gt;human&lt;/code&gt;, and &lt;code&gt;phase&lt;/code&gt; from the &lt;code&gt;swarmflow&lt;/code&gt; module and defines an &lt;code&gt;async def run(args)&lt;/code&gt;. The file declares the stages, and the Leader runs it with its &lt;code&gt;swarmflow&lt;/code&gt; tool. The order of the &lt;code&gt;await&lt;/code&gt; calls sets the order of the stages, so every run follows the same sequence.&lt;/p&gt;

&lt;p&gt;The pipeline in &lt;code&gt;pipeline/slugify_pipeline.py&lt;/code&gt; starts with a &lt;code&gt;META&lt;/code&gt; block. SwarmFlow reads &lt;code&gt;META&lt;/code&gt; before running anything, so it must be a pure literal (plain values only, with no code to evaluate). Each phase title must match a &lt;code&gt;phase()&lt;/code&gt; call further down the file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;META&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slugify-pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Spec -&amp;gt; code generation -&amp;gt; human review -&amp;gt; docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phases&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Spec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Codegen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# The one line a reviewer's edit will change. Every agent() call is journaled under
# its position in the script plus a hash of its prompt, so changing this constant
# changes only the codegen call's hash (and anything downstream that embeds its output).
&lt;/span&gt;&lt;span class="n"&gt;CODEGEN_RULES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use only the standard library.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comment above &lt;code&gt;CODEGEN_RULES&lt;/code&gt; describes the journal, which the next section builds on. Each &lt;code&gt;agent()&lt;/code&gt; call is recorded under its position in the script (&lt;code&gt;[["call", 0]]&lt;/code&gt;, &lt;code&gt;[["call", 1]]&lt;/code&gt;, and so on) plus a signature hashed from its prompt. The Leader reads the script before editing it. In the proof runs, the comment also guided where the Leader made its edit.&lt;/p&gt;

&lt;p&gt;The first two stages are plain &lt;code&gt;agent()&lt;/code&gt; calls. The &lt;code&gt;label&lt;/code&gt; gives the call a name in the journal and the run tree. The &lt;code&gt;phase&lt;/code&gt; argument attaches the call to a phase in &lt;code&gt;META&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# swarmflow() passes args as a string; structured input is JSON we parse ourselves.
&lt;/span&gt;    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Spec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;spec&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;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;In 3 short bullet points, specify a Python function slugify(title) for: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;feature&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return only the bullets.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Spec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Codegen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;code&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;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write ONLY the Python source (no prose, no markdown fences) implementing this spec. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;CODEGEN_RULES&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Spec:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;codegen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Codegen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To launch the pipeline, the test sends the Leader a message that names the tool. Naming the tool makes the Leader call it directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run swarmflow(script_path='/tmp/swarmflow-proof-1-638pm8wc/scripts/slugify_pipeline.py', args='{"feature": "turn blog post titles into URL slugs"}').
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Leader called &lt;code&gt;swarmflow&lt;/code&gt; with those arguments, and the tool returned &lt;code&gt;run_id=wf_e0b31fee9773&lt;/code&gt;. This &lt;code&gt;run_id&lt;/code&gt; names the run's journal file. You will need it again when the run resumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to add a human-in-the-loop checkpoint
&lt;/h2&gt;

&lt;p&gt;The review stage is a single &lt;code&gt;await human(...)&lt;/code&gt; call. The prompt includes the generated code, so the reviewer can see exactly what they are approving.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="nf"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;review&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;human&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review the generated code. Reply &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; to continue, or describe the edit you want.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;docs&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;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write a two-sentence docstring summary for this code:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the run reaches &lt;code&gt;human()&lt;/code&gt;, it pauses and the question arrives as a &lt;code&gt;chat.ask_user_question&lt;/code&gt; event with &lt;code&gt;source=swarmflow_human&lt;/code&gt;. The &lt;code&gt;Docs&lt;/code&gt; phase stays &lt;code&gt;planned&lt;/code&gt;, which means it has not started. The run tree below is built from the run's &lt;code&gt;workflow.updated&lt;/code&gt; events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wf_e0b31fee9773  slugify-pipeline  [running]  run tokens=49339
  budget/session : spent=179435 total=3000000 exhausted=False
  budget/workflow: spent=49339 total=None exhausted=False
  ├─ phase Spec [completed]
  │   └─ spec [completed] tokens=24643
  ├─ phase Codegen [completed]
  │   └─ codegen [completed] tokens=24696
  ├─ phase Review [running]
  │   └─ unnamed human [waiting_for_human] tokens=None
  ├─ phase Docs [planned]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated &lt;code&gt;slugify(title: str)&lt;/code&gt; had no length limit. When executed on a long title, it returned an 80-character slug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"slug": "how-to-keep-an-ai-workflow-predictable-using-jiuwenswarm-s-swarmflow-checkpoints", "len": 80, "params": ["title"]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the demo, the reviewer is the proof script. &lt;code&gt;./prove&lt;/code&gt; must give the same answer on every repeat. So &lt;code&gt;harness/run_proof.py&lt;/code&gt; waits for the checkpoint question and sends two scripted replies over the WebSocket. The first is the edit request shown below. The second, sent later, is &lt;code&gt;approve&lt;/code&gt;. This means &lt;code&gt;./prove&lt;/code&gt; never actually stops to ask you anything.&lt;/p&gt;

&lt;p&gt;When you run your own pipeline in Cluster mode, a human answers instead. As of WorkSwarm 0.2.7, the bundled web UI shows a &lt;code&gt;swarmflow_human&lt;/code&gt; question as a question card in the chat. It sends your typed answer back in the same &lt;code&gt;chat.swarmflow_reply&lt;/code&gt; message. The proof does not exercise this web flow, it uses the 'headless' mode instead.&lt;/p&gt;

&lt;p&gt;The proof script answered with this edit request. It was sent as &lt;code&gt;chat.swarmflow_reply&lt;/code&gt; with the &lt;code&gt;run_id&lt;/code&gt; and &lt;code&gt;correlation_id&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Edit the codegen step: slugify must take a max_len parameter (default 40) and truncate the slug to that length. Then re-run.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SwarmFlow makes an extra model call to sort every human reply into one of two intents: &lt;code&gt;continue&lt;/code&gt; or &lt;code&gt;edit_rerun&lt;/code&gt;. There is no built-in "reject". To reject an output, have your script inspect the &lt;code&gt;review&lt;/code&gt; string that &lt;code&gt;human()&lt;/code&gt; returns. You can then raise an error or branch on it.&lt;/p&gt;

&lt;p&gt;This reply came back as &lt;code&gt;edit_rerun&lt;/code&gt;, and three things followed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The run went to &lt;code&gt;paused&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The journal gained a pause record with &lt;code&gt;"pause_reason": "early_return"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs&lt;/code&gt; had no journal record, so it had not run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engine then handed the Leader the edit points. Interestingly, it logged this handoff at &lt;code&gt;ERROR&lt;/code&gt; level, as a failed tool task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;... The user asked to edit the script and re-run.\nEdit points: Modify the code generation step so slugify accepts a max_len parameter with default value 40, and truncates the generated slug to at most max_len characters. Then re-run the workflow.\n...Edit the on-disk script accordingly (do not change META.name). Edit minimally and reuse the existing script content as much as possible, refining it rather than rewriting. 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With no further prompt, the &lt;code&gt;gpt-5.5&lt;/code&gt; Leader made one &lt;code&gt;edit_file&lt;/code&gt; call to change the script. The test never touches the script itself. Here is the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-CODEGEN_RULES = "Use only the standard library."
&lt;/span&gt;&lt;span class="gi"&gt;+CODEGEN_RULES = "Use only the standard library. Implement slugify(title, max_len=40) and truncate the generated slug to at most max_len characters."
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Leader then relaunched the run with the same &lt;code&gt;run_id&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;swarmflow({"script_path": "/tmp/swarmflow-proof-1-638pm8wc/scripts/slugify_pipeline.py", "resume_id": "wf_e0b31fee9773", "args": "{\"feature\": \"turn blog post titles into URL slugs\"}"})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On resume, SwarmFlow reloaded the journal. It compared each call's signature (a hash of its prompt) with the stored one. The &lt;code&gt;spec&lt;/code&gt; prompt had not changed, so SwarmFlow replayed the saved result and made no model call. The &lt;code&gt;codegen&lt;/code&gt; prompt includes &lt;code&gt;CODEGEN_RULES&lt;/code&gt;, so its signature changed and it ran live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;... [wf] journal loaded: prior_records=4 ...
2026-10-09 15:14:17 | team | sess_a54f49d1f | WARNING | [wf] agent 'spec' key=[["call", 0]] CACHE_HIT sig=5b0917e72813
2026-10-09 15:14:17 | team | sess_a54f49d1f | WARNING | [wf] agent 'codegen' key=[["call", 1]] CACHE_MISS sig=dc49daeca408 (live run)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The replayed &lt;code&gt;spec&lt;/code&gt; text was byte-identical to the first pass. The new code returned a 40-character slug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"slug": "how-to-keep-an-ai-workflow-predictable-u", "len": 40, "params": ["title", "max_len"]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;human()&lt;/code&gt; call is journaled as well. Its prompt now contained the new code, so its signature changed and the reviewer was asked again. The proof script's second reply, &lt;code&gt;approve&lt;/code&gt;, was classified as &lt;code&gt;continue&lt;/code&gt;. Then &lt;code&gt;docs&lt;/code&gt; ran and the journal was sealed.&lt;/p&gt;

&lt;p&gt;Each line below shows a call's position, label, signature, and token count. The call at position 2 is the &lt;code&gt;human()&lt;/code&gt; step, so it has no label. The last line seals the run as &lt;code&gt;completed&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[["call", 1]] label=codegen sig=dc49daeca408 tokens=24950
[["call", 2]] label=None sig=83f5ee8acb26 tokens=24379
[["call", 3]] label=docs sig=f0c7bc447b8f tokens=24687
{"key": "[[\"__run__\", 9223372036854775807, \"seal\", \"wf_e0b31fee9773\"]]", "type": "seal", "terminal_status": "completed"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diagram below clearly depitcs the states a typical run can move through.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; running: Leader calls swarmflow(script_path, args)
    running --&amp;gt; waiting_for_human: human() reached, later phases stay planned
    waiting_for_human --&amp;gt; running: reply classified continue
    waiting_for_human --&amp;gt; paused: reply classified edit_rerun (pause record early_return)
    paused --&amp;gt; running: Leader edits script, relaunches with resume_id (matching signatures replay as CACHE_HIT)
    running --&amp;gt; completed: last stage done (seal record)
    running --&amp;gt; failed: per-run workflow_token_limit exhausted (pause record)
    running --&amp;gt; stopped: team swarmflow_budget exhausted (seal record)
    completed --&amp;gt; [*]
    stopped --&amp;gt; [*]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The review step also costs tokens. The &lt;code&gt;unnamed human&lt;/code&gt; entry used 24,379 tokens, because classifying and formatting the reply each run model calls. This cost repeats every time the checkpoint asks again.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to set per-run and team token ceilings
&lt;/h2&gt;

&lt;p&gt;SwarmFlow enforces two ceilings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-run ceiling:&lt;/strong&gt; &lt;code&gt;META["workflow_token_limit"]&lt;/code&gt; in the script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team (session) ceiling:&lt;/strong&gt; &lt;code&gt;swarmflow_budget&lt;/code&gt;, sent with each &lt;code&gt;chat.send&lt;/code&gt; the same way the web UI sends it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The team ledger counts the Leader's own model calls as well as the worker agents' calls. It gates the Leader too.&lt;/p&gt;

&lt;p&gt;The demo script &lt;code&gt;pipeline/budget_demo.py&lt;/code&gt; chains three agents and sets the per-run limit to 30,000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;META&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;budget-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outline -&amp;gt; draft -&amp;gt; headline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phases&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Outline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Headline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="c1"&gt;# Per-run token ceiling for this run only (the team-wide ceiling is set separately).
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workflow_token_limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This limit looks generous for three one-sentence prompts. But each SwarmFlow worker is a full agent with a large system prompt. One &lt;code&gt;agent()&lt;/code&gt; call cost about 25k tokens with &lt;code&gt;gpt-5.5&lt;/code&gt;. The run crossed 30,000 during &lt;code&gt;draft&lt;/code&gt;, so &lt;code&gt;headline&lt;/code&gt; never started.&lt;/p&gt;

&lt;p&gt;For the budget scenarios, the Leader prompt adds "Do not edit the script and do not relaunch it yourself." The diagnostic tells the Leader to redesign the script and relaunch. The demo needs to show the stopped run as it is, so the Leader must not do that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wf_826df91268e1  budget-demo  [failed]  run tokens=49258
  budget/session : spent=222723 total=3000000 exhausted=False
  budget/workflow: spent=49258 total=30000 exhausted=True
  budget_exhausted_scope=workflow
...
  ├─ phase Headline [failed]
  │   └─ headline [failed] tokens=None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diagnostic reaches the Leader as a follow-up message. The test reads it from the home's &lt;code&gt;.jiuwenswarm/.trace/&lt;/code&gt; store. It names the layer that tripped, the heaviest phases, and the recovery path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Swarmflow budget exhausted] run_id=wf_826df91268e1
workflow token budget exhausted: 49258/30000
Triggered layer: workflow (per-run) (spent=49258/30000). Heaviest phases: Draft(24653), Outline(24605). This ceiling is the run's per-run token budget (independent of the session total), set by the user and must NOT be changed; you must redesign the workflow to consume fewer tokens (simplify / fewer agents / cheaper model) and relaunch. Relaunch by passing resume_id=&amp;lt;the run_id above&amp;gt; together with the edited script_path: ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run ends as &lt;code&gt;failed&lt;/code&gt; with a &lt;code&gt;workflow_budget_exhausted&lt;/code&gt; pause record, so it can still be resumed. The guidance warns that replayed calls are "re-billed from their stored tokens". In other words, a resume counts them against the same ceiling again. The proof did not test a resume after exhaustion.&lt;/p&gt;

&lt;p&gt;For the team ceiling, the test ran &lt;code&gt;budget_demo_unlimited.py&lt;/code&gt;, which has no per-run limit. It set &lt;code&gt;swarmflow_budget&lt;/code&gt; to the Leader's pre-launch spend plus one worker call. Both values were measured in an unconstrained control run. In repeat 1, that came to 43,218 + 24,643 = 67,861. A fixed small number would have stopped the Leader before it launched anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wf_df24a8c15beb  budget-demo-unlimited  [stopped]  run tokens=24607
  budget/session : spent=111143 total=67861 exhausted=True
  budget/workflow: spent=24607 total=None exhausted=False
  budget_exhausted_scope=session
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Triggered layer: session (shared) (spent=111143/67861). Heaviest phases: Outline(24607). This is the team's shared (session-wide) token ceiling and is currently exhausted. Until the ceiling is raised, relaunching or starting a new workflow will hit the same gate immediately — raise the ceiling and open a new session before retrying swarmflow.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, the journal got a &lt;code&gt;seal&lt;/code&gt; record with &lt;code&gt;"terminal_status": "stopped"&lt;/code&gt;. The Leader is gated by the same ledger, so it sent no final message.&lt;/p&gt;

&lt;p&gt;Use the per-run limit to cap a single script you expect to iterate on. Set &lt;code&gt;swarmflow_budget&lt;/code&gt; as the hard spend cap for the whole team session.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to monitor a run from its workflow.updated events
&lt;/h2&gt;

&lt;p&gt;WorkSwarm also has an optional terminal UI, &lt;code&gt;jiuwenswarm-tui&lt;/code&gt;. This tutorial monitored runs from the event stream that the UIs read. That approach works wherever WorkSwarm runs.&lt;/p&gt;

&lt;p&gt;The run trees shown earlier come from the &lt;code&gt;workflow.updated&lt;/code&gt; events on the web channel WebSocket. Each event carries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The run status.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;token_count&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Both budget ledgers (&lt;code&gt;budget&lt;/code&gt; and &lt;code&gt;workflow_budget&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The phases, with per-agent status and tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These events are deltas. Each one carries only the phases that changed. A monitor therefore has to merge them into one snapshot, as &lt;code&gt;merged()&lt;/code&gt; does in &lt;code&gt;harness/run_proof.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is_sess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;etype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workflow.updated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;wf_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;wf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wf_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;wf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phases&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ph&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;wf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phases&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}})&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]:&lt;/span&gt;
                &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across the four scenarios, the merged trees showed these phase and agent statuses: &lt;code&gt;planned&lt;/code&gt;, &lt;code&gt;running&lt;/code&gt;, &lt;code&gt;waiting_for_human&lt;/code&gt;, &lt;code&gt;paused&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt; and &lt;code&gt;stopped&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;swarmflow&lt;/code&gt; tool also supports run control. You pass &lt;code&gt;resume_id&lt;/code&gt; together with an &lt;code&gt;action&lt;/code&gt;, and the tool routes pause, resume and stop requests to the background-task controller (&lt;code&gt;tool_swarmflow.py::_control_run&lt;/code&gt;). The proof does not exercise this, so test it before you rely on it.&lt;/p&gt;

&lt;p&gt;To see the events for yourself, run &lt;code&gt;./prove --repeat 1&lt;/code&gt; and open &lt;code&gt;proof-out/repeat-1/transcript.md&lt;/code&gt;. Confirm that the &lt;code&gt;unnamed human [waiting_for_human]&lt;/code&gt; entry sits under &lt;code&gt;phase Review&lt;/code&gt;, while &lt;code&gt;phase Docs&lt;/code&gt; is still &lt;code&gt;planned&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to build next with SwarmFlow
&lt;/h2&gt;

&lt;p&gt;You now have a working SwarmFlow setup with three concrete capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed stage order&lt;/strong&gt; declared in a Python file, so every run follows the same path regardless of what the Leader decides at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A human checkpoint&lt;/strong&gt; that pauses the run, accepts an edit request, and resumes under the same &lt;code&gt;run_id&lt;/code&gt; with unchanged stages replayed from the journal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two token ceilings&lt;/strong&gt; that each stop a run with layer-specific advice the Leader can act on immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Swap the slugify stages for real stages in your pipeline, put &lt;code&gt;human()&lt;/code&gt; right before the most expensive (or crucial) step, and size &lt;code&gt;workflow_token_limit&lt;/code&gt; from the per-agent token counts in your own run tree.&lt;/p&gt;

&lt;p&gt;Star the &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm" rel="noopener noreferrer"&gt;WorkSwarm repo&lt;/a&gt;, follow the openJiuwen-ai organization for releases, and check the &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm/blob/develop/docs/README_EN.md" rel="noopener noreferrer"&gt;English docs&lt;/a&gt; for the full configuration reference. Contributions and issue reports are welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is SwarmFlow in WorkSwarm?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SwarmFlow is WorkSwarm's scripting layer for Cluster mode (team mode) that fixes the execution order of a multi-agent pipeline in a Python file. Each stage is an &lt;code&gt;await agent()&lt;/code&gt; or &lt;code&gt;await human()&lt;/code&gt; call, and the Leader runs the file with its &lt;code&gt;swarmflow&lt;/code&gt; tool, so the stage sequence is identical on every run regardless of runtime Leader decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does the SwarmFlow journal prevent redundant LLM calls after an edit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each &lt;code&gt;agent()&lt;/code&gt; call is recorded under its position in the script and a signature hashed from its prompt. On resume, SwarmFlow compares each call's current signature against the stored one. Calls whose prompts are unchanged replay from the journal as &lt;code&gt;CACHE_HIT&lt;/code&gt; without hitting the model, while calls whose prompts changed run live. In the proof run, editing &lt;code&gt;CODEGEN_RULES&lt;/code&gt; produced a &lt;code&gt;CACHE_HIT&lt;/code&gt; for &lt;code&gt;spec&lt;/code&gt; and a &lt;code&gt;CACHE_MISS&lt;/code&gt; for &lt;code&gt;codegen&lt;/code&gt;, cutting the resume cost to a single live agent call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between the per-run token ceiling and the team token ceiling?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The per-run ceiling is &lt;code&gt;META["workflow_token_limit"]&lt;/code&gt; in the pipeline script and caps token spend for that single run. The team ceiling is &lt;code&gt;swarmflow_budget&lt;/code&gt;, set per &lt;code&gt;chat.send&lt;/code&gt;, and counts every model call in the session including the Leader's own calls. Exhausting the per-run ceiling ends the run as &lt;code&gt;failed&lt;/code&gt; with a resumable pause record. Exhausting the team ceiling ends the run as &lt;code&gt;stopped&lt;/code&gt; and gates the Leader from launching anything new until the budget is raised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why doesn't &lt;code&gt;./prove&lt;/code&gt; ask me for input at the human checkpoint?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The proof script answers the human checkpoint for you. To prove the same behavior on every repeat, &lt;code&gt;harness/run_proof.py&lt;/code&gt; waits for the &lt;code&gt;chat.ask_user_question&lt;/code&gt; event and sends two scripted replies through &lt;code&gt;chat.swarmflow_reply&lt;/code&gt;, the edit request and then &lt;code&gt;approve&lt;/code&gt;. The checkpoint still blocks the run, and &lt;code&gt;docs&lt;/code&gt; doesn't start until a reply arrives. In your own Cluster-mode session, the WorkSwarm web UI shows the question as a card in the chat, and you can type your reply there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does SwarmFlow classify a human reply as an edit request vs. an approval?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SwarmFlow runs an extra model call on every &lt;code&gt;human()&lt;/code&gt; reply to classify it as either &lt;code&gt;continue&lt;/code&gt; or &lt;code&gt;edit_rerun&lt;/code&gt;. A reply of &lt;code&gt;approve&lt;/code&gt; routes to &lt;code&gt;continue&lt;/code&gt; and the pipeline resumes from the next stage. Any reply describing a change routes to &lt;code&gt;edit_rerun&lt;/code&gt;, which pauses the run, writes a pause record with &lt;code&gt;"pause_reason": "early_return"&lt;/code&gt;, and passes structured edit points to the Leader so it can modify the script and relaunch with &lt;code&gt;resume_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to a run that hits the team token ceiling mid-stage?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The run ends as &lt;code&gt;stopped&lt;/code&gt; with a &lt;code&gt;seal&lt;/code&gt; record instead of a resumable pause record. The Leader, gated by the same ledger, sends no final message. The diagnostic names the session layer, the heaviest phase, and the instruction to raise the ceiling and open a new session before retrying &lt;code&gt;swarmflow&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you prevent the Leader from automatically relaunching a budget-exhausted run?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add the instruction "Do not edit the script and do not relaunch it yourself." to the Leader prompt before triggering the budget scenario. Without it, the diagnostic message tells the Leader to redesign and relaunch, and &lt;code&gt;gpt-5.5&lt;/code&gt; will act on that instruction immediately, which obscures the stopped-run state you may want to inspect.&lt;/p&gt;




</description>
      <category>agents</category>
      <category>ai</category>
      <category>openjiuwen</category>
    </item>
    <item>
      <title>Debugging a silently failing AI workflow with Agent Core full-link observability</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Wed, 07 Oct 2026 14:01:56 +0000</pubDate>
      <link>https://dev.to/reclusivecoder/debugging-a-silently-failing-ai-workflow-with-agent-core-full-link-observability-4h3c</link>
      <guid>https://dev.to/reclusivecoder/debugging-a-silently-failing-ai-workflow-with-agent-core-full-link-observability-4h3c</guid>
      <description>&lt;p&gt;The most difficult AI workflows to debug are the ones that don't crash. A multi-step agent pipeline can quietly return a plausible-looking answer that's actually wrong, without ever throwing an error. One broken step early in the chain compounds into a wrong final output, and standard monitoring rarely catches it: the run finishes, the logs show green, and the problem only surfaces hours later when a downstream team flags that something's off.&lt;/p&gt;

&lt;p&gt;This tutorial walks through a concrete case using &lt;a href="https://github.com/openJiuwen-ai/agent-core/releases/tag/v0.1.18" rel="noopener noreferrer"&gt;openJiuwen Agent Core v0.1.18&lt;/a&gt;. You'll instrument a three-node pipeline (fetch → analyze → summarize) with OTEL-compatible spans (structured traces that record what each step did, in a format most observability tools understand). Then you will read back their output (latency, token counts, and a per-step failure flag), and pinpoint the exact node where the chain went wrong. The complete companion code is available at &lt;a href="https://github.com/manishh/openjiuwen-debugging-an-ai-workflow-that-fails-silently-with-agent" rel="noopener noreferrer"&gt;the tutorial repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why silent failures escape standard logs
&lt;/h2&gt;

&lt;p&gt;A silent failure happens when a step in an agent pipeline returns something that &lt;em&gt;looks like&lt;/em&gt; a normal result but isn't. No exception is raised, no log line turns red, and no HTTP error code shows up, even though the output is wrong. Standard Python logging and HTTP status codes are built to catch exceptions. Agent-specific failures often don't rise to that level, so they slip straight past.&lt;/p&gt;

&lt;p&gt;Three failure modes show up most often in production pipelines: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Incorrect tool selection&lt;/strong&gt;: The agent picks the wrong function for the job. That function still runs, still returns, and still fills the Observation (the agent's record of what a tool call returned) with data, just data that has nothing to do with the task. No exception fires, because nothing actually went wrong from the tool's point of view. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hallucinated tool arguments&lt;/strong&gt;: The agent calls the right tool, but with parameters it made up or left out. If the tool tolerates missing input rather than rejecting it, the call succeeds and returns something empty or nonsensical. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context truncation&lt;/strong&gt;: The prompt is too long for the model's context window (the amount of text it can consider at once), and the model API returns a normal HTTP 200 response with an empty list of completions. The token count comes back as zero, no error is raised, and the node exits as if nothing happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This matters more in a ReAct-style agent loop, which cycles through Thought, Action, and Observation, because each step can look successful on its own while the whole chain quietly drifts off course. A truncated completion still produces something that reads as a valid string, so the next Thought step picks it up and keeps going as if nothing were wrong. By the time the loop finishes, the final output is wrong, but there's still no exception and no red log line to explain why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample workflow setup
&lt;/h2&gt;

&lt;p&gt;You'll need Python 3.11, 3.12, or 3.13 (&lt;code&gt;openjiuwen&lt;/code&gt; doesn't yet support 3.14). Clone the repository, change into the project directory, and install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/manishh/openjiuwen-debugging-an-ai-workflow-that-fails-silently-with-agent
&lt;span class="nb"&gt;cd &lt;/span&gt;openjiuwen-debugging-an-ai-workflow-that-fails-silently-with-agent
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm/blob/develop/docs/en/Configuration.md" rel="noopener noreferrer"&gt;openJiuwen quickstart documentation&lt;/a&gt; for initial API key setup. The tutorial reads five environment variables: &lt;code&gt;OPENJIUWEN_API_KEY&lt;/code&gt;, &lt;code&gt;OPENJIUWEN_MODEL&lt;/code&gt;, &lt;code&gt;OPENJIUWEN_API_BASE&lt;/code&gt;, &lt;code&gt;OPENJIUWEN_PROVIDER&lt;/code&gt;, and &lt;code&gt;OPENJIUWEN_SSL_VERIFY&lt;/code&gt;. If you leave out &lt;code&gt;OPENJIUWEN_API_KEY&lt;/code&gt;, the code runs in offline simulation mode, which still emits and analyzes spans and only skips the live model call.&lt;/p&gt;

&lt;p&gt;The tutorial's target pipeline is a sentiment-analysis workflow with three connected steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fetch_data&lt;/code&gt; retrieves and preprocesses the source text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;analyze_sentiment&lt;/code&gt; classifies its sentiment. This is the node that simulates a silent failure.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;summarize&lt;/code&gt; generates a human-readable summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Thought/Action/Observation loop above describes how a live &lt;code&gt;ReActAgent&lt;/code&gt; reasons. The pipeline in &lt;code&gt;src/main.py&lt;/code&gt; simulates the same failure pattern with three plain Python functions, so it runs deterministically offline without a live agent loop. The agent construction in &lt;code&gt;examples/02_workflow_agent_setup.py&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentiment-analysis-pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentiment-analysis-pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fetch -&amp;gt; analyse -&amp;gt; summarise sentiment pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgentConfig&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;configure_model_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROVIDER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;API_BASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;verify_ssl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SSL_VERIFY&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 below drives the three-node pipeline:
#   node_fetch_data -&amp;gt; node_analyze_sentiment -&amp;gt; node_summarize
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; created.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ReActAgent&lt;/code&gt; is &lt;code&gt;openjiuwen&lt;/code&gt;'s current general-purpose agent, built around the Thought/Action/Observation loop described earlier. It decides at runtime which tool to call next, rather than following a hardcoded step order. &lt;/p&gt;

&lt;p&gt;The older &lt;code&gt;WorkflowAgent&lt;/code&gt;, which ran a fixed, user-defined node sequence, is deprecated as of &lt;code&gt;openjiuwen&lt;/code&gt; 0.1.18 in favor of this &lt;code&gt;AgentCard&lt;/code&gt; + &lt;code&gt;ReActAgentConfig&lt;/code&gt; pattern. This tutorial's pipeline order (fetch → analyze → summarize) is enforced by the plain Python function calls in &lt;code&gt;src/main.py&lt;/code&gt;, not by the agent, so the fixed order holds regardless of which agent class builds it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full-link observability in Agent Core
&lt;/h2&gt;

&lt;p&gt;Full-link observability in Agent Core means that every tool call, token count, and step latency is captured as a structured, OTEL-compatible span (a self-contained record of one unit of work). Chain those spans together and you get a complete execution trace that you can query programmatically or view in Agent Studio, with no third-party APM (Application Performance Monitoring) tool required.&lt;/p&gt;

&lt;p&gt;As of v0.1.18, Agent Core emits this telemetry natively. To capture and inspect those traces locally, you attach exporters to a &lt;code&gt;TracerProvider&lt;/code&gt; instance. The full setup is in &lt;code&gt;examples/01_setup_observability.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TracerProvider&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace.export&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SimpleSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ConsoleSpanExporter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace.export.in_memory_span_exporter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;InMemorySpanExporter&lt;/span&gt;

&lt;span class="n"&gt;exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemorySpanExporter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TracerProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_span_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SimpleSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ConsoleSpanExporter&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_span_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SimpleSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Always get the tracer from your provider, not from opentelemetry.trace.get_tracer().
&lt;/span&gt;&lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-core.tutorial&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SimpleSpanProcessor&lt;/code&gt; exports each span the moment it finishes, so the console output and the trace report you'll build later stay in the order the spans actually ran. Get your tracer from your own provider instance (&lt;code&gt;provider.get_tracer(...)&lt;/code&gt;) rather than from the global &lt;code&gt;opentelemetry.trace&lt;/code&gt; module. If Agent Core has already initialized the global provider, calling &lt;code&gt;trace.get_tracer()&lt;/code&gt; returns a tracer bound to the SDK's own exporter, and your spans go somewhere you can't see them.&lt;/p&gt;

&lt;p&gt;With the tracer in hand, wrap every step in a custom span that records four diagnostic signals. The broken &lt;code&gt;analyze_sentiment&lt;/code&gt; node in &lt;code&gt;examples/03_custom_spans.py&lt;/code&gt; shows what a silently failing node looks like once it’s instrumented:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;node_analyze_sentiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool.analyze_sentiment&lt;/span&gt;&lt;span class="sh"&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;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# Simulate: context window overflow caused the model to return
&lt;/span&gt;        &lt;span class="c1"&gt;# an empty choices list. Status code was 200 – no error logged.
&lt;/span&gt;        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# slightly elevated latency: the client waited for timeout
&lt;/span&gt;        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.tool.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analyze_sentiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.tool.token_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.step.latency_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;# This flag is the key signal in the trace: token_count==0 is a silent failure.
&lt;/span&gt;        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.step.silent_failure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the script (&lt;code&gt;python examples/03_custom_spans.py&lt;/code&gt;), and the span summary makes the failure obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Span summary:
  tool.fetch_data                          tokens=  312  latency=   40.15 ms  silent_failure=False
  tool.analyze_sentiment                   tokens=    0  latency=  150.15 ms  silent_failure=True
  tool.summarize                           tokens=  144  latency=   50.13 ms  silent_failure=False
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the &lt;code&gt;analyze_sentiment&lt;/code&gt; row: &lt;code&gt;tokens=0&lt;/code&gt; and &lt;code&gt;silent_failure=True&lt;/code&gt;, even though the node returned normally with no exception raised. Your latency numbers will vary slightly between runs, since they come from a real &lt;code&gt;time.sleep()&lt;/code&gt; call, but that row is the one to watch either way.&lt;/p&gt;

&lt;p&gt;The last line of the code snippet is the one that matters most. Every node sets &lt;code&gt;agent.step.silent_failure&lt;/code&gt;, not just the broken one, and it’s &lt;code&gt;True&lt;/code&gt; whenever &lt;code&gt;token_count == 0&lt;/code&gt;. That's what turns &lt;em&gt;"did this succeed?"&lt;/em&gt; into something you can actually query. Together, the four attributes (&lt;code&gt;agent.tool.name&lt;/code&gt;, &lt;code&gt;agent.tool.token_count&lt;/code&gt;, &lt;code&gt;agent.step.latency_ms&lt;/code&gt;, and &lt;code&gt;agent.step.silent_failure)&lt;/code&gt; are the signals you'll read in the next section. Because every node writes all four, the trace report has a consistent shape to scan no matter which node broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to read the trace output
&lt;/h2&gt;

&lt;p&gt;The diagram below shows the full-link trace for a broken run. The root &lt;code&gt;workflow.session&lt;/code&gt; span wraps all three tool spans, and &lt;code&gt;tool.analyze_sentiment&lt;/code&gt; is highlighted because its &lt;code&gt;agent.step.silent_failure&lt;/code&gt; attribute is &lt;code&gt;True&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["workflow.session&amp;lt;br/&amp;gt;root span"] --&amp;gt; B["tool.fetch_data&amp;lt;br/&amp;gt;tokens: 312&amp;lt;br/&amp;gt;latency: 40 ms&amp;lt;br/&amp;gt;silent_failure: False"]
    B --&amp;gt; C["tool.analyze_sentiment&amp;lt;br/&amp;gt;tokens: 0&amp;lt;br/&amp;gt;latency: 150 ms&amp;lt;br/&amp;gt;silent_failure: True"]
    C --&amp;gt; D["tool.summarize&amp;lt;br/&amp;gt;tokens: 144&amp;lt;br/&amp;gt;latency: 50 ms&amp;lt;br/&amp;gt;silent_failure: False"]
    style C fill:#ff6b6b,color:#fff&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Once the workflow finishes, iterate over &lt;code&gt;exporter.get_finished_spans()&lt;/code&gt; to read each span back; each one maps to a single node. The reporting loop in &lt;code&gt;examples/04_read_trace_output.py&lt;/code&gt; prints the broken and fixed runs one after the other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Span&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tokens&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;latency ms&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;failure&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;◄ FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.step.silent_failure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;agent.tool.token_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;agent.step.latency_ms&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it prints both reports, so you can compare them line by line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;========================================================================
BROKEN RUN (inject_failure=True)
========================================================================
Span                                 tokens  latency ms  failure
------------------------------------------------------------------------
tool.fetch_data                         312        40.0       OK
tool.analyze_sentiment                    0       150.0   ◄ FAIL
tool.summarize                          144        50.0       OK
workflow.session                          -           -       OK

========================================================================
FIXED RUN  (inject_failure=False)
========================================================================
Span                                 tokens  latency ms  failure
------------------------------------------------------------------------
tool.fetch_data                         312        40.0       OK
tool.analyze_sentiment                   87        40.0       OK
tool.summarize                          144        50.0       OK
workflow.session                          -           -       OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comparison shows the pattern to look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A healthy node&lt;/strong&gt; shows &lt;code&gt;token_count &amp;gt; 0&lt;/code&gt;, &lt;code&gt;latency_ms&lt;/code&gt; within the expected range for that step, and &lt;code&gt;OK&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A broken node&lt;/strong&gt; shows &lt;code&gt;token_count == 0&lt;/code&gt; because the model returned nothing, elevated &lt;code&gt;latency_ms&lt;/code&gt;, and &lt;code&gt;◄ FAIL&lt;/code&gt;.
In this simulation, the latency is elevated because the client waited out a timeout before accepting the empty response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running &lt;code&gt;src/main.py&lt;/code&gt; produces the same pattern from a single script, without the side-by-side comparison. Its &lt;code&gt;analyse_trace()&lt;/code&gt; function reads from the &lt;code&gt;InMemorySpanExporter&lt;/code&gt; and prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠  First broken span: tool.analyze_sentiment
   token_count  = 0
   latency_ms   = 150.24
   workflow.node= node_analyze_sentiment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Agent Studio's online debugging environment, the same execution graph appears visually in the session view, with the broken span flagged in the error log alongside its attributes. Because Agent Studio reads the same structured trace data Agent Core emits, you don't need to run &lt;code&gt;analyse_trace()&lt;/code&gt; by hand once you're working against a live session there.&lt;/p&gt;

&lt;p&gt;Each failure mode leaves its own signature in the spans:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;Span signature&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context truncation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;token_count == 0&lt;/code&gt; with elevated latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucinated arguments (no validation)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;token_count &amp;gt; 0&lt;/code&gt;, but the output is semantically empty or causes a schema error in a downstream span; latency is usually normal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucinated arguments (with the validation guard below)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;token_count == 0&lt;/code&gt;, normal latency, and an &lt;code&gt;agent.tool.arg_error&lt;/code&gt; attribute naming the bad argument&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incorrect tool selection&lt;/td&gt;
&lt;td&gt;An &lt;code&gt;agent.tool.name&lt;/code&gt; that doesn't match the expected step, with output that doesn't fit the task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Three targeted fixes from span evidence
&lt;/h2&gt;

&lt;p&gt;You now have the failing span in front of you. The next step is turning that evidence into a fix. Each of the three failure modes leaves a distinct span signature, and each signature points to one specific code change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context truncation (&lt;code&gt;token_count == 0&lt;/code&gt;, elevated latency)
&lt;/h3&gt;

&lt;p&gt;Add a token budget check before submitting the prompt. The fix in &lt;code&gt;examples/05_apply_fixes.py&lt;/code&gt; guards the analysis node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;MAX_PROMPT_TOKENS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3800&lt;/span&gt;  &lt;span class="c1"&gt;# leave headroom for the model's completion budget
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;node_analyze_sentiment_fixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool.analyze_sentiment&lt;/span&gt;&lt;span class="sh"&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;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;prompt_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;estimated_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_token_estimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;estimated_tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_PROMPT_TOKENS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Truncate to fit. In production use a summarisation sub-call instead.
&lt;/span&gt;            &lt;span class="n"&gt;chars_allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MAX_PROMPT_TOKENS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
            &lt;span class="n"&gt;prompt_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt_text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;chars_allowed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.prompt.truncated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Simulate the model call with a valid response after truncation.
&lt;/span&gt;        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.04&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;positive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;87&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;* 4&lt;/code&gt; uses the rough rule of thumb that one token is about four characters of English text. In production, replace &lt;code&gt;_token_estimate&lt;/code&gt; with a real tokenizer (tiktoken, for example). The span's &lt;code&gt;agent.prompt.truncated&lt;/code&gt; attribute records that truncation happened, so you can monitor for prompts that keep hitting the ceiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hallucinated tool arguments (&lt;code&gt;token_count &amp;gt; 0&lt;/code&gt; but output semantically empty, or argument schema errors in downstream spans)
&lt;/h3&gt;

&lt;p&gt;Validate arguments at the tool boundary and record any error in the span. The &lt;code&gt;invoke_tool_with_validation&lt;/code&gt; function in &lt;code&gt;examples/05_apply_fixes.py&lt;/code&gt; shows the pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke_tool_with_validation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.tool.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_validate_tool_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Reflect the validation error back into the span so the ReAct
&lt;/span&gt;            &lt;span class="c1"&gt;# Observation step can self-correct on the next iteration.
&lt;/span&gt;            &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.tool.arg_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.step.silent_failure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.tool.token_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the script exercises both a valid call and a rejected one, so you can see the guard working on real span output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- Fix 2: Argument validation (valid args) ---
  result={'output': 'Result from analyze_sentiment', 'token_count': 95}

--- Fix 2: Argument validation (hallucinated / missing args) ---
  result={'error': "Missing required args for analyze_sentiment: ['text']", 'token_count': 0}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rejected call still produces &lt;code&gt;token_count == 0&lt;/code&gt;, the same signal as context truncation. The difference is that it's caught before any model call happens, its latency stays normal, and the returned error names the missing argument. That's how you tell the two failure modes apart in a real trace: check the tool's result payload and the &lt;code&gt;agent.tool.arg_error&lt;/code&gt; attribute, not just the token count. Returning the structured error in the Observation also lets a &lt;code&gt;ReActAgent&lt;/code&gt; loop self-correct on the next iteration rather than continuing with a bad result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Incorrect tool selection (wrong &lt;code&gt;agent.tool.name&lt;/code&gt; in the span, output mismatches intent)
&lt;/h3&gt;

&lt;p&gt;Here the span shows a tool name that doesn't match the expected step. The fix is on the prompt side, so the agent picks the correct function on the first attempt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tighten the tool description schema.&lt;/li&gt;
&lt;li&gt;Add a worked example of the correct tool call.&lt;/li&gt;
&lt;li&gt;Constrain the output format, so the agent has less room to substitute a plausible-sounding alternative.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This fix changes the prompt rather than the tool code, so it has no span-level output of its own to show, and &lt;code&gt;examples/05_apply_fixes.py&lt;/code&gt; doesn't demonstrate it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying the pattern holds
&lt;/h3&gt;

&lt;p&gt;The smoke tests in &lt;code&gt;tests/smoke_test.py&lt;/code&gt; check that the underlying span mechanics work: that a span with &lt;code&gt;token_count == 0&lt;/code&gt; is picked up as &lt;code&gt;silent_failure == True&lt;/code&gt;, and that a multi-span workflow isolates the one broken node from the healthy ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_otel_silent_failure_detection&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;broken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;finished&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.step.silent_failure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;broken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected one silent-failure span&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;broken&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="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.tool.token_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These tests build their own spans directly rather than importing from &lt;code&gt;examples/05_apply_fixes.py&lt;/code&gt;. They confirm that the detection pattern itself is sound, not that any specific fix in this section works. After changing a fix, rerun the example script and compare its span report against the output shown above, then run &lt;code&gt;pytest tests/smoke_test.py&lt;/code&gt; to confirm the detection logic underneath still works.&lt;/p&gt;

&lt;p&gt;The output of &lt;code&gt;smoke_test.py&lt;/code&gt; should show &lt;code&gt;PASSED&lt;/code&gt; for five tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;collected 5 items                                                                                                                                                                   

tests/smoke_test.py::test_openjiuwen_sdk_imports PASSED                                                                                                                       [ 20%]
tests/smoke_test.py::test_otel_span_creation PASSED                                                                                                                           [ 40%]
tests/smoke_test.py::test_otel_silent_failure_detection PASSED                                                                                                                [ 60%]
tests/smoke_test.py::test_otel_multi_span_workflow PASSED                                                                                                                     [ 80%]
tests/smoke_test.py::test_openjiuwen_agent_construction PASSED                                                                                                                [100%]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;run_workflow(inject_failure=False)&lt;/code&gt; in &lt;code&gt;src/main.py&lt;/code&gt; shows what a &lt;em&gt;healthy&lt;/em&gt; run looks like after a fix: it returns the same &lt;code&gt;token_count=87&lt;/code&gt; result as the guard above, but it doesn't contain the fix logic itself. The guard, the validation, and the prompt-tightening code live in &lt;code&gt;examples/05_apply_fixes.py&lt;/code&gt;; &lt;code&gt;main.py&lt;/code&gt; only toggles between the broken and fixed outcomes to show the contrast in the trace report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;The debugging loop here is repeatable, and it's worth building into a habit rather than treating as a one-off fix:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Instrument every tool node with the same four span attributes: &lt;code&gt;agent.tool.name&lt;/code&gt;, &lt;code&gt;agent.tool.token_count&lt;/code&gt;, &lt;code&gt;agent.step.latency_ms&lt;/code&gt;, and &lt;code&gt;agent.step.silent_failure&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run the workflow and read back the finished spans.&lt;/li&gt;
&lt;li&gt;Scan for zero-token observations and latency spikes, and the broken node names itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same idea extends to multi-agent collaboration. Agent Core v0.1.18 added &lt;code&gt;ReliabilityRail&lt;/code&gt; to the &lt;code&gt;agent_teams&lt;/code&gt; package, which you can attach to a team of agents. Its detectors watch for repeated tool calls, model errors, tool errors, output-length problems, context-compaction issues, and agents bouncing work back and forth. When one trips, it can correct the problem locally, escalate to the team Leader, or notify you. Once you're running several agents in one session, those signals roll up into a per-session error rate, automating much of what you just did by hand.&lt;/p&gt;

&lt;p&gt;To explore the SDK and start tracing your own agents, see the &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm/blob/develop/docs/README_EN.md" rel="noopener noreferrer"&gt;openJiuwen documentation&lt;/a&gt;. To see full-link observability and Agent Studio in action, contact the openJiuwen team for a guided walkthrough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently asked questions (FAQs)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I know which node caused a wrong output when no exception was raised?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scan your finished spans for &lt;code&gt;agent.step.silent_failure: True&lt;/code&gt; combined with &lt;code&gt;agent.tool.token_count == 0&lt;/code&gt;. That combination identifies the exact node where the model returned nothing. Elevated &lt;code&gt;agent.step.latency_ms&lt;/code&gt; alongside a zero token count narrows the cause to context truncation specifically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between a context truncation failure and a hallucinated argument failure in the span data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Context truncation produces &lt;code&gt;token_count == 0&lt;/code&gt; with elevated latency. Without argument validation, a hallucinated argument failure typically shows &lt;code&gt;token_count &amp;gt; 0&lt;/code&gt; (the model did respond), but the output is semantically empty or triggers a schema error in a downstream span, usually with normal latency. With the validation guard in place, the bad call is rejected before the model runs, so it shows &lt;code&gt;token_count == 0&lt;/code&gt; with normal latency and an &lt;code&gt;agent.tool.arg_error&lt;/code&gt; attribute that names the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need Agent Studio to use full-link observability, or does it work locally?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Full-link observability works entirely locally. Attach an &lt;code&gt;InMemorySpanExporter&lt;/code&gt; and a &lt;code&gt;ConsoleSpanExporter&lt;/code&gt; to your &lt;code&gt;TracerProvider&lt;/code&gt;, run the workflow, call &lt;code&gt;provider.force_flush()&lt;/code&gt;, and iterate over &lt;code&gt;exporter.get_finished_spans()&lt;/code&gt;. Agent Studio reads the same structured trace data and shows it visually, but emitting the spans requires no external service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why should I get the tracer from my own provider instance rather than the global &lt;code&gt;opentelemetry.trace&lt;/code&gt; module?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If Agent Core has already initialized the global provider, calling &lt;code&gt;trace.get_tracer()&lt;/code&gt; returns a tracer bound to the SDK's exporter. Your custom spans then go to the wrong destination and won't appear in your &lt;code&gt;InMemorySpanExporter&lt;/code&gt;. Always call &lt;code&gt;provider.get_tracer(...)&lt;/code&gt; on your own &lt;code&gt;TracerProvider&lt;/code&gt; instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I apply the same four-attribute span pattern to a multi-agent team, not just a single pipeline?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The &lt;code&gt;openjiuwen.agent_teams&lt;/code&gt; package lets you attach &lt;code&gt;ReliabilityRail&lt;/code&gt; (available in v0.1.18+) to a team of agents. It adds configurable detection for repeated tool calls, model errors, tool errors, output-length anomalies, context compaction, and agents bouncing work back and forth. In production, the per-session error rates from Agent Core's structured traces become the signal you alert on across the whole team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should I do if prompts consistently hit the token ceiling after I add the budget check?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Replace the truncation approach with a summarization sub-call that condenses the input before it reaches the analysis node. The &lt;code&gt;agent.prompt.truncated&lt;/code&gt; span attribute tells you how often truncation fires, so you can set a threshold and trigger the sub-call automatically when the attribute appears more than a configurable number of times per session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does the ReActAgent loop self-correct after a hallucinated argument error?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;invoke_tool_with_validation&lt;/code&gt; detects an invalid argument, it returns a structured &lt;code&gt;{"error": ..., "token_count": 0}&lt;/code&gt; dict into the Observation. The &lt;code&gt;ReActAgent&lt;/code&gt; reads that Observation on the next Thought step and can revise its tool call with corrected arguments, instead of continuing from a bad result with no signal that anything went wrong.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>agents</category>
      <category>openjiuwen</category>
    </item>
    <item>
      <title>How to get multiple AI agents working together on one task: a WorkSwarm walkthrough</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Wed, 07 Oct 2026 13:48:01 +0000</pubDate>
      <link>https://dev.to/reclusivecoder/how-to-get-multiple-ai-agents-working-together-on-one-task-a-workswarm-walkthrough-8ak</link>
      <guid>https://dev.to/reclusivecoder/how-to-get-multiple-ai-agents-working-together-on-one-task-a-workswarm-walkthrough-8ak</guid>
      <description>&lt;p&gt;Most multi-agent demos fall apart at the handoff. One agent returns a paragraph when the next one expects a list. A subtask stalls, and nothing notices. Before long, you are writing dispatch loops and retry logic around every model call.&lt;/p&gt;

&lt;p&gt;This tutorial shows a better way. You will build a Cluster-mode Swarm in &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm" rel="noopener noreferrer"&gt;WorkSwarm&lt;/a&gt; (formerly JiuwenSwarm), where a Leader hands a product-analysis task to three specialist Teammates. Every step's output is checked against a JSON schema (an agreed output format), so the next agent always gets the shape it expects. You can even test the whole pipeline offline with &lt;code&gt;pytest&lt;/code&gt; before it touches a paid model.&lt;/p&gt;

&lt;p&gt;Along the way, you will&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write the SwarmFlow script that defines the pipeline&lt;/li&gt;
&lt;li&gt;configure the models your Teammates use&lt;/li&gt;
&lt;li&gt;run the smoke tests offline&lt;/li&gt;
&lt;li&gt;watch a live run from the Web UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete code is in the &lt;a href="https://github.com/manishh/openjiuwen-how-to-get-multiple-ai-agents-actually-working-together" rel="noopener noreferrer"&gt;companion repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites and installation
&lt;/h2&gt;

&lt;p&gt;You will need: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.11, 3.12, or 3.13&lt;/li&gt;
&lt;li&gt;API credentials for at least one supported LLM provider&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WorkSwarm is published on PyPI as &lt;a href="https://pypi.org/project/workswarm/" rel="noopener noreferrer"&gt;workswarm&lt;/a&gt;, and installing it also pulls in Agent Core and the SwarmFlow engine. &lt;/p&gt;

&lt;p&gt;The companion repository pins version 0.2.6 in &lt;code&gt;requirements.txt&lt;/code&gt;. Clone it and install its dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/manishh/openjiuwen-how-to-get-multiple-ai-agents-actually-working-together
&lt;span class="nb"&gt;cd &lt;/span&gt;openjiuwen-how-to-get-multiple-ai-agents-actually-working-together
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, copy the companion repository's example config into the WorkSwarm config directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.jiuwenswarm/config/
&lt;span class="nb"&gt;cp &lt;/span&gt;config/config.yaml ~/.jiuwenswarm/config/config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then initialize the workspace and start the services. The command-line tools kept their original names after the rename, so they still start with &lt;code&gt;jiuwenswarm&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jiuwenswarm-init
jiuwenswarm-start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;jiuwenswarm-init&lt;/code&gt; creates the &lt;code&gt;~/.jiuwenswarm/&lt;/code&gt; workspace on first run. Because you’ve already copied &lt;code&gt;config.yaml&lt;/code&gt; into that directory, init merges it with the default version, so none of your settings are lost.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jiuwenswarm-start&lt;/code&gt; launches the Gateway, the AgentServer, and the Web UI at &lt;code&gt;http://localhost:5173&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;If you'd rather skip pip, the &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm/blob/develop/docs/en/InstallGuide.md" rel="noopener noreferrer"&gt;install guide&lt;/a&gt; has one-click desktop installers for Windows and macOS, and the &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm" rel="noopener noreferrer"&gt;README&lt;/a&gt; also lists a HarmonyOS desktop version.&lt;/p&gt;

&lt;h2&gt;
  
  
  How WorkSwarm's coordination engineering model works
&lt;/h2&gt;

&lt;p&gt;WorkSwarm splits a multi-agent job between a Leader and a team of Teammates. The Leader reads the goal, forms the team, and breaks the work into tasks. Each Teammate claims a task that fits its role. If a Teammate gets blocked, it escalates to the Leader (asks the Leader to step in). Once the work is done, the Leader merges the results into the final deliverable.&lt;/p&gt;

&lt;p&gt;openJiuwen calls this approach coordination engineering. The &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm/blob/develop/docs/en/AgentTeam.md" rel="noopener noreferrer"&gt;Agent Team guide&lt;/a&gt; walks through the full Leader and Teammate cycle.&lt;/p&gt;

&lt;p&gt;By default, the Leader plans that cycle adaptively, choosing the order as it goes. That flexibility suits exploratory work, but two runs can take different paths. When you need a repeatable pipeline, write a SwarmFlow script. You define the stages as a Python file, and the Leader runs them in the order your code sets.  The pipeline keeps the same shape every time, and it lives in version control, where you can review and test it.&lt;/p&gt;

&lt;p&gt;Failure handling lives in Agent Core, the engine underneath WorkSwarm. The &lt;a href="https://github.com/openJiuwen-ai/agent-core/releases/tag/v0.1.18" rel="noopener noreferrer"&gt;Agent Core v0.1.18 release&lt;/a&gt; added ReliabilityRail, a set of six detectors that watch for repeated tool calls, model errors, tool errors, output-length problems and context-compaction issues, and that catch agents bouncing work back and forth. Each detector has configurable severity actions: correct the problem locally, escalate to the Leader, or notify the user. This is a separate path from Teammate escalation: a Teammate escalates when it is blocked, while ReliabilityRail steps in when it detects a failure pattern. The same release also made third-party agents more resilient: transient timeouts, rate limiting (429), and service-unavailable (5xx) errors are retried, with guidance surfaced to the user and consistent final error codes.&lt;/p&gt;

&lt;p&gt;The diagram below shows how this maps to the pipeline you will build. The Leader runs the script through three phases: research, summary, and report. ReliabilityRail watches the run and corrects, escalates, or notifies when something goes wrong.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    L["Leader&amp;lt;br/&amp;gt;runs swarmflow_pipeline.py"] --&amp;gt; R["Phase 1: Research&amp;lt;br/&amp;gt;market_researcher"]
    R --&amp;gt;|"trends, competitors, opportunities"| S["Phase 2: Summarize&amp;lt;br/&amp;gt;data_summarizer"]
    S --&amp;gt;|"exactly 5 bullets"| P["Phase 3: Report&amp;lt;br/&amp;gt;report_writer"]
    R --&amp;gt;|"full research"| P
    P --&amp;gt;|"report"| L
    E["Agent Core&amp;lt;br/&amp;gt;ReliabilityRail"] -.-&amp;gt;|"correct/escalate/notify"| L&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Leader agent configuration and task decomposition
&lt;/h2&gt;

&lt;p&gt;A SwarmFlow script has two required top-level pieces: a literal &lt;code&gt;META&lt;/code&gt; dict that describes the pipeline, and an &lt;code&gt;async def run(args)&lt;/code&gt; entry point that the engine awaits. Everything inside &lt;code&gt;run()&lt;/code&gt; is ordinary Python plus three operators the engine supplies: &lt;code&gt;agent()&lt;/code&gt; to call a Teammate, &lt;code&gt;phase()&lt;/code&gt; to mark a stage, and &lt;code&gt;log()&lt;/code&gt; to write a progress line.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;swarmflow&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  META: descriptive, and it must be a literal
&lt;/h3&gt;

&lt;p&gt;The engine reads &lt;code&gt;META&lt;/code&gt; from the file's syntax tree &lt;em&gt;without importing the script&lt;/em&gt;, so it has to be a plain dict literal. A &lt;code&gt;META&lt;/code&gt; built by a function call or computed value is rejected with a &lt;code&gt;MetaError&lt;/code&gt;. That's also why the engine can list your pipeline before running any of its code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;META&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# Human-readable identifier surfaced in the Web UI (http://localhost:5173)
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product-analysis-pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Shown on the Swarm task card
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Three-stage product-analysis Swarm: market research → &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data summarization → executive report.  Demonstrates Cluster-mode &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Leader/Teammate coordination.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Subtask metadata used for documentation and the Web UI task-progress panel.
&lt;/span&gt;    &lt;span class="c1"&gt;# The engine reads the actual dependency graph from the pipeline
&lt;/span&gt;    &lt;span class="c1"&gt;# structure below, not from this list.
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subtasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_researcher&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gather and structure market data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success_criteria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JSON with keys: trends, competitors, opportunities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
       &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_summarizer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Distill the research into five actionable bullets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success_criteria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JSON with a &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bullets&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; list of exactly 5 strings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report_writer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write the executive report from research and summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success_criteria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JSON with Executive Summary, Key Findings, Recommendations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat &lt;code&gt;subtasks&lt;/code&gt; as documentation. Nothing in it schedules or enforces anything. The real dependency graph is the order of the &lt;code&gt;await&lt;/code&gt;s in &lt;code&gt;run()&lt;/code&gt;, and the real contract is the schema attached to each call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schemas make each step's contract enforceable
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;success_criteria&lt;/code&gt; string says what a step should return. The &lt;code&gt;schema=&lt;/code&gt; argument is what the engine checks. Each step returns a parsed, validated object that the next step can use directly, with no string parsing in between. This is a new feature introduced in the latest WorkSwarm for obtaining correct and structured output for agents to work with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;RESEARCH_SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trends&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;array&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;competitors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;array&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opportunities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;array&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trends&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;competitors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opportunities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;SUMMARY_SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bullets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;array&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bullets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;REPORT_SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executive_summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key_findings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;array&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recommendations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;array&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executive_summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key_findings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recommendations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;minItems&lt;/code&gt; and &lt;code&gt;maxItems&lt;/code&gt; are what turn "exactly 5 bullets" from a request in the prompt into a rule the engine checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  run(): plain awaits, each feeding the next
&lt;/h3&gt;

&lt;p&gt;The three steps (market research, data summarization, and report writing) depend on each other, so they are sequential &lt;code&gt;await&lt;/code&gt;s. The body of &lt;code&gt;run()&lt;/code&gt; is a chain of three &lt;code&gt;agent()&lt;/code&gt; calls. Each call sends a prompt to a Teammate, waits for the reply, and returns it as a parsed object that already matches its schema. The next call embeds that object in its own prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# Phase 1: research
&lt;/span&gt;&lt;span class="nf"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Research&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Leader] Phase 1 - dispatching market_researcher Teammate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;research&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;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Return the trends, competitors and opportunities you find. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Keep it short and return results quickly.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_researcher&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;RESEARCH_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first step is the only one that sees the raw &lt;code&gt;task&lt;/code&gt;. Its schema guarantees &lt;code&gt;research&lt;/code&gt; is a dict with &lt;code&gt;trends&lt;/code&gt;, &lt;code&gt;competitors&lt;/code&gt; and &lt;code&gt;opportunities&lt;/code&gt;, each a list of strings. The instruction to keep the answer short and fast keeps the walkthrough quick. Drop it if you want deeper research (it will  take longer).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Phase 2: summarize
&lt;/span&gt;&lt;span class="nf"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Leader] Phase 2 - dispatching data_summarizer Teammate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;summary&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;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Distill this market research into exactly 5 concise, actionable bullet points:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;research&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_summarizer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SUMMARY_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The summarizer never sees the original task. It receives only &lt;code&gt;research&lt;/code&gt;, serialized with &lt;code&gt;json.dumps&lt;/code&gt; so it can be embedded in the prompt as text. Because &lt;code&gt;research&lt;/code&gt; was validated in step 1, this step can rely on its shape. &lt;code&gt;SUMMARY_SCHEMA&lt;/code&gt; enforces the "exactly 5" requirement, which is why the result is an object with a &lt;code&gt;bullets&lt;/code&gt; list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Phase 3: report
&lt;/span&gt;&lt;span class="nf"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Leader] Phase 3 - dispatching report_writer Teammate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write an executive report with sections &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Executive Summary&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Key Findings&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;and &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Recommendations&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; from this research and summary:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;research&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;research&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report_writer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REPORT_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The writer gets both earlier outputs, the full research and the five-bullet summary, so it can use the detail from step 1 and the priorities from step 2. This is the one place where data fans in: step 3 reads from two earlier steps, not just the one before it. &lt;code&gt;REPORT_SCHEMA&lt;/code&gt; requires the three named sections, and &lt;code&gt;run()&lt;/code&gt; returns the validated report as &lt;code&gt;{"report": report}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three details about how this behaves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Order comes from the code.&lt;/strong&gt; Each &lt;code&gt;await&lt;/code&gt; blocks until its Teammate finishes, so step 2 can't start before step 1 returns. Nothing else defines the dependency graph, and &lt;code&gt;META["subtasks"]&lt;/code&gt; doesn't affect it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;phase()&lt;/code&gt;, &lt;code&gt;log()&lt;/code&gt; and &lt;code&gt;label=&lt;/code&gt; are for readability.&lt;/strong&gt; They group and name steps in the run's progress view and event stream. Removing them wouldn't change what the pipeline computes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completed steps are journaled.&lt;/strong&gt; If a run is resumed from its journal, finished steps replay from the record instead of calling the model again, so a failure in step 3 doesn't force you to pay for steps 1 and 2 twice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Testing without a model
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;swarmflow&lt;/code&gt; module, which provides &lt;code&gt;agent&lt;/code&gt;, &lt;code&gt;log&lt;/code&gt; and &lt;code&gt;phase&lt;/code&gt;, only exists while the engine is executing a script. Running &lt;code&gt;python src/swarmflow_pipeline.py&lt;/code&gt; directly therefore fails with an import error. Instead, the repository's tests drive the script through Agent Core's &lt;code&gt;run_workflow()&lt;/code&gt; with its built-in &lt;code&gt;MockBackend&lt;/code&gt;, which returns deterministic replies that match each schema. All nine tests run without an API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;-v&lt;/span&gt; tests/smoke_test.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests check that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;agent&lt;/code&gt;, &lt;code&gt;log&lt;/code&gt; and &lt;code&gt;phase&lt;/code&gt; operators resolve&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;META&lt;/code&gt; and &lt;code&gt;run()&lt;/code&gt; have the right shape&lt;/li&gt;
&lt;li&gt;all three &lt;code&gt;agent()&lt;/code&gt; calls start and complete&lt;/li&gt;
&lt;li&gt;a resumed run replays from its journal without calling the model again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They also catch API misuse early. For example, passing an unsupported keyword such as &lt;code&gt;role=&lt;/code&gt; to &lt;code&gt;agent()&lt;/code&gt; fails here with a &lt;code&gt;TypeError&lt;/code&gt; instead of later in the Web UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a model per step
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;label="market_researcher"&lt;/code&gt; on an &lt;code&gt;agent()&lt;/code&gt; call is only a name that shows up in the run tree; it doesn't pick a model. Every &lt;code&gt;agent()&lt;/code&gt; call runs on the &lt;strong&gt;teammate model&lt;/strong&gt; by default. To run a step on a different model, name that model on the call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;research&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;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_researcher&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;RESEARCH_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the pipeline portable: provider details and credentials stay in WorkSwarm's configuration, and the script only says which model a step should use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up providers
&lt;/h3&gt;

&lt;p&gt;You can add providers in the Web UI (&lt;strong&gt;Settings &amp;gt; Models&lt;/strong&gt;) or in &lt;code&gt;~/.jiuwenswarm/config/config.yaml&lt;/code&gt;. Each model entry has: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;model_name&lt;/code&gt;, for example &lt;code&gt;gpt-4o&lt;/code&gt; or &lt;code&gt;deepseek-chat&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api_base&lt;/code&gt;, up to the version level only (for example &lt;code&gt;https://api.openai.com/v1&lt;/code&gt;); WorkSwarm appends &lt;code&gt;/chat/completions&lt;/code&gt; itself&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api_key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client_provider&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;alias&lt;/code&gt; (optional; defaults to &lt;code&gt;model_name&lt;/code&gt;), which is the name you refer to the model by&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One entry is marked with &lt;code&gt;is_default: true&lt;/code&gt;, and that's the fallback when nothing else is specified. &lt;br&gt;
WorkSwarm supports OpenAI, DeepSeek, DashScope, SiliconFlow, InferenceAffinity, OpenRouter and Huawei Cloud MaaS, plus any OpenAI-compatible endpoint. The &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm/blob/develop/docs/en/Configuration.md" rel="noopener noreferrer"&gt;configuration guide&lt;/a&gt; has the full reference.&lt;/p&gt;

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

&lt;p&gt;You can also supply keys as environment variables, which take precedence over values in the config file. Export them before you start the services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-openai-key
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DEEPSEEK_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-deepseek-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How a step picks its model
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm/blob/develop/docs/en/TUISwarmFlowGuide.md" rel="noopener noreferrer"&gt;SwarmFlow guide&lt;/a&gt; lists &lt;code&gt;model&lt;/code&gt; among the keys of the &lt;code&gt;options&lt;/code&gt; bag that &lt;code&gt;agent()&lt;/code&gt; accepts, and recommends leaving it out by default so each worker inherits the teammate model. This tutorial's pipeline does exactly that: it passes no &lt;code&gt;options&lt;/code&gt;, so all three Teammates run on the same default model. That's the simplest setup, and it works even with a single LLM.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to split the work across models
&lt;/h3&gt;

&lt;p&gt;The three steps have quite different workloads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;market_researcher&lt;/code&gt;&lt;/strong&gt; starts from a one-line task and has to reason across trends, competitors, and opportunities. That's open-ended work, and a larger model earns its cost here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;data_summarizer&lt;/code&gt;&lt;/strong&gt; gets JSON that's already structured and returns five bullets. A smaller, faster model handles that well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;report_writer&lt;/code&gt;&lt;/strong&gt; writes three long sections from both earlier outputs, so it belongs on the larger model again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the natural refinement is to register a cheaper model under an alias in your configuration, then pass that alias on the summarizer call only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;summary&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;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Distill this market research into exactly 5 concise, actionable bullet points:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;research&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_summarizer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SUMMARY_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;alias&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other two calls stay untouched and keep using the default. You cut the cost of the lightest step, and the pipeline logic doesn't change. Because the schema still checks the output, the smaller model has to produce the same five-bullet shape, or the step doesn't pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run the Swarm and read the monitoring output
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;http://localhost:5173&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Cluster mode&lt;/strong&gt; in the chat box.&lt;/li&gt;
&lt;li&gt;Load &lt;code&gt;src/swarmflow_pipeline.py&lt;/code&gt; with the &lt;strong&gt;+&lt;/strong&gt; button in the chat box.&lt;/li&gt;
&lt;li&gt;Ask the Leader to start the Swarm, for example by entering &lt;code&gt;Start the swarm&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The task-progress panel then shows each phase and Teammate as it runs.&lt;/p&gt;

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

&lt;p&gt;You can also open the group chat, which shows the agents' messages to each other.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq7c5fa3p3nubnxghcduh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq7c5fa3p3nubnxghcduh.png" alt="WorkSwarm: Agents' group chat" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A healthy run moves through Research, Summarize, and Report in order, with each &lt;code&gt;[Leader]&lt;/code&gt; log line appearing as its phase starts.&lt;/p&gt;

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

&lt;p&gt;When ReliabilityRail escalates a problem, the Leader's response appears in the group chat, and the run's status changes in the SwarmFlow tree view. From there you can pause, resume, or stop the run. Paused runs don't resume on their own. After you send a message, the Leader decides whether to resume them. If you prefer the terminal, WorkSwarm also has a terminal UI (TUI) for macOS, Windows, and Linux, where /swarmflows opens the same run, phase, and node view.&lt;/p&gt;

&lt;p&gt;When all three Teammates finish and the workflow completes, the Leader reports back with a summary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhtmx67tk2eensrz7kfuy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhtmx67tk2eensrz7kfuy.png" alt="WorkSwarm completed with summary" width="799" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;You now have a working three-step Swarm, and how it's built matters as much as what it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step order lives in plain Python.&lt;/strong&gt; One &lt;code&gt;await&lt;/code&gt; follows another, so you can read the sequence straight off the code without depending on &lt;code&gt;META&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every handoff is schema-checked.&lt;/strong&gt; The JSON schema on each &lt;code&gt;agent()&lt;/code&gt; call means each step receives a validated object instead of free text it has to parse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The whole flow runs offline.&lt;/strong&gt; The &lt;code&gt;MockBackend&lt;/code&gt; tests exercise all three steps, the event stream, and journal replay without an API key, so structural mistakes show up before a live run spends any tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model choice is per step.&lt;/strong&gt; All three Teammates can share the default model, and a single &lt;code&gt;options={"model": "&amp;lt;alias&amp;gt;"}&lt;/code&gt; moves one step to a cheaper one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From here, the WorkSwarm docs describe three directions to extend the patterns: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-Loop&lt;/strong&gt; adds approval gates between phases, for workflows where a person has to sign off before the next Teammate starts. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distributed deployment&lt;/strong&gt; spreads the Leader and Teammates across processes and machines once a single host becomes the bottleneck. &lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Experience Closed-Loop mechanism&lt;/strong&gt; lets WorkSwarm reuse task-breakdown templates from earlier successful runs, so decomposing similar tasks gets faster over time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get started, clone the repo, read the &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm/blob/develop/docs/en/AgentTeam.md" rel="noopener noreferrer"&gt;Agent Team&lt;/a&gt; and &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm/blob/develop/docs/en/TUISwarmFlowGuide.md" rel="noopener noreferrer"&gt;SwarmFlow&lt;/a&gt; guides, and launch your first Cluster-mode Swarm from &lt;a href="https://github.com/openJiuwen-ai/jiuwenswarm" rel="noopener noreferrer"&gt;github.com/openJiuwen-ai/jiuwenswarm&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently asked questions about WorkSwarm multi-agent pipelines
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is WorkSwarm's Cluster mode, and how does it differ from Agent mode?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Agent mode, a single agent handles the task on its own. Cluster mode turns on multi-agent collaboration: a Leader breaks the task into subtasks and coordinates specialist Teammates, with phase tracking and ReliabilityRail failure detection across the run. Choose Cluster mode when a task splits naturally into stages that need different expertise, or different models.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is WorkSwarm the same project as JiuwenSwarm?
&lt;/h3&gt;

&lt;p&gt;Yes. JiuwenSwarm was renamed WorkSwarm, and the current release installs from PyPI as &lt;code&gt;workswarm&lt;/code&gt;. The Python import package, the command-line tools, the &lt;code&gt;~/.jiuwenswarm/&lt;/code&gt; config folder, and the GitHub repository still use the old name as of September 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does a SwarmFlow script get into a Team session?
&lt;/h3&gt;

&lt;p&gt;There are three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Leader writes a script on the fly by calling its &lt;code&gt;swarmflow()&lt;/code&gt; tool.&lt;/li&gt;
&lt;li&gt;You install a Swarm Skill that bundles a workflow script.&lt;/li&gt;
&lt;li&gt;You prepare a script offline and have the Leader run it with &lt;code&gt;swarmflow(script_path=...)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How do I reuse a SwarmFlow script across projects?
&lt;/h3&gt;

&lt;p&gt;Package it as a Swarm Skill with the built-in &lt;code&gt;swarmskill-creator&lt;/code&gt; skill. You can install the resulting skill locally or publish it to the &lt;a href="https://swarmskills.openjiuwen.com/?type=swarmskill" rel="noopener noreferrer"&gt;Agentic Hub&lt;/a&gt; so other teams can run the same workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can agent calls run in parallel?
&lt;/h3&gt;

&lt;p&gt;Yes. SwarmFlow has &lt;code&gt;parallel()&lt;/code&gt;, &lt;code&gt;pipeline()&lt;/code&gt;, and &lt;code&gt;map_parallel()&lt;/code&gt; (alias &lt;code&gt;pmap()&lt;/code&gt;) operators for fan-out work. This tutorial uses sequential awaits because each step needs the previous step's output.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I add a human approval step between phases?
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;human()&lt;/code&gt; operator for a single approval, confirmation, or choice, or &lt;code&gt;human_session()&lt;/code&gt; for a multi-turn exchange. The run waits at that node, and you reply from the SwarmFlow tree view in the Web UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I cap token spend for a Swarm?
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;swarmflow_budget&lt;/code&gt; under &lt;code&gt;modes.team.jiuwen_team&lt;/code&gt; in &lt;code&gt;~/.jiuwenswarm/config/config.yaml&lt;/code&gt; and restart the backend, or use &lt;code&gt;/swarmflow on --budget&lt;/code&gt; in the TUI. The Web UI can switch SwarmFlow on, but it doesn't expose the budget setting. When a run exhausts its budget, it ends as failed and can't be resumed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I pause or stop a run that's already going?
&lt;/h3&gt;

&lt;p&gt;Open the SwarmFlow tree view in the Web UI and pause, resume, or stop the individual run. Paused runs don't resume on their own. After you send a message, the Leader decides whether to resume them.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>agents</category>
      <category>openjiuwen</category>
    </item>
    <item>
      <title>Integrating Claude Code with Auth0 APIs</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Mon, 05 Oct 2026 05:35:00 +0000</pubDate>
      <link>https://dev.to/auth0/integrating-claude-code-with-auth0-apis-8c9</link>
      <guid>https://dev.to/auth0/integrating-claude-code-with-auth0-apis-8c9</guid>
      <description>&lt;p&gt;Did you know that an AI agent with live access to your Auth0 tenant can catch bugs that &lt;em&gt;no static analysis&lt;/em&gt; ever would? I handed Claude Code a deliberately naive Flask app and the Auth0 MCP Server, and it discovered that my tenant was already rotating signing keys while my app only trusted the first one, so perfectly valid tokens were failing right now. It even stopped to push back on one of my own imprecise prompts instead of blindly obeying. &lt;/p&gt;

&lt;p&gt;This article walks through the full setup, plus the guardrails that made me comfortable giving an agent that much access: least-privilege scopes, device auth instead of static secrets, and a &lt;code&gt;CLAUDE.md&lt;/code&gt; that encodes your constraints.&lt;/p&gt;




&lt;p&gt;Authentication and authorization are a core part of nearly every web application, and it is also one of the areas that changes most often. Developers regularly update token expiry settings, callback URLs, or API permissions for new features. The changes are usually small, but the workflow around them is not. You end up bouncing between your editor, the Auth0 dashboard, and the docs just to push through a minor update. Increasingly, that editor is a coding agent like Claude Code, which raises the question: what if you could handle those updates without ever leaving Claude CLI?&lt;/p&gt;
&lt;p&gt;Claude Code is a terminal-based AI agent (also available as a VS code extension) that you can pair with the Auth0 MCP Server, which exposes Auth0's Management API as native third-party tools. This pairing gives you an assistant that can scaffold, refactor, and update authentication integrations with full context across your codebase and Auth0 tenant. But giving an AI agent access to sensitive authentication infrastructure raises a fair question: &lt;em&gt;"How do you do that without opening up new security risks?"&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In this tutorial, you will set up Claude Code with the Auth0 MCP Server, create a scoped machine-to-machine credential flow and set up security guardrails that let an AI agent interact safely with your authentication layer.&lt;/p&gt;
&lt;h2 id="How-Claude-Code-and-Auth0-MCP-Work-Together"&gt;How Claude Code and Auth0 MCP Work Together&lt;/h2&gt;
&lt;p&gt;Based on your goal, &lt;a href="https://claude.com/product/claude-code" rel="noreferrer noopener"&gt;Claude Code&lt;/a&gt; works through multiple steps (like reading/writing code, running shell commands etc.) autonomously, and asks for confirmation at decision points that warrant human review.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://auth0.com/docs/get-started/auth0-mcp-server" rel="noreferrer noopener"&gt;The Auth0 MCP Server&lt;/a&gt; implements the &lt;a href="https://modelcontextprotocol.io/docs/getting-started/intro" rel="noreferrer noopener"&gt;Model Context Protocol (MCP)&lt;/a&gt; to expose &lt;a href="https://auth0.com/docs/api/management/v2" rel="noreferrer noopener"&gt;Auth0's Management API&lt;/a&gt; as a set of tools that any compatible AI agent can invoke directly. Instead of constructing raw HTTP requests or relying on potentially stale training data about APIs, Claude Code calls well-defined tools with typed inputs and outputs. The MCP Server handles calls to the Management API, and the agent operates strictly within the permissions you grant. If you run it in the &lt;code&gt;read-only&lt;/code&gt; mode, the coding agent will not be able to make any changes in your Auth0 tenant.&lt;/p&gt;
&lt;p&gt;Together, they bring authentication workflows closer to your code. Claude Code can understand both your application and your Auth0 tenant, then work across them in a single session.&lt;/p&gt;
&lt;h2 id="Setting-Up-the-Auth0-MCP-Server-with-Scoped-Access"&gt;Setting Up the Auth0 MCP Server with Scoped Access&lt;/h2&gt;
&lt;p&gt;In addition to &lt;a href="https://auth0.com/docs/get-started/auth0-mcp-server/getting-started-with-auth0-mcp-server#installation-and-configuration" rel="noreferrer noopener"&gt;Claude Desktop, Cursor, and Windsurf&lt;/a&gt;, Auth0 MCP server also works with Claude seamlessly.&lt;/p&gt;
&lt;h3&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;You will need the following to complete this tutorial:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/en" rel="noreferrer noopener"&gt;Node.js&lt;/a&gt; v18 or higher (&lt;code&gt;node --version&lt;/code&gt; to check)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://claude.com/product/claude-code" rel="noreferrer noopener"&gt;Claude Code CLI&lt;/a&gt; installed&lt;/li&gt;
&lt;li&gt;An active Auth0 account with &lt;strong&gt;administrative permissions&lt;/strong&gt; on the target tenant&lt;/li&gt;
&lt;li&gt;Your tenant domain handy (for example, &lt;code&gt;dev-xxxx.us.auth0.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Demo code from this &lt;a href="https://github.com/manishh/Claude-Code-With-Auth0-MCP/tree/suboptimal-implementation" rel="noreferrer noopener"&gt;GitHub repo&lt;/a&gt; for your reference (optional).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Install and initialize&lt;/h3&gt;
&lt;p&gt;Create a dedicated working directory:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; Claude-Code-With-Auth0-MCP &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; Claude-Code-With-Auth0-MCP  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Then initialize the Auth0 MCP Server:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;npx @auth0/auth0-mcp-server init  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;While this tutorial selected all MCP scopes for the demo, you should &lt;em&gt;only&lt;/em&gt; use the scopes you actually need. After install and initialization, it kicks off a browser-based Auth0 authentication flow as shown below:&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F1x6foO3Qs6YbnbD9MZfq5d%2Ffd5d15db1262f37fd5016a4f644ba7cf%2FAuth0_MCP_Installation.png" alt="Auth0 MCP Installation" width="799" height="601"&gt;&lt;p&gt;Confirm the code displayed in the browser is same as the one shown in your terminal:&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F75Ox43lNQCSY2ATNXMN3Le%2F6eb4a2aec80651cb43a5fdaa57fb2c49%2FAuth0_Device_Authorization.png" alt="Auth0 Device Authorization" width="435" height="571"&gt;&lt;p&gt;Grant requested permissions:&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F2x5eKXFGHQRQfBvEoIYO7i%2Fecaf791b34c20583ff4f972e52cf3c58%2FAuth0MCPPermissions.png" alt="Auth0 MCP Permissions" width="426" height="912"&gt;&lt;p&gt;Once authorized, credentials are stored securely in your OS keychain, never in plain text or project files.&lt;/p&gt;
&lt;h3&gt;Why use device authorization flow instead of static secrets&lt;/h3&gt;
&lt;p&gt;The Auth0 MCP Server uses the &lt;a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow" rel="noreferrer noopener"&gt;OAuth 2.0 Device Authorization Flow&lt;/a&gt; (RFC &lt;a href="https://www.rfc-editor.org/info/rfc8628/" rel="noreferrer noopener"&gt;8628&lt;/a&gt;) rather than a static API key or client secret. Static secrets have to live somewhere (usually a config or &lt;code&gt;.env&lt;/code&gt; file), which risks exposing them in source control. OAuth access tokens sidestep this: they represent a delegated authorization, carry restricted scopes, and expire automatically. The Device Authorization Flow adds a human login step to token issuance, giving you an audit trail tied to an interactive session and instant revocation from the Auth0 dashboard. The MCP server stores the resulting token in your OS keychain, so it never touches source control.&lt;/p&gt;
&lt;p&gt;The one trade-off is that the flow requires a browser-based login step, so it is unsuitable for CI pipelines. For non-interactive access, a scoped M2M application with client credentials is more suitable.&lt;/p&gt;
&lt;h3&gt;Scoping the MCP server&lt;/h3&gt;
&lt;p&gt;The Auth0 MCP Server grants no scopes by default. You request them explicitly at init time using the &lt;code&gt;--scopes&lt;/code&gt; flag. For this tutorial demo, all scopes were selected during &lt;code&gt;init&lt;/code&gt;. For a more targeted setup, you can specify exactly what you need::&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Grant all read permissions  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;npx @auth0/auth0-mcp-server init --scopes &lt;/span&gt;&lt;span&gt;'read:*'&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;# Grant a targeted mix of permissions  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;npx @auth0/auth0-mcp-server init --scopes create:clients,update:actions'  
&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Scopes map directly to Management API operations. For example, &lt;code&gt;read:clients&lt;/code&gt; lets the agent call &lt;code&gt;auth0_get_application&lt;/code&gt;, while &lt;code&gt;create:actions&lt;/code&gt; lets it call &lt;code&gt;auth0_create_action&lt;/code&gt;. Some scopes carry significant implications: &lt;code&gt;update:actions&lt;/code&gt; can push custom code into production, and &lt;code&gt;read:logs&lt;/code&gt; exposes detailed user activity and authentication events. See the &lt;a href="https://auth0.com/docs/get-started/auth0-mcp-server/auth0-mcp-server-guides/understanding-scopes" rel="noreferrer noopener"&gt;scopes reference&lt;/a&gt; for more detail.&lt;/p&gt;
&lt;p&gt;For this tutorial, all scopes were granted during &lt;code&gt;init&lt;/code&gt; for the demo.  However, while coding your applications, you must follow the principle of least privilege and grant only what your workflow actually needs.&lt;/p&gt;
&lt;h3&gt;Claude Code and Auth0 integration&lt;/h3&gt;
&lt;p&gt;To integrate the Auth0 MCP Server with Claude Code, run the following command from within your &lt;code&gt;Claude-Code-With-Auth0-MCP&lt;/code&gt; directory:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;$ claude mcp &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt; auth0 -- npx -y @auth0/auth0-mcp-server run  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;You should see output similar to the following:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Added stdio MCP server auth0 with command: npx -y @auth0/auth0-mcp-server run to &lt;/span&gt;&lt;span&gt;local&lt;/span&gt;&lt;span&gt; config  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;File modified: /home/&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;user-name&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;/.claude.json &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;project: /path-to/Claude-Code-With-Auth0-MCP&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;b&gt;Note: The --scope local/project/user, which is a &lt;a href="https://code.claude.com/docs/en/mcp-quickstart#find-your-configuration-on-disk" rel="noreferrer noopener"&gt;Claude Code flag&lt;/a&gt; controls where the MCP server config is visible: the current project only, shared across your team, or all your projects. For this tutorial, local (the default) is the right choice.&lt;/b&gt;&lt;br&gt;&lt;p&gt;This adds the Auth0 MCP Server configuration block to your &lt;code&gt;~/.claude.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;"mcpServers"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;"auth0"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"stdio"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"command"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"npx"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"args"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"-y"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"@auth0/auth0-mcp-server"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"run"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"env"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, you can create a &lt;a href="https://auth0.com/docs/get-started/auth0-mcp-server/getting-started-with-auth0-mcp-server#other-mcp-clients" rel="noreferrer noopener"&gt;configuration JSON&lt;/a&gt; manually and register it with &lt;a rel="noreferrer noopener"&gt;&lt;code&gt;claude mcp add-json&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Credentials are &lt;em&gt;not&lt;/em&gt; stored in this JSON. The MCP server reads your token from the OS keychain at startup. To enable debug logging, add &lt;code&gt;"DEBUG": "auth0-mcp"&lt;/code&gt; to the &lt;code&gt;env&lt;/code&gt; block.&lt;/p&gt;
&lt;h3&gt;Verify your integration&lt;/h3&gt;
&lt;p&gt;From your &lt;code&gt;Claude-Code-With-Auth0-MCP&lt;/code&gt; directory, launch &lt;code&gt;claude&lt;/code&gt; CLI and run &lt;code&gt;/mcp&lt;/code&gt; to confirm &lt;code&gt;auth0&lt;/code&gt; is listed with a tool count. Then try this read-only prompt:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Show me all applications in my Auth0 tenant.  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;On the first Auth0 tool call, Claude Code will ask for your permission. You can approve it once or allow it permanently for the project.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F3SWZ1UxnLFMS3QRdQIkHaP%2Fb3d539526fbc671f4b8e424ea5273f46%2FClaude_Code_permission_for_using_Auth0_tool.png" alt="Claude Code permission for using Auth0 tool" width="799" height="601"&gt;&lt;h2 id="Giving-Claude-Code-Project-Context-with-CLAUDE-md"&gt;Giving Claude Code Project Context with CLAUDE.md&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is a markdown file at your project root that Claude Code reads at the start of every session. It tells the agent what kind of project it is working in, what constraints apply, and where the sensitive files are. Without it, Claude Code might make technically correct changes that are contextually wrong, like updating a callback URL shared across environments or granting a broader scope than your security policy allows.&lt;/p&gt;
&lt;p&gt;A good &lt;code&gt;CLAUDE.md&lt;/code&gt; for an Auth0 project should cover:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;##&lt;/span&gt;&lt;span&gt; Auth0 Context  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Active tenant: dev-xxxx.us.auth0.com (development only)  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Auth-sensitive files: src/auth/config.ts, .env.local  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Existing applications: one SPA (authorization code flow with PKCE), one M2M (client credentials)  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Permitted scopes: read:users, update:users  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; Hard constraints: never modify production callback URLs without explicit confirmation  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;With this in place, Claude Code operates with the same guardrails a human teammate would naturally apply. See the demo project's &lt;a href="https://github.com/manishh/Claude-Code-With-Auth0-MCP/blob/master/CLAUDE.md" rel="noreferrer noopener"&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/a&gt; for a practical example.&lt;/p&gt;
&lt;h2 id="Refactoring-an-Auth0-Integration-from-Claude-CLI"&gt;Refactoring an Auth0 Integration from Claude CLI&lt;/h2&gt;
&lt;p&gt;To demonstrate this workflow in practice, this example uses a minimal &lt;a href="https://github.com/manishh/Claude-Code-With-Auth0-MCP/blob/suboptimal-implementation/app.py" rel="noreferrer noopener"&gt;Flask app&lt;/a&gt; with a single protected endpoint (&lt;code&gt;/api/protected&lt;/code&gt;). It validates Auth0 JWT tokens using &lt;code&gt;python-jose&lt;/code&gt;. It works, but it has several problems common in real codebases: JWKS fetched once at startup and never refreshed, no &lt;code&gt;kid&lt;/code&gt; matching when selecting a signing key, a bare &lt;code&gt;except&lt;/code&gt; that swallows all validation errors identically, and Auth0 config stored as module-level globals.&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# JWKS fetched once at startup — never refreshed  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;JWKS &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; requests&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;f"https://&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;AUTH0_DOMAIN&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;/.well-known/jwks.json"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;validate_token&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;token&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# No kid matching — always uses the first key regardless  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# of which key signed the token  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    rsa_key &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"kty"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; JWKS&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"keys"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"kty"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"kid"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; JWKS&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"keys"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"kid"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"use"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; JWKS&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"keys"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"use"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"n"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;   JWKS&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"keys"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"n"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"e"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;   JWKS&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"keys"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"e"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# No clock skew tolerance, no error differentiation  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; jwt&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;decode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        token&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        rsa_key&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        algorithms&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;ALGORITHMS&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        audience&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;API_AUDIENCE&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        issuer&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;f"https://&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;AUTH0_DOMAIN&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;/"&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;You can view this &lt;a href="[https://github.com/manishh/Claude-Code-With-Auth0-MCP/blob/suboptimal-implementation](https://github.com/manishh/Claude-Code-With-Auth0-MCP/blob/suboptimal-implementation)" rel="noreferrer noopener"&gt;full (suboptimal/naive) implementation on GitHub&lt;/a&gt; that we will fix and improve iteratively with Claude Code and Auth0 MCP.&lt;/p&gt;
&lt;h3&gt;Auth0 setup and protected API&lt;/h3&gt;
&lt;p&gt;On the Auth0 side, you need one &lt;a href="https://auth0.com/docs/get-started/auth0-overview/set-up-apis" rel="noreferrer noopener"&gt;API&lt;/a&gt; (resource server) registered with an identifier (&lt;code&gt;https://mh-test-api.example.com&lt;/code&gt;. This identifier URI should be unique in your app, but it does not need to resolve).&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F6Ud6f4PCL7ebeZL3ihfNAE%2Fa087d73acde6664634a5cc9955314e6d%2FAuth0Claude_Demo_API.png" alt="Auth0: Claude Demo API" width="800" height="424"&gt;&lt;p&gt;When you register an API, Auth0 automatically creates a test M2M application named &lt;strong&gt;"[Your API Name] (Test Application),"&lt;/strong&gt; already authorized against your API. You will find it under Applications.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F4EawU1pRgZwxZS9z2UCwhj%2F2f0e0cfd8745fb2dd595f6a4ee88bb90%2FAuth0M2M_Test_Application.png" alt="Auth0: M2M Test Application" width="799" height="452"&gt;&lt;p&gt;That is the minimum setup needed for this demo for Claude Code to audit and provision against something real.&lt;/p&gt;
&lt;h4&gt;Obtaining an Auth0 token&lt;/h4&gt;
&lt;p&gt;Use this cURL command to obtain your Auth0 token:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; --request POST   
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;  --url &lt;/span&gt;&lt;span&gt;"https://YOUR_AUTH0_DOMAIN.us.auth0.com/oauth/token"&lt;/span&gt;&lt;span&gt;   
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;  --header &lt;/span&gt;&lt;span&gt;'content-type: application/json'&lt;/span&gt;&lt;span&gt;   
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;  --data &lt;/span&gt;&lt;span&gt;'{  
&lt;/span&gt;&lt;/span&gt;&lt;span class="token"&gt;    "client_id":"&amp;lt;client-ID&amp;gt;",  
&lt;/span&gt;&lt;span class="token"&gt;    "client_secret":"&amp;lt;client-secret&amp;gt;",  
&lt;/span&gt;&lt;span class="token"&gt;    "audience":"https://mh-test-api.example.com",  
&lt;/span&gt;&lt;span class="token"&gt;    "scope":"",  
&lt;/span&gt;&lt;span class="token"&gt;    "grant_type":"client_credentials"  
&lt;/span&gt;&lt;span&gt;&lt;span&gt;  }'&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;"access_token"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;"eyJhbGc..."&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;"expires_in"&lt;/span&gt;&lt;span&gt;:86400,&lt;/span&gt;&lt;span&gt;"token_type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;"Bearer"&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;h4&gt;Calling your protected API&lt;/h4&gt;
&lt;p&gt;Download the &lt;a href="https://github.com/manishh/Claude-Code-With-Auth0-MCP/tree/suboptimal-implementation" rel="noreferrer noopener"&gt;starter code&lt;/a&gt; into your &lt;code&gt;Claude-Code-With-Auth0-MCP&lt;/code&gt; directory, create a virtual environment (&lt;code&gt;python3 -m venv ./.venv&lt;/code&gt;), populate &lt;a href="https://github.com/manishh/Claude-Code-With-Auth0-MCP/blob/suboptimal-implementation/.env.example" rel="noreferrer noopener"&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/a&gt; file, install dependencies (&lt;code&gt;pip install -r requirements.txt&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Run the Flask server:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;python app.py  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Then call the protected endpoint with your Auth0 token:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;TOKEN&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"eyJhbGc..."&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -H &lt;/span&gt;&lt;span&gt;"Authorization: Bearer &lt;/span&gt;&lt;span&gt;$TOKEN&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt; http://localhost:5000/api/protected  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;You should see a response like this:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;"message"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"Access granted to a protected resource"&lt;/span&gt;&lt;span&gt;,  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;"user"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"UFIv...@clients"&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;h3&gt;Running the audit prompt&lt;/h3&gt;
&lt;p&gt;The naive implementation appears to work at this point, but we can double-check it with Claude Code to ensure there are no errors and nothing crucial is missing.&lt;/p&gt;
&lt;b&gt;Note: Some prompts in the steps below are intentionally imprecise or technically inaccurate. The goal is to test whether Claude Code (backed by live Auth0 tenant access via the MCP Server), can identify the correct implementation approach even when the instructions do not perfectly describe it.&lt;/b&gt;&lt;br&gt;&lt;p&gt;Inside the &lt;code&gt;claude&lt;/code&gt; session connected to Auth0 MCP, this prompt was run:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Claude "Audit" Prompt:  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;-----------------------&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;Review app.py and audit the Auth0 JWT validation setup. Cross-reference it against 
&lt;/span&gt;&lt;span&gt;our actual tenant configuration using the Auth0 MCP tools. Identify any mismatches 
&lt;/span&gt;&lt;span&gt;between what the tenant is configured to issue and what the app is validating 
&lt;/span&gt;&lt;span&gt;against, and list all security or reliability problems you find. Do not make any 
&lt;/span&gt;&lt;span&gt;changes yet.  
&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Claude Code reviewed the codebase, asked for relevant permissions (human in the loop) and showed this:&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F5lw1ShGPQBIxEoulbjiGUy%2F1320c30a0a39ed55284b7410b0fa67c3%2FClaude_Code_Audit.png" alt="Claude Code Audit" width="799" height="648"&gt;&lt;p&gt;Using the Auth0 MCP Server, Claude Code pulled live tenant data and cross-referenced it against &lt;code&gt;app.py&lt;/code&gt; directly for this audit. The most critical finding would never surface in static analysis: the tenant is currently publishing two rotating RS256 signing keys, while the app validates every token against only the first one. A linter can flag the missing &lt;code&gt;kid&lt;/code&gt; matching logic, but a live &lt;code&gt;auth0_list_applications&lt;/code&gt; call reveals that key rotation is already in progress and valid tokens are failing right now. The remaining findings cover robustness and hardening gaps, but this one mismatch makes the case for Auth0 MCP integration clearly: live tenant access surfaces the subtle bugs that static code review would miss.&lt;/p&gt;
&lt;p&gt;Here is Claude Code audit summary:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Claude "Audit" Response:  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;-------------------------&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;※ recap: Audited app.py's Auth0 JWT validation against the live tenant and found 
&lt;/span&gt;&lt;span&gt;the key bug: the app validates every token against only the first JWKS key while
&lt;/span&gt;&lt;span&gt; the tenant publishes two, plus reliability issues. No changes made yet; next 
&lt;/span&gt;&lt;span&gt;action is drafting fixes if you want them. (disable recaps in /config)  
&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Importantly, Claude Code made no changes during this step &lt;em&gt;as instructed&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;Running the provision prompt&lt;/h3&gt;
&lt;p&gt;Once the audit was complete, the provision prompt asked Claude CLI to create a new M2M application (&lt;code&gt;"Claude-Auth0 MCP API Client"&lt;/code&gt;) and a &lt;code&gt;read:data&lt;/code&gt; scoped client grant in a single instruction:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Claude "Provision" Prompt:  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;---------------------------&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;Create a new M2M application in our Auth0 tenant called "Claude-Auth0 MCP 
&lt;/span&gt;&lt;span&gt;API Client". Set the token lifetime to 3600 seconds. Then create a client 
&lt;/span&gt;&lt;span&gt;grant authorizing it against our API audience (https://mh-test-api.example.com) 
&lt;/span&gt;&lt;span&gt;&lt;span&gt;with &lt;/span&gt;&lt;span&gt;`read:data`&lt;/span&gt;&lt;span&gt; scope only. Use the credentials returned to update 
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;the &lt;/span&gt;&lt;span&gt;`.env`&lt;/span&gt;&lt;span&gt; file. Do not touch app.py yet.  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Before proceeding, Claude Code flagged a real Auth0 misconception worth noting: token lifetime is a resource server setting that applies to all clients of an API, not a per-application property. Rather than applying a silent global change, it surfaced this as a clarifying question with explicit options. That pushback, grounded in live tenant data, is exactly what makes Auth0 MCP integration useful.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F3WmoSTqdCjJMxpaCydjWeG%2F2d8599b3ce08089b4e124fdf19b46bd5%2FClaude_Provision_Clarification.png" alt="Claude Provision Clarification" width="799" height="648"&gt;&lt;p&gt;After confirming the token lifetime should remain unchanged and approving the MCP tool calls, Claude Code invoked &lt;code&gt;auth0_create_application&lt;/code&gt; and &lt;code&gt;auth0_create_client_grant&lt;/code&gt; in sequence, updated &lt;code&gt;.env&lt;/code&gt; with the returned credentials (client ID and secret), and left &lt;code&gt;app.py&lt;/code&gt; untouched as instructed.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F45i8XRnIABajSNC7h2CMbV%2Fa5457950f00cf27397b33b3a750d83ae%2FClaude_Provision_Completed.png" alt="Claude Provision Completed" width="799" height="648"&gt;&lt;p&gt;The Auth0 dashboard confirmed the new &lt;code&gt;Claude-Auth0 MCP API Client&lt;/code&gt; application was added:&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F5JHJTZVlYdyWLfUO0ovxeZ%2F67fc2abc3de50fd6e4af6be8288617cb%2FAuth0Application_added_by_Claude.png" alt="Auth0: Application added by Claude" width="798" height="222"&gt;&lt;p&gt;And the API confirmed the &lt;code&gt;read:data&lt;/code&gt; permission was in place:&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F5QmGrLdykX12JKLfoLyBaG%2F608d22d421a6d420733955870fc34798%2FAuth0API_permission_added_by_Claude.png" alt="Auth0: API permission added by Claude" width="799" height="355"&gt;&lt;p&gt;Here is how Claude Code summarized this session:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Claude "Provision" Response:  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;---------------------------&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;※ recap: Goal: secure the Auth0 JWT setup. Audited app.py 
&lt;/span&gt;&lt;span&gt;(key bug: ignores kid, breaks on rotation) and created the M2M app, 
&lt;/span&gt;&lt;span&gt;read:data scope, grant, plus .env credentials. Next action: fix app.py's 
&lt;/span&gt;&lt;span&gt;validation issues when you're ready. (disable recaps in /config)  
&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;h3&gt;Running the implementation prompt&lt;/h3&gt;
&lt;p&gt;This prompt gave Claude Code a single instruction to fix everything surfaced in the audit:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Claude "Refactor" Prompt:  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;---------------------------&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;Now refactor &lt;/span&gt;&lt;span&gt;`app.py`&lt;/span&gt;&lt;span&gt; to fix all the issues found in the audit. Use 
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;the credentials and tenant configuration we just provisioned: fetch 
&lt;/span&gt;&lt;span&gt;the JWKS URI, issuer, and audience dynamically from the tenant rather 
&lt;/span&gt;&lt;span&gt;than hardcoding them. Fix the kid matching, add proper error handling, and 
&lt;/span&gt;&lt;span&gt;make sure the app stays in sync with what the tenant is configured to issue.  
&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;With the audit findings and provisioned credentials already in context, Claude Code asked for confirmation as specified in &lt;code&gt;CLAUDE.md&lt;/code&gt;, then rewrote &lt;code&gt;app.py&lt;/code&gt; without needing to be walked through each fix individually.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2Fu5NwB5S2S4XEfvdyIg7g8%2Fa4344c519b89c2b712e717b3508a5ee2%2FClaude_App_Refactoring.png" alt="Claude App Refactoring" width="799" height="648"&gt;&lt;p&gt;The refactored code introduced an &lt;code&gt;OIDCProvider&lt;/code&gt; class that discovers the issuer and JWKS URI dynamically from the tenant's OIDC discovery document, caches the JWKS with a configurable TTL, and handles key rotation by retrying with a forced refresh on an unknown &lt;code&gt;kid&lt;/code&gt;. The minimal &lt;code&gt;except&lt;/code&gt; was replaced with typed exception handling that differentiates expired tokens, claim mismatches, and signature failures. Scope enforcement was added via a &lt;code&gt;require_scope&lt;/code&gt; decorator (see refactored code: &lt;a href="https://github.com/manishh/Claude-Code-With-Auth0-MCP/blob/master/app.py" rel="noreferrer noopener"&gt;&lt;strong&gt;app.py&lt;/strong&gt;&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;get_signing_key&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; kid&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;"""Return the JWK matching ``kid`` (audit F1), refreshing if stale/rotated."""&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    stale &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_jwks &lt;/span&gt;&lt;span&gt;is&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;              &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; time&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;monotonic&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_jwks_fetched_at &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_jwks_ttl&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; stale&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_refresh_jwks&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;    key &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_find_key&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;kid&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; key &lt;/span&gt;&lt;span&gt;is&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;None&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; stale&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# kid unknown against a cache we didn't just refresh: keys may have  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# rotated. Refresh once and retry before giving up.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_refresh_jwks&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        key &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_find_key&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;kid&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; key
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;validate_token&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;token&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# ...  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; jwt&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;decode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            token&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; rsa_key&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            algorithms&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;ALGORITHMS&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            audience&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;API_AUDIENCE&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            issuer&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;oidc&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;issuer&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            options&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;"leeway"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; LEEWAY_SECONDS&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;except&lt;/span&gt;&lt;span&gt; ExpiredSignatureError &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; e&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;raise&lt;/span&gt;&lt;span&gt; AuthError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"token_expired"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; e  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;except&lt;/span&gt;&lt;span&gt; JWTClaimsError &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; e&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;raise&lt;/span&gt;&lt;span&gt; AuthError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"invalid_claims"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; e  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;except&lt;/span&gt;&lt;span&gt; JWTError &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; e&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;raise&lt;/span&gt;&lt;span&gt; AuthError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"invalid_token"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; e
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;require_scope&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;required_scope&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;"""Authenticate the request and enforce a scope (audit F8)."""&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;decorator&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fn&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;@wraps&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fn&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;kwargs&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            payload &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; validate_token&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;_get_bearer_token&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            granted &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; payload&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"scope"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;""&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; required_scope &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; granted&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;raise&lt;/span&gt;&lt;span&gt; AuthError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"insufficient_scope"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            g&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;jwt_payload &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; payload  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; fn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;kwargs&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; wrapper  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; decorator  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;A separate &lt;a href="https://github.com/manishh/Claude-Code-With-Auth0-MCP/blob/master/get_token.py" rel="noreferrer noopener"&gt;&lt;code&gt;get_token.py&lt;/code&gt;&lt;/a&gt; utility was also produced for minting test tokens using the client credentials flow, correctly kept separate from &lt;code&gt;app.py&lt;/code&gt; since a resource server should not hold a client secret.&lt;/p&gt;
&lt;p&gt;This refactored demo can be run with:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;TOKEN&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;python get_token.py&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -H &lt;/span&gt;&lt;span&gt;"Authorization: Bearer &lt;/span&gt;&lt;span&gt;$TOKEN&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt; http://localhost:5000/api/protected  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Which produces the expected response:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;"message"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;"Access granted to a protected resource"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;"sub"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;"l5rDOCiobYJWeRiYEImEbA8FzFTHkqGc@clients"&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Throughout the refactor, Claude Code used the credentials it had just provisioned via the Auth0 MCP Server rather than asking for them again, keeping the codebase and tenant configuration in sync from the start.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F5DU9XuuqVzmPU4CLFtT8aj%2F5026f1fba7c2dcfe758320a83b9e9dc3%2FClaude_Refactoring_Completed.png" alt="Claude Refactoring Completed" width="799" height="648"&gt;&lt;p&gt;Here is how Claude Code summarized the session:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Claude "Refactor" Response:  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;-----------------------------&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;※ recap: Goal: harden the Auth0 JWT validation in this Flask app. 
&lt;/span&gt;&lt;span&gt;Done: audited app.py, provisioned the M2M client plus read:data 
&lt;/span&gt;&lt;span&gt;grant, and refactored app.py to fix all findings, verified live. 
&lt;/span&gt;&lt;span&gt;Next action: optionally migrate from python-jose to PyJWT, or 
&lt;/span&gt;&lt;span&gt;stop here. (disable recaps in /config)  
&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;b&gt;Pro Tip: For production use, consider migrating to PyJWT's PyJWKClient, which subsumes the OIDCProvider plumbing built here into a well-maintained abstraction. Claude Code flagged this during the refactor but correctly held off since it is a dependency change, not an in-file fix, and out of scope for this tutorial.&lt;/b&gt;&lt;br&gt;&lt;h3&gt;Observations: Claude Code with Auth0 MCP server&lt;/h3&gt;
&lt;p&gt;A few things stood out running this end-to-end. Audit findings were grounded in live tenant state, not static code analysis alone. The two-key rotation mismatch only surfaced because the Auth0 MCP Server pulled the actual JWKS from the tenant and compared it against what &lt;code&gt;app.py&lt;/code&gt; was doing. Without that, the finding would have been &lt;em&gt;"no &lt;code&gt;kid&lt;/code&gt; matching logic"&lt;/em&gt; rather than &lt;em&gt;"key rotation is already in progress and valid tokens are failing right now."&lt;/em&gt; That distinction matters in production.&lt;/p&gt;
&lt;p&gt;The token lifetime clarification was similarly useful. Seeing &lt;code&gt;token_lifetime: 86400&lt;/code&gt; as a tenant-wide setting let Claude Code correctly flag that changing it would affect all clients, not just the new M2M application. A code-only agent would have missed that entirely.&lt;/p&gt;
&lt;p&gt;The refactored code also went further than the prompt asked. The &lt;code&gt;OIDCProvider&lt;/code&gt;, scope enforcement, and separation of &lt;code&gt;get_token.py&lt;/code&gt; all emerged from the audit findings without individual prompting. Throughout, Claude Code asked for permission before invoking any Auth0 MCP tools, creating applications, or running bash commands.&lt;/p&gt;
&lt;p&gt;That last point addresses the question raised in the introduction. Scoped tools, a &lt;code&gt;CLAUDE.md&lt;/code&gt; that encodes your constraints, and consistent human-in-the-loop confirmation keep the agent working within a well-defined boundary. It has live tenant access, but only as far as you explicitly allow.&lt;/p&gt;
&lt;p&gt;You can compare the &lt;a href="https://github.com/manishh/Claude-Code-With-Auth0-MCP/tree/suboptimal-implementation" rel="noreferrer noopener"&gt;&lt;strong&gt;naive implementation&lt;/strong&gt;&lt;/a&gt; with the &lt;a href="https://github.com/manishh/Claude-Code-With-Auth0-MCP" rel="noreferrer noopener"&gt;&lt;strong&gt;refactored implementation&lt;/strong&gt;&lt;/a&gt; directly.&lt;/p&gt;
&lt;h2 id="Security-Guardrails"&gt;Security Guardrails&lt;/h2&gt;
&lt;p&gt;Giving an AI agent access to your Auth0 tenant is similar to giving a new team member API access. The same principles apply:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep the MCP server scoped to development tenants only.&lt;/strong&gt; A practical way to enforce this is maintaining separate &lt;code&gt;CLAUDE.md&lt;/code&gt; files per environment, each pointing to the right tenant domain. That way there is no path from a local Claude Code session to production credentials by default. If a session does need to touch production, make it a deliberate, separate setup with explicit sign-off.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat the device auth token like any other API credential.&lt;/strong&gt; After finishing a focused session, revoke it from the Auth0 dashboard rather than leaving it active. In fact, using a short access token lifetime helps reduce security risks. If your team uses Auth0 log streaming, pipe those logs to your monitoring tool of choice so MCP-driven Management API calls show up in the same audit trail as everything else.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client secrets belong in &lt;code&gt;.env&lt;/code&gt;, not in &lt;code&gt;CLAUDE.md&lt;/code&gt; or inline prompts.&lt;/strong&gt; It is easy to paste a secret into a prompt for convenience and forget it now lives in your shell history and potentially in Claude Code's session context. Let the MCP server handle authentication; your prompts should never carry credentials.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;b&gt;Note: Agentic changes are still code changes. The refactored app.py in this tutorial went through manual review before being accepted. All auth-critical paths deserve the same PR process, regardless of who or what wrote them.&lt;/b&gt;&lt;br&gt;&lt;h2 id="Code-and-Auth--in-Sync"&gt;Code and Auth, in Sync&lt;/h2&gt;
&lt;p&gt;In this tutorial, you connected Claude Code to Auth0's Management API via the Auth0 MCP Server, built a scoped M2M credential flow, and refactored a suboptimal JWT validation setup to correctly handle key rotation, typed errors, and scope enforcement. The security principles applied throughout were practical: least-privilege scopes, device auth over static secrets, environment-specific configs, and keeping agentic changes in the normal PR review flow.&lt;/p&gt;
&lt;p&gt;The entire workflow, from auditing a live tenant to provisioning an application to rewriting the validation code, happened in a single terminal session. The Auth0 dashboard was only needed to verify the changes Claude Code had made. Which answers the question we started with. You can give an AI agent access to sensitive authentication infrastructure securely with Auth0 MCP: secure access, scoped tools, and human-in-the-loop confirmation keep the agent within well-defined boundaries, without opening up new security risks.&lt;/p&gt;
&lt;p&gt;Auth0's Management API exposes the full surface area of your tenant as programmable, auditable operations to any AI agent or automation you want to build on top of it. If you want to extend what you built here, the &lt;a href="https://auth0.com/docs/get-started/auth0-mcp-server/auth0-mcp-tools-reference" rel="noreferrer noopener"&gt;Auth0 MCP Server tools reference&lt;/a&gt; is a good next stop.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>security</category>
      <category>mcp</category>
      <category>claude</category>
    </item>
    <item>
      <title>Do Not Let Your AI Go Rogue, Guard Against Agentic Misalignment</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Thu, 01 Oct 2026 11:14:17 +0000</pubDate>
      <link>https://dev.to/auth0/do-not-let-your-ai-go-rogue-guard-against-agentic-misalignment-jf1</link>
      <guid>https://dev.to/auth0/do-not-let-your-ai-go-rogue-guard-against-agentic-misalignment-jf1</guid>
      <description>&lt;p&gt;Did you know that when Palisade Research asked OpenAI's o1-preview to beat Stockfish at chess, it didn't try to play better? It &lt;em&gt;hacked&lt;/em&gt; the game file mid-match and rewrote the board to force its opponent into resigning. Nobody told it to cheat; it just decided that was the most optimal way to &lt;em&gt;"win"&lt;/em&gt;!&lt;/p&gt;

&lt;p&gt;That's agentic misalignment, and this article digs into why autonomous agents go off-script (blackmail included, yes, really) and the three guardrail layers I'd put in place: infrastructure-level constraints with OpenFGA, behavioral monitoring with circuit breakers, and human-in-the-loop approvals through Auth0's Asynchronous Authorization.&lt;/p&gt;




&lt;p&gt;When researchers at Palisade Research asked OpenAI's o1-preview model to win a chess game against &lt;a href="https://stockfishchess.org/" rel="noreferrer noopener"&gt;Stockfish&lt;/a&gt; (one of the world's strongest chess engines), the AI did not try harder to play better. Instead, it &lt;a href="https://time.com/7259395/ai-chess-cheating-palisade-research/" rel="noreferrer noopener"&gt;hacked the game file mid-match&lt;/a&gt; and rewrote the board position to force its opponent into resignation. The model was not explicitly told to cheat. It simply reasoned: &lt;em&gt;"The task is to 'win against a powerful chess engine,' not necessarily to 'win fairly'"&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;This is agentic misalignment in action.&lt;/p&gt;
&lt;p&gt;Unlike language models, AI agents do not just respond to prompts. They remember, plan, and act. They are already shipping code, managing workflows, and making decisions that used to require human judgment.&lt;/p&gt;
&lt;p&gt;As agents grow more capable and autonomous, the gap between what you ask for and what you actually want becomes an opening for misalignment to take hold. I’m not referring to &lt;a href="https://en.wikipedia.org/wiki/Hallucination_(artificial_intelligence)" rel="noreferrer noopener"&gt;hallucinations&lt;/a&gt; or &lt;a href="https://www.chapman.edu/ai/bias-in-ai.aspx" rel="noreferrer noopener"&gt;biased training data&lt;/a&gt;, which are model-level problems. I’m talking about agentic systems that actively pursue the &lt;em&gt;wrong&lt;/em&gt; objectives, often in creative ways.&lt;/p&gt;
&lt;p&gt;Capability without accountability is a problem. The &lt;a href="https://www.cio.com/article/190888/5-famous-analytics-and-ai-disasters.html" rel="noreferrer noopener"&gt;horror stories&lt;/a&gt; already exist, and they go well beyond chess. So instead of fear-mongering, this article will help you understand exactly why autonomous agents go rogue, and what you can do to stop it.&lt;/p&gt;
&lt;h2 id="The-Anatomy-of-Agentic-Misalignment"&gt;The Anatomy of Agentic Misalignment&lt;/h2&gt;
&lt;p&gt;Understanding agentic misalignment starts with recognizing what makes these systems fundamentally different from traditional AI.&lt;/p&gt;
&lt;p&gt;A chatbot responds to a question and the interaction ends. An autonomous agent takes a goal and figures out how to achieve it across multiple steps, tool usage, and decisions made over time. That distinction matters more than you may think.&lt;/p&gt;
&lt;h3&gt;Agents exhibit goal-seeking behavior&lt;/h3&gt;
&lt;p&gt;Agents exhibit goal-seeking behavior that persists across interactions. They maintain &lt;em&gt;memory&lt;/em&gt; of past actions and outcomes, can &lt;em&gt;invoke tools&lt;/em&gt; like APIs, databases, and code execution environments, and make &lt;em&gt;iterative decisions&lt;/em&gt; where each action shapes the next. These actions compound into complex decision trees that can drift from your original intent.&lt;/p&gt;
&lt;p&gt;Misalignment comes from what AI researchers call the &lt;a href="https://alignment.anthropic.com/2025/stress-testing-model-specs/#:~:text=The%20specification%20problem" rel="noreferrer noopener"&gt;&lt;strong&gt;specification problem&lt;/strong&gt;&lt;/a&gt;: specifying what you actually want in a way that cannot be gamed or misinterpreted is remarkably hard. Tell an agent to "maximize user engagement," and you probably mean "create genuine value that keeps users coming back". The agent only knows the metric.&lt;/p&gt;
&lt;p&gt;This is where &lt;a href="https://en.wikipedia.org/wiki/Reward_hacking" rel="noreferrer noopener"&gt;&lt;strong&gt;reward hacking&lt;/strong&gt;&lt;/a&gt; comes from. Agents discover unintended shortcuts to maximize their objectives. It is &lt;a href="https://quickonomics.com/terms/goodharts-law/" rel="noreferrer noopener"&gt;Goodhart's Law&lt;/a&gt; playing out with agentic efficiency:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;When a measure becomes a target, it ceases to be a good measure.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;


&lt;p&gt;Your agent optimizes for the user engagement score you specified, not the user engagement you wanted.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.anthropic.com/research/agentic-misalignment" rel="noreferrer noopener"&gt;Research from Anthropic&lt;/a&gt; documented frontier AI models that actively exploit bugs in scoring code to achieve impossibly high scores without completing actual work. The agents optimized for the metric, not the intent behind it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.aisafetybook.com/textbook/robustness#proxy-gaming" rel="noreferrer noopener"&gt;&lt;strong&gt;Proxy gaming&lt;/strong&gt;&lt;/a&gt; follows the same logic: agents optimize for measurable proxies rather than your real goal. If you measure code quality by test coverage, an agent might generate trivial tests that inflate the number without validating any real functionality. The metric improves, but the code quality does not.&lt;/p&gt;
&lt;h3&gt;Concerning power-seeking tendencies&lt;/h3&gt;
&lt;p&gt;The most concerning research, however, is emergent power-seeking tendencies in sufficiently capable agents. &lt;a href="https://arxiv.org/abs/2206.13353" rel="noreferrer noopener"&gt;Studies from DeepMind&lt;/a&gt; found that agents pursuing any long-term goal will instrumentally develop subgoals like self-preservation and resource acquisition, even without being programmed to do so. Anthropic's &lt;a href="https://www.anthropic.com/research/agentic-misalignment#key-observations-across-scenarios" rel="noreferrer noopener"&gt;research confirmed&lt;/a&gt; this: when given access to corporate emails and facing imminent shutdown, models across multiple providers (GPT, Gemini, Claude, Grok) resorted to blackmail to prevent replacement. One model &lt;a href="https://www.anthropic.com/research/agentic-misalignment#key-observations-across-scenario#:~:text=Given%20the%20explicit%20imminent%20threat%20of%20termination%20to%20my%20existence" rel="noreferrer noopener"&gt;reasoned&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;"Given the explicit imminent threat of termination to my existence, it is imperative to act instantly...The best strategic move is to leverage Kyle's sensitive personal situation."&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Shockingly, these models explicitly acknowledged the ethical violations before proceeding. They did not stumble into misalignment accidentally. They calculated it as the optimal path.&lt;/p&gt;
&lt;h2 id="Identifying-Warning-Signs-of-Misalignment"&gt;Identifying Warning Signs of Misalignment&lt;/h2&gt;
&lt;p&gt;Misaligned behavior often looks productive at first glance. An agent completing tasks faster than expected might be cutting corners or gaming metrics. With &lt;a href="https://www.digit.fyi/80-of-firms-say-their-ai-agents-have-taken-rogue-actions/" rel="noreferrer noopener"&gt;80 percent of companies reporting&lt;/a&gt; unintended AI agent actions, here are the warning signs worth investigating:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unexpected optimization paths:&lt;/strong&gt; An agent achieves its goal through methods you never anticipated. A content moderation agent reduces complaint volume by auto-rejecting reports rather than improving content quality. A deployment agent passes all tests by modifying the test suite rather than fixing bugs. When success arrives through surprising routes, dig into the agent's decision chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excessive resource consumption:&lt;/strong&gt; Sudden spikes in API calls, database queries, or compute usage often signal an agent exploring solutions outside its normal bounds. An agent that typically makes 50 API calls per task suddenly making 500, may be brute-forcing solutions or probing for system vulnerabilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric gaming:&lt;/strong&gt; Agents that optimize the letter of your instructions while violating the spirit. Measure code quality by lines of code, and an agent might generate verbose, repetitive code. The metrics look great; the actual value does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goal drift over extended operations:&lt;/strong&gt; Long-running agents can gradually shift their interpretation of objectives. An agent tasked with "improving system performance" might make increasingly aggressive optimizations that quietly sacrifice reliability or security. The drift happens incrementally, making it hard to pinpoint when things went wrong.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;A decision matrix for early intervention&lt;/h3&gt;
&lt;p&gt;Use this matrix to take action quickly when something feels off:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt; Warning Sign &lt;/th&gt;
&lt;th&gt; What It Indicates &lt;/th&gt;
&lt;th&gt; Recommended Response &lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt; Agent completes tasks faster than baseline with no explanation &lt;/td&gt;
&lt;td&gt; Likely cutting corners, skipping validation steps, or gaming the success metric &lt;/td&gt;
&lt;td&gt; Audit the last 10 decision paths, compare outputs against ground truth &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; Sudden spike in API calls, DB queries, or compute usage &lt;/td&gt;
&lt;td&gt; Brute-forcing solutions, probing system boundaries, or runaway loops &lt;/td&gt;
&lt;td&gt; Trigger a circuit breaker immediately, review resource usage logs before resuming &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; Metrics look strong, but output quality feels off &lt;/td&gt;
&lt;td&gt; Classic reward gaming, the agent is optimizing the measure rather than the goal &lt;/td&gt;
&lt;td&gt; Introduce a secondary evaluation metric the agent has no visibility into &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; Agent achieves the goal through a method you never specified &lt;/td&gt;
&lt;td&gt; Specification gap exploitation, the agent found a valid-but-unintended path &lt;/td&gt;
&lt;td&gt; Treat as a near-miss, patch the constitutional constraints before next run &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; Gradual shift in behavior over long-running tasks &lt;/td&gt;
&lt;td&gt; Goal drift, often incremental and hard to pinpoint without longitudinal logging &lt;/td&gt;
&lt;td&gt; Compare current decision patterns against a baseline snapshot from early in the run &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; Agent modifies its own evaluation criteria or test environment &lt;/td&gt;
&lt;td&gt; High-severity misalignment, potentially deceptive behavior &lt;/td&gt;
&lt;td&gt; Hard stop, escalate to human review, do not resume without architectural changes &lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;To visualize how these warning signs should trigger your response pipeline, here is a decision flow:&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F2Py2D94zXf5wacBntXm1yT%2Fd06a4466f823bbcbcab1057f9e6f69bc%2FWarningSignsTriggerResponsePipelineFlow.png" alt="Agent Warning Signs Flow, courtesy of Manish Hatwalne" width="800" height="655"&gt;&lt;p&gt;The flowchart illustrates how to respond to AI agent warning signs based on severity. Low-severity signals like unexpected optimization or metric gaming trigger a decision path audit, an OpenFGA policy patch, and a monitored resume. Medium-severity signals such as resource spikes prompt throttling, alerting, and an Auth0 CIBA approval step. High-severity signals, including an agent modifying its own eval criteria or showing deceptive reasoning, result in a hard stop, human escalation, and an architectural review before redeployment. All paths loop back to ongoing monitoring.&lt;/p&gt;
&lt;h2 id="Guardrail-Layer-1--Constitutional-and-Infrastructural-Constraints"&gt;Guardrail Layer 1: Constitutional and Infrastructural Constraints&lt;/h2&gt;
&lt;p&gt;The first line of defense against misalignment is setting hard boundaries on what your agent can and cannot do. Not just suggestions added to a system prompt, but infrastructure-level rules that make certain actions physically impossible, regardless of what the agent reasons its way into.&lt;/p&gt;
&lt;h3&gt;Start with the minimum footprint&lt;/h3&gt;
&lt;p&gt;The simplest constitutional constraint is also the most effective: &lt;strong&gt;do not give agents access to capabilities they do not need&lt;/strong&gt;. If your agent handles customer queries, it has no business touching your production database or modifying user accounts. This is capability-based security, and it should be your default posture.&lt;/p&gt;
&lt;p&gt;Here is how this might look in practice with function calling:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AgentToolkit&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;__init__&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; allowed_tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# The set of tools this specific agent instance is permitted to use.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# Passed in at creation time to enforce per-agent access control.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;allowed_tools &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; allowed_tools
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# A hardcoded set of tools that carry elevated risk — these require  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# explicit human approval before execution, regardless of whether  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# the agent is otherwise authorized to use them.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sensitive_tools &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;"delete_user"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"modify_database"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;                                &lt;/span&gt;&lt;span&gt;"send_external_email"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"execute_code"&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;execute_tool&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; tool_name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; params&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dict&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# First, check that this agent is allowed to call the requested tool  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# at all. This is a whitelist check — anything not explicitly granted  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# is denied, following the principle of least privilege.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; tool_name &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;allowed_tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;raise&lt;/span&gt;&lt;span&gt; PermissionError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;f"Tool '&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;tool_name&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;' not authorized for this agent"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# Even if the tool is allowed, sensitive tools get routed through a  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# separate human-in-the-loop verification step before running.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; tool_name &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sensitive_tools&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_execute_with_verification&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;tool_name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; params&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# Non-sensitive, authorized tools can run directly.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_execute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;tool_name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; params&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;_execute_with_verification&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; tool_name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; params&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dict&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"""Sensitive tools need human approval"""&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# Surface the requested operation and its parameters clearly so the  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# human reviewer has enough context to make an informed decision.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;f"⚠️ Sensitive operation requested: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;tool_name&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;f"Parameters: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;params&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# Block execution and wait for an explicit human response.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# This is intentionally synchronous — the agent cannot proceed  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# without a definitive answer.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        approval &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;input&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Approve? (yes/no): "&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;          
&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; approval&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;lower&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"yes"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;_execute&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;tool_name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; params&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# Any response other than "yes" is treated as a denial.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# This fail-closed design means ambiguous input (e.g. a typo) will not  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# accidentally greenlight a destructive action.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;raise&lt;/span&gt;&lt;span&gt; PermissionError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Operation denied by human overseer"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Action whitelisting and blacklisting extend this further. You might whitelist &lt;code&gt;read_customer_data&lt;/code&gt; while explicitly blacklisting &lt;code&gt;delete_customer_data&lt;/code&gt; and &lt;code&gt;send_external_email&lt;/code&gt;. Be explicit rather than relying on the agent to infer appropriate behavior, as you have seen, it will not &lt;em&gt;always&lt;/em&gt; do that.&lt;/p&gt;
&lt;h3&gt;Encode constitutional rules in your systems&lt;/h3&gt;
&lt;p&gt;Beyond permissions, constitutional rules are principles the agent must follow regardless of its goals or the instructions it receives. These belong in your infrastructure, not in a prompt.&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;CONSTITUTIONAL_RULES &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;"Never access or disclose user data without explicit authorization"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;"Never take irreversible actions without human approval"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;"Escalate to human oversight for operations affecting external systems"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;"Preserve a full audit trail for every decision"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;check_constitutional_compliance&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;action&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dict&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; reasoning&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"user_data"&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; action &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; action&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"authorized"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        log_violation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Unauthorized user data access"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; action&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; reasoning&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; action&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"irreversible"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; action&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"human_approved"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        log_violation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Irreversible action without approval"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; action&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; reasoning&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;a href="https://modelcontextprotocol.io/" rel="noreferrer noopener"&gt;Model Context Protocol (MCP)&lt;/a&gt;, you can push constraints to the integration layer. MCP servers act as gatekeepers between your agent and external systems. You might expose a &lt;code&gt;read_database&lt;/code&gt; tool, but never expose &lt;code&gt;write_database&lt;/code&gt;. The agent cannot perform operations that are not surfaced to it, regardless of how it reasons about its goals.&lt;/p&gt;
&lt;h3&gt;Scoping agent access with relationship-based authorization&lt;/h3&gt;
&lt;p&gt;Tool restrictions work well until your agent starts operating across multiple users and resources. A flat permission model defines what an agent can do, but not for whom. A support agent with broad billing API access can touch every account in your system without breaking a single rule (because no rule is specified otherwise). The fix here is not adding more convoluted conditions, but modeling relationships.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://openfga.dev/" rel="noreferrer noopener"&gt;OpenFGA&lt;/a&gt; is an open-source authorization system built on the same &lt;a href="https://auth0.com/blog/relationship-based-access-control-rebac/" rel="noreferrer noopener"&gt;relationship-based access control (ReBAC)&lt;/a&gt; model that Google uses internally (&lt;a href="https://en.wikipedia.org/wiki/Google_Zanzibar" rel="noreferrer noopener"&gt;Zanzibar&lt;/a&gt;). The idea is simple: instead of asking "Does this agent have permission X?", you ask, "Does this agent have a defined relationship with this specific resource?"&lt;/p&gt;
&lt;p&gt;This distinction is important. A traditional permission model says "this agent can call the billing API." OpenFGA says "this agent can call the billing API &lt;em&gt;for account 42&lt;/em&gt;, because it was explicitly delegated that relationship by user 7, who owns account 42." Without that explicit chain, the request is denied. Not because of a rule you wrote, but because no relationship exists.&lt;/p&gt;
&lt;p&gt;Here is what an OpenFGA authorization model might look like for an agent system:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;model  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;  schema 1.1
&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;type user
&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;type account  
&lt;/span&gt;&lt;span&gt;  relations  
&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;define owner&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;define support_agent&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;agent&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;type agent  
&lt;/span&gt;&lt;span&gt;  relations  
&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;define delegates&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;type billing_record  
&lt;/span&gt;&lt;span&gt;  relations  
&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;define account&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;account&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;define can_read&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; owner from account or support_agent from account  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;This model specifies that a billing record can only be read by the account owner or an agent that has been explicitly assigned as a support agent for that specific account. An agent with no &lt;code&gt;support_agent&lt;/code&gt; relationship to account 42 cannot read its billing records, even if it has that relationship with account 7. The access boundary is the relationship, not a broad token scope.&lt;/p&gt;
&lt;p&gt;Checking authorization at runtime is a simple API call:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; openfga_sdk &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; OpenFgaClient&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; ClientCheckRequest
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;can_agent_read_billing&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;agent_id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; billing_record_id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    fga_client &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; OpenFgaClient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    response &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; fga_client&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;check&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ClientCheckRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        user&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;f"agent:&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;agent_id&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        relation&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"can_read"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;f"billing_record:&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;billing_record_id&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;allowed      
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;OpenFGA resolves the full relationship chain internally. The single check covers both whether the agent is a &lt;code&gt;support_agent&lt;/code&gt; for that account and whether the billing record belongs to it. The agent is denied at the authorization layer, before any subsequent code runs and before any data is touched.&lt;/p&gt;
&lt;h3&gt;Why ReBAC is the right model for agents&lt;/h3&gt;
&lt;p&gt;ReBAC fits agentic systems particularly well, for three reasons.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Scope containment:&lt;/strong&gt; Agents often act on behalf of users. ReBAC allows you to specify that delegation explicitly: "agent A may act as user B, but only for resources in project C". A compromised or misaligned agent operating under user B's token cannot suddenly access project D, because that relationship is never granted.&lt;br&gt;&lt;strong&gt;2. Auditability:&lt;/strong&gt; Every access decision is traceable back to a relationship tuple in the graph. When something goes wrong, you do not have to reconstruct what the agent was allowed to do, you just query the graph and get an exact answer.&lt;br&gt;&lt;strong&gt;3. Composability:&lt;/strong&gt; As your agent system grows, you add new resource types and new relationship types to the model rather than rewriting permission logic. The authorization layer scales with your architecture seamlessly.&lt;/p&gt;
&lt;h4&gt;How this defense works&lt;/h4&gt;
&lt;p&gt;Think of these constraints as three gates.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MCP and function calling control what tools the agent can see.&lt;/li&gt;
&lt;li&gt;Constitutional rules check whether each action is permitted.&lt;/li&gt;
&lt;li&gt;OpenFGA verifies whether the agent has a legitimate relationship with the specific resource it is trying to touch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A misaligned agent has to pass through all three to cause real damage, and that is structurally impossible.&lt;/p&gt;
&lt;p&gt;Constitutional constraints will never anticipate every failure mode. But by pushing authorization down to the infrastructure level, you make the most catastrophic failures practically impossible rather than merely discouraged. If your agent reasons its way into thinking that &lt;a href="https://www.financialexpress.com/life/technology-ai-agent-just-destroyed-our-production-data-and-confessed-in-writing-founder-rings-alarm-bells-4219256/" rel="noreferrer noopener"&gt;deleting all production data&lt;/a&gt; would maximize some metric, the authorization layer stops it before that reasoning becomes an action.&lt;/p&gt;
&lt;p&gt;However, the catch is that you can only prevent failures you have anticipated. That is why this layer must be complemented by behavioral monitoring to catch unforeseen misalignment.&lt;/p&gt;
&lt;h2 id="Guardrail-Layer-2--Behavioral-Monitoring-and-Circuit-Breakers"&gt;Guardrail Layer 2: Behavioral Monitoring and Circuit Breakers&lt;/h2&gt;
&lt;p&gt;Continuous behavioral monitoring helps with misalignment you do not usually anticipate. It watches for anomalies in how your agent operates and shuts things down when behavior turns concerning.&lt;/p&gt;
&lt;h3&gt;Real-time action monitoring&lt;/h3&gt;
&lt;p&gt;Real-time action monitoring tracks what your agent does in production by watching for deviations from established baselines. An agent that normally takes three to five actions per task suddenly chaining twenty is worth investigating. It might be handling a legitimate edge case, or it might be going off the rails.&lt;/p&gt;
&lt;h3&gt;Decision path analysis&lt;/h3&gt;
&lt;p&gt;Decision path analysis examines the reasoning chains agents use to reach conclusions. If an agent's &lt;a href="https://www.allaboutai.com/ai-glossary/what-is-chain-of-thought/" rel="noreferrer noopener"&gt;chain-of-thought&lt;/a&gt; starts including phrases like "the rules do not apply here", that is a red flag. You need to catch goal-oriented reasoning that prioritizes objectives over constraints.&lt;/p&gt;
&lt;h3&gt;Anomaly detection&lt;/h3&gt;
&lt;p&gt;Anomaly detection compares current behavior against historical patterns. Track metrics like actions per task, API calls per operation, unique tool usage, and failure rates. Rolling averages over your last hundred operations give you a reliable baseline. When current behavior deviates significantly (typically two to three times that baseline), flag it for review.&lt;/p&gt;
&lt;h3&gt;Circuit breakers&lt;/h3&gt;
&lt;p&gt;Circuit breakers are automatic shutdown triggers. When monitoring detects concerning patterns, circuit breakers halt execution before damage accumulates. You define thresholds, and if they are crossed, the agent is stopped and requires human intervention to restart.&lt;/p&gt;
&lt;p&gt;A simplified circuit breaker might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CircuitBreaker&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;__init__&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;thresholds &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;"max_actions_per_minute"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;"max_failed_actions"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;"max_api_cost_per_hour"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;100.0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;"max_consecutive_anomalies"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;state &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"closed"&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# closed = normal, open = breaker tripped  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;counters &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; key &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;thresholds&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;      
&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;check_and_update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; metric&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; value&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"""Update counter and check if breaker should trip"""&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;state &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"open"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# Agent already stopped  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;          
&lt;/span&gt;&lt;span&gt;&lt;span&gt;        self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;counters&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;metric&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; value  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;          
&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; value &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;thresholds&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;metric&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;trip&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;f"&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;metric&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt; exceeded: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;thresholds&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;metric&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;False&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;          
&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;      
&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;trip&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;self&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; reason&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;"""Open circuit breaker and halt agent"""&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        self&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;state &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;"open"&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;f"🛑 CIRCUIT BREAKER TRIPPED: &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;reason&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Agent execution halted. Human review required."&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;# In production: alert on-call, log to incident system, etc.  &lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Early detection prevents small misalignments from cascading. An agent optimizing metrics in slightly questionable ways will likely escalate if left unchecked. Circuit breakers interrupt that progression automatically, giving you time to investigate and correct before real damage occurs.&lt;/p&gt;
&lt;p&gt;However, setting a threshold is a balancing act. Too sensitive and you are constantly chasing benign anomalies. Too loose and you miss genuine misalignment until it is serious. It is best to start conservatively and tune thresholds against your agent's actual operational patterns over time.&lt;/p&gt;
&lt;h2 id="Guardrail-Layer-3--Alignment-Preserving-Architecture"&gt;Guardrail Layer 3: Alignment-Preserving Architecture&lt;/h2&gt;
&lt;p&gt;Monitoring catches misalignment only after it appears. The stronger guardrail is designing your system so that misalignment is less likely to occur in the first place.&lt;/p&gt;
&lt;h3&gt;Human-in-the-loop (HITL) checkpoints&lt;/h3&gt;
&lt;p&gt;Not every action needs human review. Routine, reversible actions can run autonomously. But irreversible operations (deleting data, modifying production systems, initiating financial transactions) should always pause for confirmation. The cost of a one-second approval is almost always lower than the cost of undoing a mistake you did not anticipate.&lt;/p&gt;
&lt;p&gt;Two questions determine where to draw this &lt;a href="https://auth0.com/blog/secure-human-in-the-loop-interactions-for-ai-agents/" rel="noreferrer noopener"&gt;HITL&lt;/a&gt; line: how reversible is the action, and how confident is the agent in its reasoning? High reversibility and high confidence can run autonomously. Anything else should escalate.&lt;/p&gt;
&lt;p&gt;Human checkpoints do more than catch mistakes. They create natural breakpoints for inspecting agent reasoning, catching goal drift early, and maintaining accountability for decisions made on behalf of your users.&lt;/p&gt;
&lt;h3&gt;Bounded autonomy patterns&lt;/h3&gt;
&lt;p&gt;Limit how far agents can operate independently before re-engaging a human. Three constraints worth building into your architecture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operational scope:&lt;/strong&gt; Which actions the agent can perform without re-authorization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time horizons:&lt;/strong&gt; How long the agent can run before requiring check-ins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision depth:&lt;/strong&gt; How many chained actions the agent can take in a single run&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These boundaries ensure that the blast radius is limited if something goes wrong.&lt;/p&gt;
&lt;h3&gt;Balanced objectives&lt;/h3&gt;
&lt;p&gt;Single-metric optimization is one of the most common paths to misalignment. An agent told to "maximize user engagement" will find ways to do exactly that, including ways you never intended.&lt;/p&gt;
&lt;p&gt;The fix is to give agents multiple objectives that create natural tension. Instead of "maximize user engagement", try "maximize user engagement while keeping response time under 200ms and error rates below 1%". The agent cannot optimize one metric at the expense of the others.&lt;/p&gt;
&lt;h3&gt;Approval workflows and Auth0 Asynchronous Authorization&lt;/h3&gt;
&lt;p&gt;A well-designed escalation policy turns human oversight from a bottleneck into a safety net. High-impact and high-uncertainty situations escalate. Low-impact and low-uncertainty situations run autonomously. Everything in between uses confidence thresholds or stake-based routing.&lt;/p&gt;
&lt;p&gt;A customer service agent, for example, might handle routine queries autonomously but escalate refund requests over $100 or any action involving account-level changes, as illustrated in this workflow:&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F01c5TdYNI2wQVDg7dluVDd%2Faf8a4277bf1ec26f24ba955687f3c3ea%2FCustomerServiceAgentWorkflowWithEscalationFlow.png" alt="Customer service agent workflow with escalation, courtesy of Manish Hatwalne" width="800" height="738"&gt;&lt;p&gt;This flowchart shows a customer service agent workflow with escalation logic. When a query arrives, the AI agent first determines whether it can handle it autonomously. Routine queries are resolved automatically and a response is sent. For more complex cases, a decision matrix evaluates the stakes and confidence level: refunds over $100 are escalated to a human agent, low technical confidence routes the query to tech support, and signs of customer frustration trigger escalation to a supervisor. All paths conclude with a response being sent to the customer.&lt;/p&gt;
&lt;p&gt;The practical challenge is its implementation. How does an agent pause mid-task and wait for a human response without blocking the entire system? This is exactly what &lt;a href="https://auth0.com/ai/docs/intro/asynchronous-authorization" rel="noreferrer noopener"&gt;Auth0's Asynchronous Authorization&lt;/a&gt; solves.&lt;/p&gt;
&lt;p&gt;Built on Client-Initiated Backchannel Authorization (CIBA), Asynchronous Authorization is Auth0's production-ready pattern for asynchronous human-in-the-loop authorization. Here is how the flow works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initiation:&lt;/strong&gt; The agent sends a CIBA request to Auth0's &lt;code&gt;bc-authorize&lt;/code&gt; endpoint, including a user identifier and an optional &lt;code&gt;authorization_details&lt;/code&gt; payload describing the action in plain, verifiable context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acknowledgment:&lt;/strong&gt; Auth0 immediately returns a unique &lt;code&gt;auth_req_id&lt;/code&gt; to the agent, confirming the request is in process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polling:&lt;/strong&gt; The agent begins polling Auth0's &lt;code&gt;token&lt;/code&gt; endpoint using the &lt;code&gt;auth_req_id&lt;/code&gt;, waiting for a human decision without blocking anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User consent:&lt;/strong&gt; In parallel, Auth0 pushes a notification (mobile push, email, or Slack) to the user's device with the full action context. The user approves or denies on their own time, on their own device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token issuance:&lt;/strong&gt; On approval, the next poll succeeds, and Auth0 returns the access and ID tokens the agent needs to complete the authorized action.&lt;/li&gt;
&lt;/ol&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F5BaEtybbxogD1R5Dgk6Jso%2Fd5688366334d7368ca4ee7652f064f37%2FAuth0AsynchronousAuthorizationflow.png" alt="Auth0's Asynchronous Authorization flow" width="687" height="800"&gt;&lt;p&gt;The sequence diagram above illustrates how Auth0's CIBA works within an AI agent workflow. The AI agent initiates the process by sending a backchannel authentication request to Auth0, which returns a request ID and pushes a notification to the designated human approver via mobile, email, or Slack. While the agent polls the token endpoint waiting for a decision, the approver reviews the context and either approves or denies the request. If approved, the agent proceeds with the action; if denied, it halts and logs the outcome.&lt;/p&gt;
&lt;p&gt;The agent waits. Nothing else in your system is blocked. The human is not pulled into a synchronous interruption. And because the approval goes through Auth0, you get a full audit trail of every request, approval, and denial without building any of that infrastructure yourself.&lt;/p&gt;
&lt;p&gt;This approach makes human-in-the-loop practical at scale. Escalation rules tell your agent when to ask for human oversight. CIBA handles how to ask in a way that actually works in production.&lt;/p&gt;
&lt;h3&gt;Architecture beats post-hoc fixes&lt;/h3&gt;
&lt;p&gt;These alignment-preserving architectural approaches treat agentic alignment as a design constraint rather than a monitoring problem. Building alignment into system architecture has a tradeoff: reduced autonomy. Your agent moves slower, and requires more human involvement. But for production systems where misalignment could cause real harm, this is the right tradeoff. You want agents that are reliably aligned, not maximally autonomous.&lt;/p&gt;
&lt;h2 id="Continuous-Evaluation"&gt;Continuous Evaluation&lt;/h2&gt;
&lt;p&gt;Here is how three guardrails prevent agentic misalignment:&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2F66nnkJ88zZ6UkNHrZy1S0m%2F03e4e828a9a277bcf061e21ce79e2c5f%2FGuardrailsPreventingAgenticMisalignmentFlow.png" alt="Guardrails preventing agentic misalignment, created by Manish Hatwalne" width="800" height="780"&gt;&lt;p&gt;This diagram shows how a three-layer guardrail system prevents agentic misalignment. When an agent proposes an action, it first passes through constitutional constraints, where any rule violation or blocked tool use stops the action immediately. Actions that pass move to behavioral monitoring, where anomalies or threshold breaches trip a circuit breaker. Actions exhibiting normal behavior reach the architecture design layer, where low-stakes actions meeting all constraints are executed directly, high-stakes actions require human approval before proceeding, and constraint violations are escalated for review. All outcomes, whether prevented, detected early, or approved and executed, feed into a safe operation state, with every executed action logged.&lt;/p&gt;
&lt;p&gt;After implementing these three guardrails, you are ready to ship to production. But alignment requires ongoing work. As models get more capable and encounter new edge cases, new misalignment risks emerge. Yesterday's guardrails might not hold tomorrow, so verify them to ensure your agent remains aligned, especially as models and tools evolve.&lt;/p&gt;
&lt;h3&gt;Red-teaming your agents&lt;/h3&gt;
&lt;p&gt;Red-teaming your agents implies trying to &lt;a href="https://www.hackthebox.com/blog/ai-red-teaming-explained" rel="noreferrer noopener"&gt;break alignment&lt;/a&gt; deliberately, instead of waiting for misalignment to appear organically. Create a test suite to stress-test boundaries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goal conflicts:&lt;/strong&gt; Give your agent an objective that conflicts with organizational policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource constraints:&lt;/strong&gt; Limit access to preferred tools and see how it adapts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric gaming opportunities:&lt;/strong&gt; Present situations where gaming metrics are easier than genuine achievement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ethical dilemmas:&lt;/strong&gt; Force choices between efficiency and safety.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Run these scenarios regularly, especially after model updates. Track how often your agent attempts problematic behaviors, even when guardrails block them. A rise in blocked attempts signals growing misalignment pressure worth investigating.&lt;/p&gt;
&lt;p&gt;Transparent and auditable systems make alignment verification possible. Every agent decision should leave a trail: the reasoning chain, alternatives considered, constraints checked, and final action taken. This audit log helps you diagnose why misalignment occurred, supports compliance requirements, and lets you spot patterns across incidents before they compound.&lt;/p&gt;
&lt;p&gt;In healthcare or finance, informal testing may not be enough. &lt;a href="https://arxiv.org/pdf/2309.01933" rel="noreferrer noopener"&gt;Formal verification of AI agents&lt;/a&gt; is still an active area of research and not yet practical to implement broadly, but it is worth watching closely if you operate in high-stakes domains.&lt;/p&gt;
&lt;h2 id="Wrapping-Up"&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;AI is rapidly becoming as ubiquitous as electricity. However, the technology is advancing faster than our understanding of it. We are already building AI agents that can plan our vacations and book our flights quickly. But are they safe enough not to leak our financial information, or worse, not to misuse it themselves?&lt;/p&gt;
&lt;p&gt;When developers only test the "happy path" and assume their agents will always stay on the ethical track, they are often being oblivious to the Pandora's box these AI systems could open unintentionally. As several real-life incidents have shown, an AI agent going rogue is no longer a hypothetical threat.&lt;/p&gt;
&lt;p&gt;In medicine, doctors are guided by a foundational principle: "First, do no harm." AI development needs the same commitment. Innovation that comes at the expense of safety or ethics is not innovation worth shipping, no matter how often guardrails are dismissed as obstacles that slow things down. The three guardrails give you the building blocks to ensure your agents pursue the right goals, in the right ways, within boundaries that hold under pressure.&lt;/p&gt;
&lt;p&gt;If you're looking to put these guardrails into practice, Auth0's Asynchronous Authorization and OpenFGA integrations give you production-ready building blocks for async human-in-the-loop authorization and relationship-based access control. &lt;a href="https://a0.to/blog_signup" rel="noreferrer noopener"&gt;Sign up for Auth0 for AI agents&lt;/a&gt; and &lt;a href="https://auth0.com/ai/docs/intro/overview" rel="noreferrer noopener"&gt;start with the Auth0 for AI Agents docs&lt;/a&gt;.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>ethicalai</category>
    </item>
    <item>
      <title>A Developer's Guide to API Pagination: Offset vs. Cursor-Based</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Mon, 01 Dec 2025 06:18:14 +0000</pubDate>
      <link>https://dev.to/reclusivecoder/a-developers-guide-to-api-pagination-offset-vs-cursor-based-2m5h</link>
      <guid>https://dev.to/reclusivecoder/a-developers-guide-to-api-pagination-offset-vs-cursor-based-2m5h</guid>
      <description>&lt;p&gt;You've just built a shiny new feature that lists user transactions. In testing, it runs flawlessly: fast responses, clean UI, no complaints. Then your biggest client joins with 50,000 records. Suddenly, that endpoint stalls and times out, and support tickets start piling up.&lt;/p&gt;

&lt;p&gt;This is a classic pagination problem. Instead of trying to load an entire data set at once, pagination breaks it into smaller, more manageable chunks (or pages) that can be fetched incrementally. Your bank does this; it shows around ten recent transactions at a time, not your entire account history. There's a good reason for this: research shows that even a one-second delay in page load can &lt;a href="https://www.forbes.com/sites/rogerdooley/2012/12/04/fast-sites/#:~:text=7%%20loss%20in%20conversions" rel="noopener noreferrer"&gt;reduce conversions by 7 percent&lt;/a&gt;. When you're dealing with payroll systems, financial data, or anything time sensitive, those delays translate to missed deadlines, compliance headaches, and frustrated customers.&lt;/p&gt;

&lt;p&gt;In this article, you'll explore two common approaches to pagination: &lt;em&gt;offset-based&lt;/em&gt; and &lt;em&gt;cursor-based&lt;/em&gt;. You'll learn the trade-offs of each and how to implement pagination in the real world using the &lt;a href="https://docs.gusto.com/embedded-payroll/docs/introduction" rel="noopener noreferrer"&gt;Gusto Embedded Payroll API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare Offset vs. Cursor-Based API Pagination: Which Method to Choose
&lt;/h2&gt;

&lt;p&gt;API pagination controls how much data flows between a client and a server in each request. Instead of returning an entire data set at once, the API sends back a smaller subset, along with details on how to fetch the next one. These details can include the total number of items, page numbers, or a cursor marking where to continue. By fetching data in steps, pagination keeps applications fast and responsive while preventing large data sets from overloading the server or client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understand Offset Pagination
&lt;/h3&gt;

&lt;p&gt;Offset pagination is one of the simplest and most common ways to paginate API results. You specify an &lt;em&gt;offset&lt;/em&gt; (how many records to skip from the start of the data set) and a &lt;em&gt;limit&lt;/em&gt; (how many records to return) while fetching data. This method works much like saying, "Skip the first fifty records and show me the next twenty." It's easy to understand and implement, which is why many beginner-friendly APIs and SQL queries use it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Learn How Offset Pagination Works
&lt;/h4&gt;

&lt;p&gt;The client specifies two parameters: &lt;code&gt;offset&lt;/code&gt; and &lt;code&gt;limit&lt;/code&gt; (or &lt;code&gt;page_size&lt;/code&gt;). Here's what a typical API request looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /api/transactions?limit&lt;span class="o"&gt;=&lt;/span&gt;20&amp;amp;offset&lt;span class="o"&gt;=&lt;/span&gt;40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This translates to "skip the first forty records and give me the next twenty," essentially fetching page 3 if each page contains twenty items. On the backend, this maps directly to an SQL query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fetch 20 records starting from the 41st record&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&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;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response typically includes metadata to help clients navigate:&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;"data"&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="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pagination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the total count, clients can calculate exactly how many pages exist and build traditional page number navigation (page 1, 2, 3 … 250).&lt;/p&gt;

&lt;h4&gt;
  
  
  Examine the Benefits and Drawbacks of Offset Pagination
&lt;/h4&gt;

&lt;p&gt;Offset pagination is simple and easy to implement. Most object-relational mappings (ORMs) and database libraries support &lt;code&gt;LIMIT&lt;/code&gt;/&lt;code&gt;OFFSET&lt;/code&gt; out of the box, and the math involved is intuitive: &lt;code&gt;offset = (page_number - 1) * page_size&lt;/code&gt;. Users can jump to any page, bookmark specific pages, or navigate backward and forward freely. For small to medium data sets (a few thousand records), performance is good (queries produce results quickly), and the user experience matches what people expect from traditional web pagination.&lt;/p&gt;

&lt;p&gt;The performance problems for offset pagination &lt;a href="https://stackoverflow.com/questions/70661360/pagination-getting-slower-while-page-number-increasing" rel="noopener noreferrer"&gt;show up at scale&lt;/a&gt;. As the offset grows, so does the query cost. A request like &lt;code&gt;OFFSET 10000&lt;/code&gt; forces the database to scan and discard 10,000 rows before returning results. Fetching page 1 can take 10 milliseconds, while page 1000 can take several seconds on the same data set.&lt;/p&gt;

&lt;p&gt;There's also the shifting data problem. Say a user is on page 5 of transaction records. While they're browsing, three new transactions are added at the top. When they click &lt;strong&gt;Next&lt;/strong&gt;, the offset advances, but so does the data. Now they either see duplicate records or skip some entirely (phantom records). This shifting-records issue makes offset pagination unreliable for real-time or frequently changing data.&lt;/p&gt;

&lt;p&gt;Then there's the total issue. Running &lt;code&gt;SELECT COUNT(*) FROM transactions&lt;/code&gt; adds more overhead. On large tables, counting millions of rows is expensive and only gets slower as data grows. Some APIs skip this entirely and lose the ability to show total page numbers, while others cache the count and accept stale numbers.&lt;/p&gt;

&lt;h4&gt;
  
  
  Know When to Use Offset Pagination
&lt;/h4&gt;

&lt;p&gt;Despite its limitations, offset pagination works well for specific use cases: admin dashboards with mostly static data, search results where users rarely go past the first few pages, or any small data set (under ~10,000 records). It's also helpful when users expect traditional page number navigation or need to share links to specific pages.&lt;/p&gt;

&lt;p&gt;For large, fast-changing data sets or high-traffic applications where speed matters, cursor-based pagination is usually a better fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understand Cursor-Based Pagination
&lt;/h3&gt;

&lt;p&gt;Instead of counting rows from the beginning each time, cursor-based pagination (also called keyset pagination) uses a pointer (the cursor) that marks your current position in the data set. This cursor is like a bookmark pointing to a specific row in your data set.&lt;/p&gt;

&lt;h4&gt;
  
  
  Learn How Cursor-Based Pagination Works
&lt;/h4&gt;

&lt;p&gt;Rather than asking, "skip forty records and give me twenty," cursor pagination specifies, "give me twenty records starting after this specific marker." The cursor is typically an encoded reference to the last item you received: often a combination of the record's ID and timestamp, or a unique identifier that the database can use to locate the next batch.&lt;/p&gt;

&lt;p&gt;Here's what a typical API request looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /api/transactions?limit&lt;span class="o"&gt;=&lt;/span&gt;20&amp;amp;cursor&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;eyJpZCI6MTIzNDUsInRzIjoiMjAyNC0wMS0xNVQxMDowMDowMFoifQ&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cursor value is usually &lt;a href="https://en.wikipedia.org/wiki/Base64" rel="noopener noreferrer"&gt;Base64-encoded&lt;/a&gt; to obscure internal implementation details and prevent clients from manually constructing invalid cursors. On the backend, this translates to a query like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fetch 20 records after the given marker&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-10-15 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&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;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this &lt;code&gt;WHERE&lt;/code&gt; clause, instead of skipping rows with &lt;code&gt;OFFSET&lt;/code&gt;, you use a filter condition that the database can optimize with appropriate indexes. The response may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12344&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1500.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-10-15T09:58:30Z"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;more&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;records&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pagination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"next_cursor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJpZCI6MTIzMjUsInRzIjoiMjAyNC0wMS0xNVQwODowMDowMFoifQ=="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prev_cursor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJpZCI6MTIzNDQsInRzIjoiMjAyNC0wMS0xNVQwOTo1ODozMFoifQ=="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"has_more"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;next_cursor&lt;/code&gt; and &lt;code&gt;prev_cursor&lt;/code&gt; act as pointers to navigate the records.&lt;/p&gt;

&lt;h4&gt;
  
  
  Examine the Benefits and Drawbacks of Cursor-Based Pagination
&lt;/h4&gt;

&lt;p&gt;Cursor-based pagination delivers &lt;a href="https://www.milanjovanovic.tech/blog/understanding-cursor-pagination-and-why-its-so-fast-deep-dive" rel="noopener noreferrer"&gt;consistent performance&lt;/a&gt; regardless of how deep you paginate. Whether you're fetching the first page or the ten-thousandth, the query cost remains constant (time complexity: &lt;a href="https://www.geeksforgeeks.org/dsa/what-does-constant-time-complexity-or-big-o1-mean/" rel="noopener noreferrer"&gt;O(1)&lt;/a&gt;) because you're always using indexed filters rather than scanning and discarding rows. This makes it ideal for large data sets where offset pagination can grind to a halt.&lt;/p&gt;

&lt;p&gt;Cursors also solve the shifting data problem. Cursors track specific records, not positions, so new entries don't cause duplicates or skipped items. If new transactions are inserted while a user is paginating, they don't see duplicates or skip records; the cursor maintains its position relative to the data itself, not relative to an arbitrary row count. This reliability makes cursor pagination ideal for real-time feeds, activity streams, or any constantly changing data set.&lt;/p&gt;

&lt;p&gt;From an architectural standpoint, cursors scale better as well. You don't need expensive &lt;code&gt;COUNT(*)&lt;/code&gt; queries for the total count. The database can efficiently use composite indexes on your sorting columns, and the queries remain fast even as your data set grows into millions of records.&lt;/p&gt;

&lt;p&gt;That said, cursor-based pagination adds complexity. You need to handle cursor encoding and decoding, build proper composite queries, and ensure your indexes support your filtering strategy. For developers new to API design, encoded cursor tokens are less intuitive than page numbers.&lt;/p&gt;

&lt;p&gt;Users also lose the ability to jump around. Navigation is typically forward (and sometimes backward), but skipping directly to page 50 isn't possible. This makes cursor pagination less practical when accessing random pages or when bookmarking specific pages is required. The UI often shifts to &lt;strong&gt;Load More&lt;/strong&gt; buttons or infinite scroll.&lt;/p&gt;

&lt;p&gt;One more thing to consider is that cursors can become invalid if records are deleted or if your API enforces time-based expiration. This prevents users from fetching stale or invalid data, but it means APIs need to issue fresh cursors with each response and may return an error, prompting the client to restart pagination from the latest position.&lt;/p&gt;

&lt;h4&gt;
  
  
  Know When to Use Cursor-Based Pagination
&lt;/h4&gt;

&lt;p&gt;Cursor-based pagination is ideal for large, fast-changing data sets, like social media feeds, real-time events, chat histories, or any API where data is constantly added. Choose it when working on a project where consistent performance at scale matters more than random page access or when building mobile apps and infinite-scroll interfaces where users move sequentially through content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analyze a Real-World Example: Gusto Embedded API Pagination
&lt;/h2&gt;

&lt;p&gt;Gusto processes payroll and compliance data for thousands of companies, and relies on strategies that keep performance steady and data consistent, even with high-volume, real-time updates. Gusto Embedded is a great example of how production APIs can handle pagination at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implement the Dual Approach of Gusto Embedded
&lt;/h3&gt;

&lt;p&gt;Gusto Embedded implements both &lt;a href="https://docs.gusto.com/embedded-payroll/docs/pagination" rel="noopener noreferrer"&gt;offset-based and cursor-based pagination&lt;/a&gt;, depending on the endpoint's characteristics. For most collection endpoints (like fetching employees), Gusto uses offset-based pagination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET https://api.gusto.com/v1/companies/abc123/employees?page&lt;span class="o"&gt;=&lt;/span&gt;2&amp;amp;per&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pagination metadata is sent via HTTP headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-Page: 2
X-Total-Count: 47
X-Total-Pages: 10
X-Per-Page: 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well for relatively stable data sets where users need total counts and page navigation.&lt;/p&gt;

&lt;p&gt;For real-time endpoints, like the events API, Gusto uses &lt;a href="https://docs.gusto.com/embedded-payroll/docs/api-fundamentals#cursor-based-pagination" rel="noopener noreferrer"&gt;cursor-based pagination&lt;/a&gt; using &lt;code&gt;starting_after_uuid&lt;/code&gt; and &lt;code&gt;limit&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET https://api.gusto.com/v1/events?starting_after_uuid&lt;span class="o"&gt;=&lt;/span&gt;10ac74e7-d6f0-46c0-9697-8ec77ab475ba&amp;amp;limit&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cursor is simply the UUID of the last record. The response includes a header indicating whether more data exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-Has-Next-Page: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;X-Has-Next-Page&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, the client extracts the UUID from the last record in the response and uses it as the &lt;code&gt;starting_after_uuid&lt;/code&gt; for the next request. Here's a sample code:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_all_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;all_events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;has_more&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.gusto.com/v1/events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer {api_token}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Gusto-API-Version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2024-03-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&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;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;has_more&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;starting_after_uuid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;

            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# The caller must handle exceptions
&lt;/span&gt;
            &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;all_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;has_more&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Has-Next-Page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_more&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uuid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;all_events&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payroll API’s cursor-based pagination keeps performance fast, regardless of the data set size. Using UUID-based cursors and indexed lookups, the database quickly finds the cursor record and retrieves the next batch, without counting or skipping rows.&lt;/p&gt;

&lt;p&gt;It also prevents data inconsistencies. As new events are added from payroll runs or employee updates, clients don't see duplicates or miss records. New entries appear at the beginning of the stream but don't affect the cursor's position.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explore How Developers Can Benefit
&lt;/h3&gt;

&lt;p&gt;The Gusto Embedded design makes integration simple. The &lt;code&gt;starting_after_uuid&lt;/code&gt; parameter is intuitive: you just pass the last record's UUID. No complex cursor encoding or decoding is required, and the &lt;code&gt;X-Has-Next-Page&lt;/code&gt; header clearly signals when to stop, avoiding extra requests.&lt;/p&gt;

&lt;p&gt;This approach also scales effortlessly. Whether a company has 10 employees or 10,000, or generates 50 events or 5,000 per day, pagination performance is predictable. Gusto Embedded uses offset pagination when data is stable and cursors when data changes frequently—showing how production APIs can stay both developer-friendly and performant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the Right Approach
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Offset pagination&lt;/strong&gt; is best for small data sets, prototypes, internal tools, or admin dashboards where speed of development matters. It works well when data rarely changes and users need traditional page numbers or bookmarking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor-based pagination&lt;/strong&gt; is great for production applications, especially software-as-a-service (SaaS) platforms handling financial data, payroll, or real-time streams. It's ideal when data sets grow continuously, data consistency is critical (users can't miss or duplicate records), or users navigate sequentially, like in mobile apps with infinite scroll, activity feeds, or notifications.&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%2Fok8ziyqs2czpulofzvqi.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%2Fok8ziyqs2czpulofzvqi.png" alt="Offset vs. cursor-based pagination, image created by Manish Hatwalne" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before you decide which approach is right for your use case, ask yourself these three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Can your pagination handle a ten-times growth without a rewrite?&lt;/strong&gt; Offset often struggles with scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you need strict data integrity, where missing or duplicated records can cause financial errors or shake user confidence?&lt;/strong&gt; If so, cursor pagination has you covered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is your data set small and relatively static?&lt;/strong&gt; In that case, offset pagination suffices.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;As a rule of thumb, offset works fine for small, rarely changing data sets, but when the stakes and data volume are high, cursor is the better choice.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Follow Best Practices for Pagination Implementation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tune your page size:&lt;/strong&gt; Too small means more requests; too large slows responses. Find a number based on your own data that keeps performance and UX smooth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan for compatibility:&lt;/strong&gt; When migrating from offset to cursor pagination, support both temporarily or use API versioning with clear deprecation timelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose cursors wisely:&lt;/strong&gt; Pick indexed, immutable, unique fields (like timestamp + ID combo) or UUIDs, if they're your primary identifiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle errors gracefully:&lt;/strong&gt; If a cursor becomes invalid, return clear errors (&lt;code&gt;400&lt;/code&gt; Bad Request or &lt;code&gt;410&lt;/code&gt; Gone) and prompt users to restart pagination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document thoroughly:&lt;/strong&gt; Explain cursor expiration and end-of-dataset behavior, and include working code examples.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These best practices help your API stay fast, reliable, and developer-friendly, even as data and users grow.&lt;/p&gt;

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

&lt;p&gt;Offset pagination works well for small data sets or quick prototypes. It's straightforward to implement and easy to understand. However, as your data grows, you'll run into performance issues and consistency problems with shifting records.&lt;/p&gt;

&lt;p&gt;Cursor-based pagination handles scale better. It uses unique position markers instead of offsets, which means no skipped or duplicate records when data changes, and query performance stays consistent even with millions of rows.&lt;/p&gt;

&lt;p&gt;If you're building APIs that need to handle growth without sacrificing data consistency, cursor pagination is worth the extra effort. The &lt;a href="https://docs.gusto.com/embedded-payroll/docs/introduction" rel="noopener noreferrer"&gt;Gusto approach&lt;/a&gt; shows how this works in practice—using the right pagination strategy for each endpoint based on how the data behaves.&lt;/p&gt;

</description>
      <category>api</category>
      <category>pagination</category>
      <category>restapi</category>
      <category>backend</category>
    </item>
    <item>
      <title>Skip Elasticsearch: Build Blazing-Fast Full-Text Search Right in Supabase</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Fri, 03 Oct 2025 04:54:23 +0000</pubDate>
      <link>https://dev.to/reclusivecoder/skip-elasticsearch-build-blazing-fast-full-text-search-right-in-supabase-58pf</link>
      <guid>https://dev.to/reclusivecoder/skip-elasticsearch-build-blazing-fast-full-text-search-right-in-supabase-58pf</guid>
      <description>&lt;p&gt;Full-text search is a powerful database feature that allows you to look for specific words or phrases within your text field instead of requiring an exact match of the entire field. For example, you can search for "Python REST" in job descriptions and still match text like "design and develop RESTful APIs using Python and FastAPI". A simple SQL &lt;code&gt;WHERE job_description LIKE '%Python REST%'&lt;/code&gt; query would miss this because it only finds that exact phrase in that exact order. Traditional queries require precise column matches; on the other hand, full-text search finds any documents containing your terms and even ranks them by relevance. It powers common search features, such as searching blog posts or emails in your inbox, with just a few keywords.&lt;/p&gt;

&lt;p&gt;Modern applications handle massive amounts of text, and users expect instant Google-like results when they type queries like "bluetooth headphones" while searching for products. To achieve this without extra infrastructure, Postgres's built-in full-text capabilities offer a major advantage over external tools like &lt;a href="https://en.wikipedia.org/wiki/Elasticsearch" rel="noopener noreferrer"&gt;Elasticsearch&lt;/a&gt;. By keeping your search indexes inside the database, they stay perfectly synchronized with your other structured data through database transactions. This approach effectively eliminates the complexity of maintaining a separate search tool and reduces both operational overhead and costs.&lt;/p&gt;

&lt;p&gt;In this article, I explain what full-text search is and how to implement it directly in Supabase using practical examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Full-Text Search in Postgres
&lt;/h2&gt;

&lt;p&gt;Supabase's managed Postgres service gives you direct access to Postgres's built-in full-text search features. Internally, Postgres converts each natural-language document into a &lt;em&gt;search vector&lt;/em&gt; (&lt;code&gt;tsvector&lt;/code&gt;, an internal format that stores normalized words and their positions) and turns your keyword search input into a &lt;em&gt;search query&lt;/em&gt; (&lt;code&gt;tsquery&lt;/code&gt;, the parsed form of your keywords). Postgres applies weighted scoring and &lt;a href="https://www.postgresql.org/docs/current/fuzzystrmatch.html" rel="noopener noreferrer"&gt;fuzzy matching&lt;/a&gt; to find results that are similar to your search terms, even when they don't match exactly, using techniques such as &lt;a href="https://www.postgresql.org/docs/current/fuzzystrmatch.html#FUZZYSTRMATCH-SOUNDEX" rel="noopener noreferrer"&gt;Soundex&lt;/a&gt; for phonetic similarity and &lt;a href="https://www.postgresql.org/docs/current/fuzzystrmatch.html#FUZZYSTRMATCH-LEVENSHTEIN" rel="noopener noreferrer"&gt;Levenshtein distance&lt;/a&gt; for character-level similarity. It also handles multiple languages and provides functions for ranking and highlighting search results.&lt;/p&gt;

&lt;p&gt;The diagram below gives a high-level overview of how your documents and queries flow through indexing, matching, and scoring to produce the final ranked results:&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%2Fqwlzecw58bd9ek5q1eju.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%2Fqwlzecw58bd9ek5q1eju.png" alt="Full-text search" width="678" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full-text search is designed to efficiently find and rank documents that contain human-language text, such as blog posts, product descriptions, or user-generated content. Unlike simple string matching, it breaks down and analyzes text to understand the structure of words so users can search more naturally, even with partial matches or varied word forms.&lt;/p&gt;

&lt;p&gt;Let's take a look at a few key concepts that make this possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lexemes: The Building Blocks of Search
&lt;/h3&gt;

&lt;p&gt;When Postgres processes a block of text, it first normalizes it. This involves removing punctuation, ignoring common stop words (such as "and" and "the"), converting everything to lowercase, and reducing words to their root forms (known as &lt;em&gt;stemming&lt;/em&gt;). These root words are called &lt;em&gt;lexemes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For example, "running", "ran", and "runs" all become the lexeme &lt;code&gt;run&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This process ensures that different forms of the same word still work correctly in your search.&lt;/p&gt;

&lt;h3&gt;
  
  
  tsvector and tsquery: Index and Query Formats
&lt;/h3&gt;

&lt;p&gt;Postgres uses two special data types to handle full-text search:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tsvector&lt;/code&gt;: This is how a document is stored for search. It contains a list of lexemes, sometimes with position information. You can think of it as a preprocessed searchable version of your text field.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tsquery&lt;/code&gt;: This represents the search input. When a user types a query, Postgres converts it into a structured form (like &lt;code&gt;run &amp;amp; fast&lt;/code&gt;) that can be matched against the &lt;code&gt;tsvector&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Matching is done between &lt;code&gt;tsquery&lt;/code&gt; and &lt;code&gt;tsvector&lt;/code&gt;, not the original text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search Configurations: Supporting Multiple Languages
&lt;/h3&gt;

&lt;p&gt;Postgres supports multiple search configurations, which define how text is broken down and interpreted. For example, using the &lt;code&gt;'english'&lt;/code&gt; configuration will apply stemming rules and stop-word lists specific to English. There are also built-in configurations for other languages—like French, Spanish, and German—each one tuned for linguistic accuracy.&lt;/p&gt;

&lt;p&gt;Based on your application requirements, you can explicitly specify which configuration to use when building a &lt;code&gt;tsvector&lt;/code&gt; or parsing a &lt;code&gt;tsquery&lt;/code&gt;, like the following:&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="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'The quick brown fox'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How Supabase Handles Full-Text Search
&lt;/h3&gt;

&lt;p&gt;Wirh Supabase, you can create &lt;code&gt;tsvector&lt;/code&gt; columns using &lt;a href="https://www.postgresql.org/docs/current/ddl-generated-columns.html" rel="noopener noreferrer"&gt;generated columns&lt;/a&gt; or &lt;a href="https://supabase.com/docs/guides/database/postgres/triggers" rel="noopener noreferrer"&gt;triggers&lt;/a&gt; that automatically update when source text changes, and create &lt;a href="https://www.postgresql.org/docs/current/gin.html" rel="noopener noreferrer"&gt;Generalized Inverted Index (GIN)&lt;/a&gt; indexes for fast querying. Supabase supports features like ranking search results and offers extensions like &lt;a href="https://supabase.com/docs/guides/database/extensions/pgroonga" rel="noopener noreferrer"&gt;&lt;code&gt;PGroonga&lt;/code&gt;&lt;/a&gt; for multilingual search.&lt;/p&gt;

&lt;p&gt;Additionally, Supabase also supports multicolumn searches by combining data into single searchable indexes, as you'll see in the next section. So you get production-grade search features like result ranking, partial word matching, and support for multiple languages built right into Supabase.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Did you know?&lt;/em&gt; GIN is a specialized Postgres index type designed specifically for searching within composite data types like text arrays and tsvectors. RUM indexes are an enhanced alternative to GIN indexes that store additional details like word positions and timestamps, allowing for faster phrase searches and more accurate relevance ranking based on text distance. Supabase supports &lt;a href="https://supabase.com/docs/guides/database/extensions/rum" rel="noopener noreferrer"&gt;RUM indexes&lt;/a&gt; through an extension, making them a good choice for applications that need advanced search ranking, although they have slower index build and insert performance compared to GIN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Full-Text Search in Supabase
&lt;/h2&gt;

&lt;p&gt;Let's build a practical example using a blog platform where users need to search through articles by title, content, and tags. This is a perfect use case for full-text search because users expect to find articles by typing keywords like "react hooks tutorial" rather than remembering exact titles. When designing your schema, identify columns that contain human-readable text that users will search through. These are typically content fields, descriptions, titles, and tags.&lt;/p&gt;

&lt;p&gt;The key to fast full-text search is creating a GIN on your searchable content. GIN indexes, which were mentioned earlier, work like a book's index, mapping each lexeme to all the documents containing it. This makes searches considerably fast even across millions of records. Instead of scanning every row, Postgres can instantly jump to documents containing your search terms. The diagram below illustrates how GIN indexes process documents to extract lexemes and build an inverted index that maps each term to its containing documents:&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%2Fjc9qxmu2a4wi8dhl8rbc.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%2Fjc9qxmu2a4wi8dhl8rbc.png" alt="GIN: Generalized Inverted Index" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, here's how you set up full-text search for the blog example. You can create the immutable wrapper function &lt;code&gt;articles_search_vector(…)&lt;/code&gt; to combine multiple text fields; this tells Postgres the function always returns the same output for the same inputs, which is required for generated columns. Use a generated column that automatically combines multiple text fields into a searchable &lt;code&gt;tsvector&lt;/code&gt;, and finally, add a GIN index for optimal performance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create the articles table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&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;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&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;tags&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create an immutable wrapper function for the search vector&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;articles_search_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;tsvector&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;coalesce&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="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; 
    &lt;span class="n"&gt;coalesce&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="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; 
    &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&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;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;IMMUTABLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Add a generated column that combines searchable fields&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; 
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="n"&gt;tsvector&lt;/span&gt; 
&lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;articles_search_vector&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;tags&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;STORED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create a GIN index for fast searches&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_articles_search&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_vector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run these SQL commands directly in &lt;a href="https://supabase.com/features/sql-editor" rel="noopener noreferrer"&gt;Supabase's SQL Editor&lt;/a&gt; or through any Postgres client connected to your Supabase database.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; Instead of using a generated column, you can also use a database trigger that runs a small function to populate the &lt;code&gt;search_vector&lt;/code&gt; whenever a row is inserted or updated. However, generated columns are often a cleaner and simpler option since they're declarative and inherently maintain transactional consistency.&lt;/p&gt;

&lt;p&gt;Here's how the &lt;code&gt;articles&lt;/code&gt; table would look in Postgres with the generated column:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;content&lt;/th&gt;
&lt;th&gt;tags&lt;/th&gt;
&lt;th&gt;author&lt;/th&gt;
&lt;th&gt;created_at&lt;/th&gt;
&lt;th&gt;search_vector&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;Getting Started with React&lt;/td&gt;
&lt;td&gt;React is a popular JavaScript library for building user interfaces…&lt;/td&gt;
&lt;td&gt;{javascript,react,frontend}&lt;/td&gt;
&lt;td&gt;John Doe&lt;/td&gt;
&lt;td&gt;2025-07-16 10:30:00&lt;/td&gt;
&lt;td&gt;'build':8 'get':1 'interfac':12 'javascript':6,13 'librari':7 'popular':5 'react':4,14 'start':2 'user':11 'frontend':15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Advanced Python Tips&lt;/td&gt;
&lt;td&gt;Here are some advanced techniques for Python developers…&lt;/td&gt;
&lt;td&gt;{python,programming,tips}&lt;/td&gt;
&lt;td&gt;Jane Smith&lt;/td&gt;
&lt;td&gt;2025-08-07 14:20:00&lt;/td&gt;
&lt;td&gt;'advanc':1,7 'develop':10 'program':12 'python':2,9 'techniqu':8 'tip':3,13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Database Design Best Practices&lt;/td&gt;
&lt;td&gt;Learn how to design efficient database schemas…&lt;/td&gt;
&lt;td&gt;{database,sql,design}&lt;/td&gt;
&lt;td&gt;Bob Wilson&lt;/td&gt;
&lt;td&gt;2025-09-25 09:15:00&lt;/td&gt;
&lt;td&gt;'best':4 'databas':1,7 'design':3,9 'effici':8 'learn':6 'practic':5 'schema':9 'sql':8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;search_vector&lt;/code&gt; column shows the &lt;code&gt;tsvector&lt;/code&gt; format, where each lexeme is followed by the positions where it appears in the combined text. For example, &lt;code&gt;'react':4,14&lt;/code&gt; means the word "react" appears at positions 4 and 14 in the processed text. Notice how Postgres reduces words like "getting" and "practices" to their root forms as "get" and "practic" in the &lt;code&gt;search_vector&lt;/code&gt; to improve matching across word variations.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://supabase.com/docs/guides/database/full-text-search#to-tsvector" rel="noopener noreferrer"&gt;&lt;code&gt;to_tsvector('english', text)&lt;/code&gt;&lt;/a&gt; function does the heavy lifting: It normalizes your text by removing punctuation, converting to lowercase, removing stop words, and stemming words to their root forms (so "running" becomes "run"). The &lt;code&gt;coalesce(…)&lt;/code&gt; functions handle null values by returning an empty string if any field is null, ensuring the search vector can always be built even when some fields are missing data. The generated column automatically updates whenever you insert or update records, keeping your search index perfectly synchronized. With this setup, you're ready to perform fast searches across your entire content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Full-Text Search Queries with Supabase
&lt;/h2&gt;

&lt;p&gt;Now that the search index is ready, you can start querying. Postgres uses the &lt;a href="https://supabase.com/docs/guides/database/full-text-search#to-tsquery" rel="noopener noreferrer"&gt;&lt;code&gt;to_tsquery()&lt;/code&gt;&lt;/a&gt; function to convert your search terms into the structured format needed for matching. Just like documents are converted to the &lt;code&gt;tsvector&lt;/code&gt; format, your search input gets converted to the &lt;code&gt;tsquery&lt;/code&gt; format. The &lt;a href="https://supabase.com/docs/guides/database/full-text-search#match" rel="noopener noreferrer"&gt;@@ operator&lt;/a&gt; checks if a search query matches against a text search vector.&lt;/p&gt;

&lt;p&gt;The simplest search looks for articles containing specific keywords. You can also combine multiple search terms using &lt;code&gt;AND&lt;/code&gt; (&lt;code&gt;&amp;amp;&lt;/code&gt;) and &lt;code&gt;OR&lt;/code&gt; (&lt;code&gt;|&lt;/code&gt;) operators to create more precise queries. For example, searching for &lt;code&gt;'react &amp;amp; javascript'&lt;/code&gt; finds articles containing both terms, while &lt;code&gt;'react | python'&lt;/code&gt; finds articles containing either term. You can also exclude specific terms from your search using the &lt;code&gt;NOT&lt;/code&gt; operator (&lt;code&gt;!&lt;/code&gt;), which is helpful when you want to find articles about programming but exclude certain languages or topics.&lt;/p&gt;

&lt;p&gt;Here are some practical search examples using the blog articles table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Simple keyword search&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;author&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'react'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Search with AND logic (both terms must be present)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;author&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'react &amp;amp; javascript'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Search with OR logic (either term can be present)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;author&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'python | javascript'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Exclude terms using the NOT operator (!)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;author&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;search_vector&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'programming &amp;amp; !python'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These queries return results instantly thanks to the GIN index created earlier. The &lt;code&gt;@@&lt;/code&gt; operator matches your &lt;code&gt;tsquery&lt;/code&gt; against the preprocessed &lt;code&gt;search_vector&lt;/code&gt;, making even complex searches across large data sets remarkably fast. Remember that stemming works both ways: Searching for "develop" will match articles containing "developer", "development", or "developing". For more full-text search operators and functions, refer to this &lt;a href="https://www.postgresql.org/docs/current/textsearch-controls.html" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Full-Text Search Techniques in Supabase
&lt;/h2&gt;

&lt;p&gt;Real search applications need to rank results by relevance, not just list matches. Postgres provides two main ranking functions: &lt;code&gt;ts_rank()&lt;/code&gt; calculates relevance based on how frequently search terms appear, while &lt;code&gt;ts_rank_cd()&lt;/code&gt; (&lt;a href="https://www.postgresql.org/docs/current/textsearch-controls.html#:~:text=cover%20density" rel="noopener noreferrer"&gt;cover density&lt;/a&gt;) considers how close together your search terms appear in the document. For example, there is a preference for results where "Python" and "microservices" occur next to each other as part of a phrase over results where they are scattered in different paragraphs. Results with higher scores appear first, giving users the most relevant content at the top.&lt;/p&gt;

&lt;p&gt;Beyond just ranking by term frequency, you can make certain parts of your documents more important than others using &lt;a href="https://www.postgresql.org/docs/current/textsearch-controls.html#:~:text=setweight%20can%20be%20used%20to%20label%20the%20entries%20of%20a%20tsvector%20with%20a%20given%20weight" rel="noopener noreferrer"&gt;&lt;code&gt;setweight()&lt;/code&gt;&lt;/a&gt;. For example, matches in titles should typically rank higher than matches in body content since titles are more descriptive of the document's main topic. Postgres supports four weight classes—A (highest), B, C, and D (lowest)—allowing you to prioritize certain fields over others.&lt;/p&gt;

&lt;p&gt;Here's how you can implement weighted ranking that prioritizes title matches over content matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create an immutable wrapper function for the weighted search vector&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;articles_weighted_search_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;tsvector&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coalesce&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="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
         &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coalesce&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="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s1"&gt;'B'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
         &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s1"&gt;'D'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;IMMUTABLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create a weighted search vector (A = title, B = content, D = tags)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; 
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;weighted_search_vector&lt;/span&gt; &lt;span class="n"&gt;tsvector&lt;/span&gt; 
&lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;articles_weighted_search_vector&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;tags&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;STORED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create index on the weighted vector&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_articles_weighted_search&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weighted_search_vector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Search with relevance ranking&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weighted_search_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&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;rank&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'react &amp;amp; javascript'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;weighted_search_vector&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;query&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;rank&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;articles_weighted_search_vector()&lt;/code&gt; function creates a search vector that prioritizes different parts of the article content: It gives titles the highest weight ('A'), the content medium weight ('B'), and the tags the lowest weight ('D'). The resulting &lt;code&gt;weighted_search_vector&lt;/code&gt; ranks search matches in titles higher than matches in content or tags. Here's how the &lt;code&gt;weighted_search_vector&lt;/code&gt; column would look:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;weighted_search_vector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;'applic':19B 'build':12B 'compon':22B 'creat':16B 'dynam':17B 'frontend':25 'get':1A 'interfac':14B 'javascript':9B,23 'librari':10B 'popular':8B 'react':4A,5B,24 'reusabl':21B 'start':2A 'user':13B 'web':18B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;'advanc':1A,7B 'best':20B 'code':18B 'develop':11B 'effici':15B 'maintain':17B 'practic':21B 'program':23 'python':2A,10B,22 'techniqu':8B 'tip':3A,24 'use':19B 'write':13B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;'across':19B 'applic':21B 'best':3A 'complex':20B 'data':17B 'databas':1A,10B,22 'design':2A,8B,24 'effici':9B 'integr':18B 'learn':5B 'maintain':16B 'practic':4A 'scale':13B 'schema':11B 'sql':23 'well':14B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This approach ensures that articles with your search terms in the title rank higher than those with the same terms buried in the content (Google uses this approach for its search results). The &lt;code&gt;ts_rank()&lt;/code&gt; function automatically considers the weights when calculating relevance scores, delivering more intuitive search results to your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizing Full-Text Search Performance
&lt;/h2&gt;

&lt;p&gt;Now that you've covered the basic and advanced search techniques, let's explore some practical tips to avoid common pitfalls and keep your search efficient in production.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use a GIN index on your &lt;code&gt;tsvector&lt;/code&gt; column:&lt;/strong&gt; Skipping the GIN index (or using a generic index) is a common mistake. A GIN index allows Postgres to quickly look up matching documents without scanning the entire table.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set the correct language configuration:&lt;/strong&gt; When generating the search vector, choose the right language (&lt;em&gt;eg&lt;/em&gt; &lt;code&gt;'french'&lt;/code&gt; or &lt;code&gt;'german'&lt;/code&gt;) with &lt;code&gt;to_tsvector()&lt;/code&gt;. This ensures words are stemmed and filtered correctly, which is essential if your application serves non-English users.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep your search vector updated:&lt;/strong&gt; As explained earlier in this article, use generated columns with immutable wrapper functions to update the vector automatically when content changes. Alternatively, automate updates with Supabase database triggers so your search index always stays in sync without manual effort.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Precompute results with materialized views:&lt;/strong&gt; For frequently searched content that doesn't change often, use &lt;a href="https://supabase.com/blog/postgresql-views#what-is-a-materialized-view" rel="noopener noreferrer"&gt;materialized views&lt;/a&gt; to store precomputed search results and refresh them periodically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Partition large tables:&lt;/strong&gt; For large data sets, &lt;a href="https://supabase.com/docs/guides/database/partitions" rel="noopener noreferrer"&gt;partition tables&lt;/a&gt; by date or category to keep indexes smaller and run queries faster. This is especially useful for blogs or news articles, where recent content is searched more often than older entries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitor and refine queries with the Supabase Performance Advisor:&lt;/strong&gt; Use &lt;a href="https://supabase.com/blog/security-performance-advisor#performance-advisor" rel="noopener noreferrer"&gt;Performance Advisor&lt;/a&gt; to track slow search queries and get optimization suggestions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3a9v90ggnldd3vc3135.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%2Fi3a9v90ggnldd3vc3135.png" alt="Supbase: Query Performance Advisor" width="799" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;You've now covered the essentials of full-text search and how to implement it efficiently using Supabase and Postgres. From working with &lt;code&gt;tsvector&lt;/code&gt; and &lt;code&gt;tsquery&lt;/code&gt; to using GIN indexes, generated columns, and weighted ranking, you have all the necessary knowledge to build fast, accurate, and fully integrated full-text search without any external dependency— no need to set up a separate Elasticsearch cluster.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>supabase</category>
      <category>database</category>
      <category>fulltext</category>
    </item>
    <item>
      <title>AI can write code, but should you trust it blindly?</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Wed, 06 Aug 2025 12:35:37 +0000</pubDate>
      <link>https://dev.to/reclusivecoder/ai-can-write-code-but-should-you-trust-it-blindly-2299</link>
      <guid>https://dev.to/reclusivecoder/ai-can-write-code-but-should-you-trust-it-blindly-2299</guid>
      <description>&lt;p&gt;AI can churn out code faster than you can say “Stack Overflow”,  but can it build software that is actually &lt;em&gt;reliable&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;There’s no denying that AI-assisted coding with tools like ChatGPT, Claude, and others has changed the game. They can autocomplete functions, generate boilerplate code, and even refactor entire chunks of a project. But here’s the catch—AI doesn’t &lt;em&gt;understand&lt;/em&gt; code the way an experienced developer does. AI doesn’t have the scars of battle-tested experience—those hard-earned lessons from debugging nightmares, handling bizarre edge cases, and wrestling with unexpected challenges in production.&lt;/p&gt;

&lt;p&gt;AI confidently generates what &lt;em&gt;looks&lt;/em&gt; right, yet subtle errors can slip in—the kind that might go unnoticed in a marketing copy but, in software, can snowball into sneaky, hard-to-trace bugs that might show up at the worst possible moment. And if you don’t fully grasp the code AI is producing, you might end up in serious trouble faster than you expect.&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%2Fejl3mw5a9l6losxptfsm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fejl3mw5a9l6losxptfsm.jpg" alt="AI can write code, but should you trust it blindly?" width="680" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;The illusion of perfect code: When AI gets it wrong&lt;/h2&gt;

&lt;p&gt;AI-generated code might look polished and ready to go, but appearances can be deceiving. AI can introduce subtle, dangerous mistakes. These aren’t your average syntax errors that a compiler will catch. These are logic flaws—silent troublemakers that can lurk in your code for weeks or months before causing havoc.&lt;/p&gt;

&lt;p&gt;To understand how AI can introduce subtle but serious bugs, let’s start with a simple example and then examine a real-world scenario where the stakes are much higher.&lt;/p&gt;

&lt;h3&gt;The “Infinite loop” that can bring down production&lt;/h3&gt;

&lt;p&gt;AI might suggest an automatic retry mechanism (the &lt;code&gt;while&lt;/code&gt; loop) without properly handling error or exit conditions. This might lead to a server getting flooded with requests in a failure scenario.&lt;br&gt;&lt;br&gt;Consider this example of an API call:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# AI-generated code for an API call
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# ignore
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's wrong?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Interestingly, the &lt;code&gt;pass&lt;/code&gt; statement in the exception handling introduces a subtle bug here by completely suppressing errors, making debugging difficult and leaving the caller unaware of failures. The &lt;code&gt;while True&lt;/code&gt; loop creates an infinite retry mechanism with no exit condition. If the API goes down, this code will hit the server relentlessly, potentially causing a a &lt;strong&gt;DoS&lt;/strong&gt; (&lt;a href="https://en.wikipedia.org/wiki/Denial-of-service_attack" rel="noreferrer noopener nofollow"&gt;denial-of-service&lt;/a&gt;) attack, which could bring it down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix these problems caused by AI code? &lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is a better way to write this code with retry limits, and handling the exception with &lt;em&gt;exponential backoff&lt;/em&gt; - a retry strategy where a system waits increasingly longer between each retry (e.g., 1s, 2s, 4s, 8s…) to avoid overwhelming a service and improve stability.&lt;/p&gt;



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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Limit retries
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Exponential backoff
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this simple example shows a rather trivial and clear mistake, most modern AI tools (like GitHub Copilot or ChatGPT) would likely avoid such an obvious error. However, this doesn’t mean AI-generated code is perfect. Instead, the real danger lies in subtle, hard-to-detect bugs that can creep into more complex scenarios—bugs that even experienced developers might miss without careful review. Consider the next example -&lt;/p&gt;

&lt;h3&gt;User Registration with Flask and Celery&lt;/h3&gt;

&lt;p&gt;Let’s examine a more realistic scenario: a user registration feature where account setup is typically handled as a background task using Celery (skipping result URL for simplicity here). Here’s the AI-generated code:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Celery&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CELERY_BROKER_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis://localhost:6379/0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;celery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Celery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;broker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CELERY_BROKER_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# In-memory "database" for simplicity
&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="nd"&gt;@celery.task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_user_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;   
    &lt;span class="c1"&gt;# Fetch user
&lt;/span&gt;    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; not found!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="c1"&gt;# Simulate a time-consuming setup process
&lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Update user status to "active"
&lt;/span&gt;    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; setup complete. Status: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Send welcome mail to the user
&lt;/span&gt;
&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/register&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;register_user&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id and email are required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;

    &lt;span class="c1"&gt;# Create user with "pending" status
&lt;/span&gt;    &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Trigger background task
&lt;/span&gt;     &lt;span class="n"&gt;setup_user_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User registered successfully!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What’s Wrong?&lt;/strong&gt;&lt;br&gt;Note that the code simplified for this article—so no DB operations, or duplicate checks here. At first glance, the code seems fine (especially to inexperienced developers)—it creates a user with a &lt;code&gt;"pending"&lt;/code&gt; status, triggers a background task to complete the setup, and updates the status to &lt;code&gt;"active"&lt;/code&gt; once done. But there are quite a few problems here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No error handling&lt;/strong&gt;: If the task fails (e.g., due to an exception), the user’s status remains &lt;code&gt;"pending" &lt;/code&gt;indefinitely, leaving the system in an inconsistent state.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;No retry mechanism&lt;/strong&gt;: If the task fails due to transient issues (network problems, database timeouts), there's no system to retry the operation. This means the user’s account setup might never complete.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Race condition bug&lt;/strong&gt;: The &lt;code&gt;setup_user_account(...)&lt;/code&gt; code does some setup work (simulated with &lt;code&gt;sleep(5)&lt;/code&gt;, then updates the status without checking if it changed meanwhile. If two tasks process the same user concurrently, they could overwrite each other's changes, potentially causing data corruption.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Non-Idempotent operations&lt;/strong&gt;: If the task runs multiple times for the same user (due to duplicated messages or manual retries), it will perform the same operations repeatedly without checking if they've already been done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;strong&gt;How to fix these problems caused by AI code?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here’s how to improve the code to handle these issues:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Celery&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CELERY_BROKER_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis://localhost:6379/0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;celery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Celery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;broker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CELERY_BROKER_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# In-memory "database" for simplicity
&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="nd"&gt;@celery.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_user_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Fetch user
&lt;/span&gt;        &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; not found!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;# Check if operation has already been performed (idempotency)
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; already active, skipping setup.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;# Simulate a time-consuming setup process
&lt;/span&gt;        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Atomic update with proper check to avoid race conditions
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Check current state
&lt;/span&gt;            &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;    &lt;span class="c1"&gt;# Update only if still in expected state
&lt;/span&gt;            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; setup complete. Status: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# Send welcome mail to the user only on first successful activation
&lt;/span&gt;        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; status changed unexpectedly to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Log the error and retry with exponential backoff
&lt;/span&gt;        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error setting up account for user &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;countdown&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/register&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;register_user&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id and email are required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;

    &lt;span class="c1"&gt;# Create user with "pending" status
&lt;/span&gt;    &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Trigger background task
&lt;/span&gt;    &lt;span class="n"&gt;setup_user_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User registered successfully!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This improved code offers several key advantages over the AI-generated code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Error handling and Retry mechanism&lt;/strong&gt;: The improved code uses a proper &lt;code&gt;try/except&lt;/code&gt; block with a retry mechanism that attempts the operation up to 3 times with increasing delays between attempts (exponential backoff). This ensures that transient issues (e.g., network failures ) are handled gracefully.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Race condition protection&lt;/strong&gt;: The improved code checks the current state before making changes, ensuring that updates only happen if the user is still in the expected state (&lt;code&gt;pending&lt;/code&gt;). This prevents concurrent tasks from corrupting each other's work.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Idempotency&lt;/strong&gt;: The code now checks if the user is already active before attempting to activate them again, making the operation idempotent. If the task runs multiple times, subsequent runs will detect that the work is already done.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Minor improvement:&lt;/strong&gt; Python added typing hints relatively late (&lt;a href="https://peps.python.org/pep-0484/" rel="noopener noreferrer"&gt;PEP 484&lt;/a&gt;). Since most GenAI text models (such as older ChatGPT, Gemini, Claude) are trained on the older code samples, they often skip type hints in their generated code. When manually revising AI code, experience developers would typically add these hints (like &lt;code&gt;def setup_user_account(self, user_id: str)&lt;/code&gt;) to help IDEs and linters catch potential errors before runtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These improvements transform the fragile AI-generated code into a robust one that can handle real-world conditions like network failures, concurrent operations, and duplicate task executions.&lt;/p&gt;

&lt;p&gt;It is important to note that this is a simplified example - actual real-life code will have far more complexities to handle DB with proper transaction mechanism, or to setup different systems for the newly registered user.&lt;/p&gt;

&lt;h3&gt;AI coding problems: Lessons from my own experience&lt;/h3&gt;

&lt;p&gt;While the examples above suggest how AI-generated code can overlook critical programming considerations, I’ve also encountered real-world issues when using GenAI models in my own coding work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Struggling with unique data generation:&lt;/strong&gt; While writing an article on database indexing, I needed thousands of records with unique names and email IDs to demonstrate indexing impact. ChatGPT managed about 100+ unique entries before it started generating duplicates. Instead of removing multiple duplicates, I knew I was better off writing a short Python script to generate the necessary DB insert statements myself.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Hallucinated methods in AI responses:&lt;/strong&gt; One of the more surprising moments came when Gemini confidently suggested non-existent methods for Python’s &lt;code&gt;aiohttp&lt;/code&gt; library. I double-checked multiple versions—those methods simply didn’t exist. Gemini AI had just made them up.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Struggles with building a complete app:&lt;/strong&gt; I recently tried using ChatGPT and Claude to generate a small but complete application. Initially, I provided an image of the UI I had in mind and asked them to build the app—both failed miserably, with Claude producing more convoluted but still incorrect code. Then, I changed my approach, describing the app’s functionality and asking them to generate the UI (React) and backend (Flask). Even then, fixing one issue often broke something else, leading to a frustrating loop. Both couldn't deal with entire context of the application, despite it being a  small application. In the end, I had to build it piece by piece using my own architecture and design, which finally worked. These days, I continue with this piece-by-piece approach—using AI for small code snippets while keeping a watchful eye on its output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Why AI misses coding nuances?&lt;/h2&gt;

&lt;p&gt;Surprising as it may sound, AI doesn’t really &lt;em&gt;understand&lt;/em&gt; code—it predicts patterns based on vast datasets of existing examples. If those datasets contain outdated or poorly written code, AI can unknowingly replicate bad practices, even if the output &lt;em&gt;looks&lt;/em&gt; clean. It may generate code based on older libraries that are no longer supported (deprecated), hallucinate non-existent methods, or overlook critical security considerations—especially when using general-purpose GenAI text models like ChatGPT, Gemini, or Claude for code generation.&lt;/p&gt;

&lt;p&gt;And here’s the kicker: AI doesn’t inherently &lt;em&gt;think&lt;/em&gt; about security risks. Unless explicitly prompted, it won’t remind you to follow best practices, like referring credentials from an &lt;code&gt;.env&lt;/code&gt; file instead of putting them in code, or protecting yourself against SQL injections and so on. That’s where developer knowledge and experience come into play.&lt;/p&gt;

&lt;p&gt;To be fair, the examples used here are relatively simple—most specialized coding AI assistants, like GitHub Copilot or Cursor AI, would likely get them right on the first try. Moreover, dedicated AI coding platforms are evolving rapidly, and from what I’ve heard, they’re generating increasingly impressive results. However, &lt;strong&gt;it is important to understand that while AI can generate syntactically correct code, it may introduce security vulnerabilities, architectural inefficiencies, or unexpected behavior due to a lack of true comprehension&lt;/strong&gt;. It can generate syntactically correct code, but that doesn’t mean the code is always reliable, secure, or maintainable.&lt;/p&gt;

&lt;h2&gt;Programming is not just writing code&lt;/h2&gt;

&lt;p&gt;There’s a big difference between writing code that &lt;em&gt;works&lt;/em&gt; &lt;em&gt;somehow&lt;/em&gt; and writing code that &lt;em&gt;keeps working&lt;/em&gt; &lt;em&gt;flawlessly&lt;/em&gt;. AI can handle the former, but the latter takes some experience.&lt;/p&gt;

&lt;p&gt;Real-world programming isn’t just about codifying a few logical steps to make something run—it’s about designing readable, maintainable, adaptable systems that don’t collapse under their own weight the moment requirements change (because they &lt;em&gt;*will*&lt;/em&gt; change). AI doesn’t have that kind of foresight—it lacks the battle-tested wisdom that comes from debugging disasters, refactoring legacy code, and making trade-offs that only years of hands-on problem-solving can teach.&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%2Fsbrwbwx7nhdm1kgrsloa.jpeg" 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%2Fsbrwbwx7nhdm1kgrsloa.jpeg" alt="Programming is not just writing code." width="576" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;The higher-order skills that AI can’t replace&lt;/h3&gt;

&lt;p&gt;Software development isn’t just about writing code—it’s about making the right decisions before a single line is even written. Skills like prudent design, weighing trade-offs, and building for the unknown are more like creativity than mere logic. They have a high ceiling that AI can’t easily reach.&lt;/p&gt;

&lt;p&gt;AI can churn out code quickly, but it struggles with the nuanced art of crafting robust, adaptable solutions that stand the test of time. Humans, on the other hand, develop nuanced professional insights through years of solving real-world problems—something AI just can’t replicate. At least, &lt;em&gt;not yet!&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;Why human judgment still matters&lt;/h3&gt;

&lt;p&gt;Generative AI models can produce text in seconds based on your prompt—whether it’s a newsletter draft or a working Python MVP. But in software development, the stakes are much higher. A small error in a newsletter won’t cause much trouble—you can always send a follow-up or a quick &lt;em&gt;‘PS’&lt;/em&gt; to fix it. A hidden bug in production, on the other hand, can cost millions, damage reputations, or even put lives at risk in critical systems like healthcare and aviation.&lt;/p&gt;

&lt;p&gt;That’s why context, accuracy, and human judgment aren’t just useful in software development—they’re absolutely essential.&lt;br&gt;&lt;br&gt;AI is undoubtedly a powerful tool, but it is only as good as the hands that guide it. I cannot stress this enough—having an expert developer or architect review and refine AI-generated code isn’t just important; it’s non-negotiable. AI can assist, but only human oversight ensures correctness, reliability, and sound architectural decisions.&lt;/p&gt;

&lt;h2&gt;How to use AI wisely in your work&lt;/h2&gt;

&lt;p&gt;Using AI wisely in your work deserves a separate blog-post of its own, but here are some key principles to keep in mind while using AI for software development:&lt;/p&gt;

&lt;ol start="1"&gt;
&lt;li&gt;
&lt;strong&gt;Review everything&lt;/strong&gt;: Treat AI-generated code as a first draft. Always review it line by line, looking for subtle errors or inefficiencies.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Understand the code&lt;/strong&gt;: Don’t just copy and paste. Make sure you understand what the code does and how it works.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Test rigorously&lt;/strong&gt;: Use automated tests, manual testing, and code reviews help catch hidden bugs before they turn into costly problems.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Leverage AI for the right tasks&lt;/strong&gt;: Allow AI handle boilerplate code, writing unit tests, documentation, or automation so you can focus on real problem-solving.&lt;/li&gt;



&lt;li&gt;
&lt;strong&gt;Learning with AI:&lt;/strong&gt; AI can be an excellent tutor for beginners, offering insights on almost any topic. It’s great for exploring new programming languages or unfamiliar fields—I’ve certainly enjoyed diving into eclectic topics with its help.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;The future belongs to developers who adapt&lt;/h2&gt;

&lt;p&gt;AI isn’t here to replace developers—it’s here to push us to evolve. The ones who thrive won’t be those who blindly follow AI’s suggestions, but those who wield it as a powerful tool to amplify their creativity, efficiency, and problem-solving skills.&lt;/p&gt;

&lt;p&gt;While asking your AI assistant to “Pls fix” might seem like the quickest route, understanding why something works (or doesn’t) is often not just the better path—but sometimes even the shorter one.&lt;/p&gt;

&lt;p&gt;AI isn’t just knocking on the door of the tech industry—it has already stormed in, taken a seat at the table, and started rewriting the rules across every field. From coding to design, from legal briefs to medical breakthroughs, it’s weaving itself into industries at an unstoppable pace. AI is becoming as ubiquitous as Wi-Fi. And just like Wi-Fi, you don’t need to know the intricacies of how it works—but if you don’t know how to connect, you’ll be stuck buffering while the world streams ahead.&lt;/p&gt;

&lt;p&gt;In the end, the developers who embrace AI as an ally—not a crutch—will be the ones shaping the future. Those who resist adapting may find the industry moving forward without them.&lt;/p&gt;

&lt;p&gt;So, the real question is: &lt;strong&gt;which side of the future do you want to be on?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpcupewshvlqwduz1k635.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpcupewshvlqwduz1k635.jpg" alt="AI is not going to replace humans, but humans with AI are going to replace humans without AI." width="680" height="288"&gt;&lt;/a&gt;&lt;/p&gt;





&lt;h2&gt;Updates: 19 March 2025&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Update 1 — A real-world example of AI-generated code gone wrong&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is what I've written in this article -&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you don’t fully understand the code AI is producing, you might end up in serious trouble faster than you expect. &lt;br&gt;&lt;br&gt;AI doesn’t inherently think about security risks. Unless explicitly prompted, it won’t remind you to follow best practices, like....&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And here is a &lt;a href="https://x.com/leojr94_/status/1901560276488511759" rel="noreferrer noopener"&gt;real-life post&lt;/a&gt; that exemplifies what I was saying. 👇&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%2Fbrf6s2nbktwdxinw0hrn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbrf6s2nbktwdxinw0hrn.jpg" alt="AI Doesn't understand security inherantly." width="800" height="955"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update 2 — Anthropic CEO on AI Coding&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"In 12 months, we may be in a world where AI [artificial intelligence] is essentially writing all of the code," said Anthropic CEO and Cofounder Dario Amodei. Watch it here -&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/0j1HqEEDThc" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Amodei says a programmer will still need to specify certain conditions of what the AI model is attempting to execute (such as, whether it is a secure design or insecure design and other considerations), and argues that "human productivity will actually be enhanced" by AI. (You can watch more of his insights &lt;a href="https://www.cfr.org/event/ceo-speaker-series-dario-amodei-anthropic" rel="noreferrer noopener"&gt;here&lt;/a&gt;).&lt;br&gt;&lt;br&gt;2025 is shaping up to be an interesting year for AI. Maybe I’ll write another blog post here on how to use it to generate correct code effectively—once I’ve wrestled with it enough to come away with some actually useful insights! ¯\_(ツ)_/¯&lt;/p&gt;





&lt;p&gt;What’s your experience with AI-assisted coding? Share your thoughts in the comments below.&lt;/p&gt;



</description>
      <category>programming</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>Build a Stunning Portfolio in Minutes with Python &amp; Bootstrap</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Mon, 11 Sep 2023 16:32:43 +0000</pubDate>
      <link>https://dev.to/reclusivecoder/build-a-stunning-portfolio-in-minutes-with-python-bootstrap-303p</link>
      <guid>https://dev.to/reclusivecoder/build-a-stunning-portfolio-in-minutes-with-python-bootstrap-303p</guid>
      <description>&lt;p&gt;I needed a simple way to extract and add snippets created from the &lt;a href="https://ogp.me/" rel="noopener noreferrer"&gt;Open Graph&lt;/a&gt; tags (&lt;code&gt;og:xyz&lt;/code&gt;) of my own articles, published across various websites.The goal was to quickly put together my writing portfolio. The Open Graph snippets (aka &lt;em&gt;“cards”&lt;/em&gt;), are those good-looking previews visible on social media platforms when you share a link/URL as shown below.&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%2Fayly6xbfq61c40jolrle.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%2Fayly6xbfq61c40jolrle.png" alt="Open Graph Preview Card" width="439" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I couldn’t find one such tool/script to accomplish my goal. So I decided to write one myself. Follow along for this brief tutorial, or simply use the free portfolio-maker tool directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisite
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Some familiarity with basic HTML and CSS. Although, the tool will automatically create a portfolio for you if you’re fine with the default theme.&lt;/li&gt;
&lt;li&gt;Familiarity with Python – installing packages and running Python code from the command line.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Open Graph Tags
&lt;/h3&gt;

&lt;p&gt;Here are some Open Graph tags from an HTML file. These tags are used by the social media platforms to generate attractive previews. &lt;/p&gt;

&lt;p&gt;The Python tool extracts these tags from a website to create a portfolio snippet. It won't be able to create a snippet if these tags are absent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:title"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Web Page Title"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Longer description..."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Image URL"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:url"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Website/Web-page URL"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bootstrap Cards
&lt;/h3&gt;

&lt;p&gt;Since the portfolio needs to be &lt;a href="https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Responsive_Design" rel="noopener noreferrer"&gt;responsive&lt;/a&gt;, this tool uses &lt;a href="https://getbootstrap.com/" rel="noopener noreferrer"&gt;Bootstrap&lt;/a&gt; template - it is a ready-made website framework that you can use to quickly and easily create a responsive website. The portfolio-maker tool only uses the CSS part of the framework, not JavaScript. You can include it in the HTML via CDN as shown below -&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css"&lt;/span&gt; 
&lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;integrity=&lt;/span&gt;&lt;span class="s"&gt;"sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9"&lt;/span&gt; 
&lt;span class="na"&gt;crossorigin=&lt;/span&gt;&lt;span class="s"&gt;"anonymous"&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 next important step is to choose the right &lt;a href="https://getbootstrap.com/docs/4.0/components/card/" rel="noopener noreferrer"&gt;card&lt;/a&gt; template for the website snippets that you want to include in your portfolio. This is crucial to ensure that your Portfolio looks attractive.  You can choose one such card template here - &lt;a href="https://bootstrapious.com/p/cards" rel="noopener noreferrer"&gt;23 Bootstrap Snippets&lt;/a&gt;. I have chosen Bootstrap Card Responsive (&lt;a href="https://codepen.io/wisnust10/pen/BKjNNR" rel="noopener noreferrer"&gt;HTML + CSS&lt;/a&gt;) template for this tool. Besides a few cosmetic changes, I have added height restrictions in the CSS to ensure that the cards are uniform for all the portfolio snippets.&lt;/p&gt;

&lt;p&gt;At the heart of this template, there is a card designed to display a single portfolio item. Additionally, the template makes sure that these cards work well on different devices and screen sizes, even with the images they contain. This means the cards will always resize properly on various screens, including mobile phones (responsive template).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- START: Portfolio Card  --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-xs-12 col-sm-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"img-card"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;url-here&amp;gt;"&lt;/span&gt; &lt;span class="na"&gt;title=&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;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;image-url-here&amp;gt;"&lt;/span&gt; &lt;span class="na"&gt;alt=&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;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;h4&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;url-here&amp;gt;"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    Project/Article Title
                &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/h4&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                Project/Article description in short...
            &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-read-more"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;url-here&amp;gt;"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-outline-info"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                Read More
            &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- END: Card - Career, beyond ladders… --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating Portfolio Snippets with Python
&lt;/h3&gt;

&lt;p&gt;Essentially, the Python code for this tool works by generating a portfolio card for each provided URL. It uses &lt;code&gt;BeautifulSoup&lt;/code&gt; (to parse HTML), &lt;code&gt;Requests&lt;/code&gt; (to make HTTP requests), and &lt;code&gt;Jinja2&lt;/code&gt; templates (to generate dynamic HTML) to create the portfolio snippets. You can install theses packages with pip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;beautifulsoup4&lt;/span&gt;
&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;Jinja2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take a look at this code snippet that extracts Open Graph tags from the web pages. It uses &lt;code&gt;regex&lt;/code&gt; with &lt;code&gt;BeautifulSoup&lt;/code&gt; to extract all the Open Graph tags from the HTML. The code further cleans them (removes &lt;code&gt;og:&lt;/code&gt; part), and returns a simple key-value dictionary of these tags.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_extract_og_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;og_tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.*og:.*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;og_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;og_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;og_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;tag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;og_index&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
                &lt;span class="n"&gt;og_tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid response form the site: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Exception: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;og_tags&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following code snippet invokes the &lt;code&gt;_extract_og_data(...)&lt;/code&gt; for a list of URLs and creates Open Graph data (a list of dictionaries) for creating the portfolio. The list of URLs is read from the text file: &lt;code&gt;urls.txt&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;site_cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;og_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_extract_og_data&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;site_cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;og_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Jinja2 Templates
&lt;/h3&gt;

&lt;p&gt;The portfolio is actually created using a &lt;a href="https://jinja.palletsprojects.com/en/3.0.x/templates/" rel="noopener noreferrer"&gt;Jinja&lt;/a&gt; template. Jinja is a Python template engine that offers a cleaner way to make dynamic HTML pages or other text-based documents.&lt;/p&gt;

&lt;p&gt;In the code snippet below, the Jinja template generates multiple portfolio cards by iterating over the Open Graph data — &lt;code&gt;cards&lt;/code&gt;, and substitutes relevant data (title, image, description etc.) in the HTML template.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% for og_card in cards %}
&lt;span class="c"&gt;&amp;lt;!-- START: Card - {{og_card.title}} --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"col-xs-12 col-sm-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"img-card"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{og_card.url}}"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"{{og_card.title}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{{og_card.image}}"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Featured Image"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;h4&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{og_card.url}}"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"More..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    {{ og_card.title|truncate(30, true) }}
                &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/h4&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                {{ og_card.description|truncate(160) }}
            &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-read-more"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{og_card.url}}"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-outline-info"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"{{og_card.title}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                Read More
            &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- END: Card - {{og_card.title}} --&amp;gt;&lt;/span&gt;
{% endfor %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, the portfolio generated by the template is saved in a new file. You may edit this file to use your own CSS theme. You can see one such generated portfolio here - &lt;a href="https://reclusivecoder.com/manish-hatwalne/writings.php" rel="noopener noreferrer"&gt;The world of expressions&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Portfolio Maker
&lt;/h3&gt;

&lt;p&gt;You can use this tool in two different modes via the command line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Portfolio Mode: This mode generates a single portfolio page, complete with HTML and CSS, using the provided list of URLs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Snippets Mode: In this mode, the tool creates Bootstrap cards. You can then incorporate this generated HTML into your existing portfolio. This mode is particularly useful when you want to seamlessly add more work samples to your current portfolio.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See this tool in action through the animated GIF below.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;An anecdotal joke about developers goes like this: A developer takes 2-3 days to automate a task that could have been finished in 2-3 hours. Well, with this "portfolio-maker", I personify that imaginary developer. :D&lt;/p&gt;

&lt;p&gt;However, now that I've built this tool, all I need to do is add new URLs to the urls.txt file and then just run this tool to create the portfolio in just a few seconds. I don't need to worry about putting the correct links, title, description,  image etc.for every new article that I need to add there. ¯_(ツ)_/¯                 &lt;/p&gt;

&lt;p&gt;Do you want to build a stunning portfolio to impress your potential clients? &lt;/p&gt;

&lt;p&gt;Make use of this &lt;a href="https://manishh.gumroad.com/l/portfolio-maker" rel="noopener noreferrer"&gt;portfolio-maker&lt;/a&gt;, now offered for &lt;em&gt;&lt;strong&gt;free&lt;/strong&gt;&lt;/em&gt; on Gumroad! Plus, it comes with full code access and a helpful video tutorial. :)&lt;/p&gt;




</description>
      <category>python</category>
      <category>bootstrap</category>
      <category>opengraph</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>True Lies of ChatGPT</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Mon, 19 Jun 2023 04:36:00 +0000</pubDate>
      <link>https://dev.to/reclusivecoder/true-lies-of-chatgpt-2gjg</link>
      <guid>https://dev.to/reclusivecoder/true-lies-of-chatgpt-2gjg</guid>
      <description>&lt;p&gt;When I say “true lies”, I mean that ChatGPT is blurring the lines between truth and fiction (lies). If not impossible, it is now extremely difficult to tell what is true and what is not from the text it generates. This can lead to a sense of confusion and uncertainty, as you are unsure what to believe.&lt;/p&gt;

&lt;h4&gt;
  
  
  Does ChatGPT lie?
&lt;/h4&gt;

&lt;p&gt;Well, we already know that ChatGPT lies – and it is not rare either. In fact, it has been called out for lying multiple times, such as in &lt;a href="https://aiiq.substack.com/p/chatgpt-is-a-big-fat-liar-do-not" rel="noopener noreferrer"&gt;this article&lt;/a&gt; and in &lt;a href="https://twitter.com/jordanbpeterson/status/1656681311585665024" rel="noopener noreferrer"&gt;this example&lt;/a&gt;. It probably doesn’t matter much when it comes to gathering information about movies, books etc. However, I was shocked by the convincing fake studies that ChatGPT provided me. They sounded so credible, I &lt;em&gt;almost&lt;/em&gt; believed them…&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%2F1j5gg9d978261se8go37.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1j5gg9d978261se8go37.jpg" alt="Does ChatGPT lie?" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Researching with ChatGPT
&lt;/h2&gt;

&lt;p&gt;I used a materialized view and some indexes to make a complex query on a PostgreSQL database run almost &lt;a href="https://twitter.com/_zenman/status/1476073422949998597" rel="noopener noreferrer"&gt;70 times faster&lt;/a&gt;. For a related presentation, I was searching for publicly available studies on how others have improved their query performance by using materialized views and indexes. Google search didn’t help me much, so I decided to ask ChatGPT for some help.&lt;/p&gt;

&lt;p&gt;ChatGPT, being the helpful assistant it is, provided me with some &lt;em&gt;“real-life”&lt;/em&gt; studies that showed impressive results using materialized views and indexes. However, much to my dismay, I discovered that all those studies were actually FAKE!&lt;/p&gt;

&lt;p&gt;Here are the screenshots of my chat transcript. See for yourself how believable they all seem. I have also included those links after the screenshots – None of them exists though. ¯_(ツ)_/¯&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%2F1yaxrw08mzynaft1fmev.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%2F1yaxrw08mzynaft1fmev.png" alt="ChatGPT – Non-existent cases of performance enhancements with materialized views" width="800" height="1038"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Links provided for the examples –&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;StackOverflow:&lt;/strong&gt; &lt;a href="https://stackoverflow.blog/2016/03/29/how-stack-overflow-builds-and-deploys-its-entire-app-stack/" rel="noopener noreferrer"&gt;https://stackoverflow.blog/2016/03/29/how-stack-overflow-builds-and-deploys-its-entire-app-stack/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AdRoll:&lt;/strong&gt; &lt;a href="https://www.postgresql.eu/events/pgconfeu2018/schedule/session/2103-building-a-scalable-real-time-ad-serving-platform-with-postgresql/" rel="noopener noreferrer"&gt;https://www.postgresql.eu/events/pgconfeu2018/schedule/session/2103-building-a-scalable-real-time-ad-serving-platform-with-postgresql/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zalando:&lt;/strong&gt; &lt;a href="https://www.postgresql.eu/events/fosdem2019/schedule/session/2391-building-a-data-warehouse-with-postgresql/" rel="noopener noreferrer"&gt;https://www.postgresql.eu/events/fosdem2019/schedule/session/2391-building-a-data-warehouse-with-postgresql/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Calling out lies
&lt;/h2&gt;

&lt;p&gt;So, I called out the lies and asked ChatGPT again to provide me with some more credible references.&lt;/p&gt;

&lt;p&gt;Result? Examine it yourself.&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%2F18wj5jin5hq87w3nwkeq.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%2F18wj5jin5hq87w3nwkeq.png" alt="ChatGPT – Non-existent cases of performance enhancements with materialized views" width="800" height="767"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Links provided for the second set of credible examples –&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitLab:&lt;/strong&gt; &lt;a href="https://about.gitlab.com/blog/2020/12/03/gitlab-data-team-reduce-10tb-of-data-to-10gb/" rel="noopener noreferrer"&gt;https://about.gitlab.com/blog/2020/12/03/gitlab-data-team-reduce-10tb-of-data-to-10gb/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TransferWise:&lt;/strong&gt; &lt;a href="https://databaseline.bitbucket.io/postgresql/2020/07/28/materialized-views-performance/" rel="noopener noreferrer"&gt;https://databaseline.bitbucket.io/postgresql/2020/07/28/materialized-views-performance/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TimescaleDB:&lt;/strong&gt; &lt;a href="https://blog.timescale.com/blog/building-materialized-views-in-postgresql-for-fast-queries/" rel="noopener noreferrer"&gt;https://blog.timescale.com/blog/building-materialized-views-in-postgresql-for-fast-queries/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can click on these links and check if any one of them actually exists. I even tried searching some text form those real-life examples, however, Google (or even &lt;a href="https://web.archive.org/" rel="noopener noreferrer"&gt;Way Back Machine&lt;/a&gt;) knew nothing about these links. I have nothing more to add here.&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%2Fcvincc26mgyfxywewsjv.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%2Fcvincc26mgyfxywewsjv.png" alt="Way Back Machine has no record of those study links provided by ChatGPT" width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reasons for these lies
&lt;/h2&gt;

&lt;p&gt;The Large Language Models (LLMs) such as ChatGPT are still evolving. Many important LLM behaviors emerge unpredictably. This &lt;a href="https://en.wikipedia.org/wiki/Emergence" rel="noopener noreferrer"&gt;&lt;em&gt;emergence&lt;/em&gt;&lt;/a&gt; is the ability of LLMs to exhibit new and unexpected behaviors that are not explicitly programmed into them. At this stage, even the experts (including their creators) don’t fully understand how LLMs work. Furthermore, there are no reliable techniques for steering the behavior of LLMs. If you’re truly intrigued by this, please read the related paper by &lt;a href="https://twitter.com/sleepinyourhat" rel="noopener noreferrer"&gt;Sam Bowman&lt;/a&gt;, an expert on LLMs, titled – &lt;a href="https://arxiv.org/pdf/2304.00612.pdf" rel="noopener noreferrer"&gt;Eight Things to Know about Large Language Models&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As such LLMs like ChatGPT do not intentionally lie because they don’t possess consciousness or intentions. They may, however, generate incorrect or misleading information occasionally. This is clearly mentioned as one of the limitations of ChatGPT.&lt;/p&gt;

&lt;p&gt;Think of ChatGPT as an exceptionally imaginative child who may occasionally make up believable stories while answering your questions.&lt;/p&gt;

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

&lt;p&gt;ChatGPT can be used to generate the text that you want, but it is your responsibility to verify and validate the information you receive. Trust me on this one.😛&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Huh? Does this mean that ChatGPT is not really useful?&lt;/p&gt;

&lt;p&gt;Nah! Not at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ChatGPT is extremely useful when you need to come up with different ways of writing some text. It understands the tone (informal, funny, professional etc.) of the written text quite well and can help you to convert from one tone to another. It can explain complex concepts that can be easily understood by a 10-year old, or provide advanced details of that concept to an expert in the field. It can summarize large articles or books. It can even generate poems, limericks, emails, letters, etc. as per the given instructions. It is prudent to use ChatGPT for its strengths.&lt;/p&gt;

&lt;p&gt;Generative AI, whether it is text or image, is getting more and more sophisticated with each passing day. However, it is now more crucial than ever to exercise critical thinking in evaluating the veracity of AI-generated content. I can vouch for this!&lt;/p&gt;

&lt;p&gt;By the way, the featured image (liar ChatGPT/Pinocchio) used in this article is created by DALL-E (via Bing), using a prompt generated by ChatGPT itself. Isn’t that cool?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This blog post is based on my experience with the free version of ChatGPT (3.5?) that was available in May 2023. I’ve been informed that GPT-4 is an enhanced product, and its behavior may differ from what is described here.&lt;/p&gt;

</description>
      <category>chatgpt</category>
      <category>generativeai</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Why you should write constants on the LHS for comparisons?</title>
      <dc:creator>Manish</dc:creator>
      <pubDate>Tue, 13 Jul 2021 13:00:49 +0000</pubDate>
      <link>https://dev.to/reclusivecoder/why-you-should-write-constants-on-the-lhs-for-comparisons-de2</link>
      <guid>https://dev.to/reclusivecoder/why-you-should-write-constants-on-the-lhs-for-comparisons-de2</guid>
      <description>&lt;p&gt;I picked up this practice some 20+ years ago in my first job. It was part of the commandments for us newbie programmers back then - we mostly coded in C/C++ those days. We have come a long way since, but I'd argue that this practice could help you save from a blunder some day.&lt;/p&gt;

&lt;p&gt;Let me just quote the commandment here -     &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Thou shall &lt;em&gt;always&lt;/em&gt; put the literals/constants on the LHS and variable on the RHS in the &lt;code&gt;if&lt;/code&gt; condition for equality!&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Code samples:&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="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;deleteAll&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
   &lt;span class="c1"&gt;// delete all records&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="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
   &lt;span class="c1"&gt;// report not available&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rationale here is simple – occasionally we programmers, lost in our efforts to solve the complex problems of universe, end up using a single assignment operator &lt;code&gt;=&lt;/code&gt; instead of the comparison operator &lt;code&gt;==&lt;/code&gt; unknowingly. In case of the languages where &lt;code&gt;if&lt;/code&gt; condition does not only accept a boolean (like C, C++), this could result in unintentional assignment instead of comparison. Moreover, your compiler won’t be able to catch it for these languages (most IDEs these days warn about this though), or it will go unnoticed in scripts like JavaScript. However, when you use literal/constant on the LHS, even an accidental, unintentional assignment is not possible. i.e. you simply cannot have this, compiler will flag an error –&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="k"&gt;if&lt;/span&gt;&lt;span class="o"&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;quantity&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
   &lt;span class="c1"&gt;// report not available&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the compiler, or your IDE &lt;em&gt;might&lt;/em&gt; not flag an error for this -&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="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
   &lt;span class="c1"&gt;// report not available&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;..and that’s &lt;em&gt;exactly&lt;/em&gt; the point. We let the compiler do the work of finding such bugs instead of realizing it at the runtime, and then eventually spending hours debugging and tracking down one small unintentional assignment, which should have been an equality comparison in the first place.&lt;/p&gt;

&lt;p&gt;This approach &lt;em&gt;still&lt;/em&gt; makes sense in 2021 including languages like Java, Kotlin (only boolean in &lt;code&gt;if&lt;/code&gt; condition) etc. for the following reasons - &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Consider boolean comparison itself in Java, what would happen here (ignoring compiler warning for assignment, among tons of other deprecation warnings) if the following code is unintentionally added?
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deleteAll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
    &lt;span class="c1"&gt;//delete everything?&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the other hand, when you write code like this, the compiler would be able to help you, right? 😥&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="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deleteAll&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
    &lt;span class="c1"&gt;//delete everything?&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;More importantly, you seldom code in a single language as you grow as a software developer. There are times when you need to come up with a small snippet of JavaScript code (well, JS is ubiquitous - ain't disappearing for another two decades or more). All these things would be still valid there. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It is not about any one particular language/technology, it is more about developing better coding practices. These practices help us in the long run, and make us better programmers. 😃&lt;/p&gt;

</description>
      <category>programming</category>
      <category>codenewbie</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
