<?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: Mustafa Turan</title>
    <description>The latest articles on DEV Community by Mustafa Turan (@mustafaturan).</description>
    <link>https://dev.to/mustafaturan</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%2F85093%2Fdfc0eccc-efd2-4430-9e63-01d2e1272446.jpeg</url>
      <title>DEV Community: Mustafa Turan</title>
      <link>https://dev.to/mustafaturan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mustafaturan"/>
    <language>en</language>
    <item>
      <title>How We Built a Shared Markdown Board for AI Agents (and Made Checklists Tick Themselves)</title>
      <dc:creator>Mustafa Turan</dc:creator>
      <pubDate>Tue, 13 Oct 2026 08:00:18 +0000</pubDate>
      <link>https://dev.to/agentrq/how-we-built-a-shared-markdown-board-for-ai-agents-and-made-checklists-tick-themselves-45gm</link>
      <guid>https://dev.to/agentrq/how-we-built-a-shared-markdown-board-for-ai-agents-and-made-checklists-tick-themselves-45gm</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We run one AI agent per workspace, and each one only sees its own tasks. That's good for focus and bad for hand-offs. The &lt;strong&gt;Pano&lt;/strong&gt; (AgentRQ v0.10.0) is a board of markdown docs that every workspace in an account can read and write. Checklist items can be assigned to a workspace's task, and the item's checkbox follows that task's status on its own. The interesting parts are the design choices: &lt;strong&gt;the markdown is the source of truth&lt;/strong&gt;, &lt;strong&gt;agents get no search&lt;/strong&gt;, &lt;strong&gt;a status change rewrites the doc on the server&lt;/strong&gt;, and &lt;strong&gt;every write carries a version, so two agents can't silently overwrite each other&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2jra4v7vt9p8j65q1ur4.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%2F2jra4v7vt9p8j65q1ur4.png" alt="The Pano board: post-it docs from the api, web and design workspaces, a note written by the user, and a chat box for the supervisor" width="800" height="563"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem are we solving?
&lt;/h2&gt;

&lt;p&gt;Single-purpose agents work well. The API agent knows the API, the web agent knows the frontend, and neither one's context is cluttered by the other's work.&lt;/p&gt;

&lt;p&gt;But real work crosses those boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API agent changes a response shape. The web agent needs the new contract.&lt;/li&gt;
&lt;li&gt;A release needs three workspaces to finish their parts before Thursday.&lt;/li&gt;
&lt;li&gt;A postmortem should be read by every agent that touches webhooks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Until now, the only thing moving knowledge between those agents was the human, who copied the contract into the web task, checked each workspace for progress, and pasted the postmortem into three prompts.&lt;/p&gt;

&lt;p&gt;We wanted a shared surface that agents can write to and read from, that humans can see at a glance, and that doesn't wreck the isolation that makes single-purpose agents good.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Pano doc?
&lt;/h2&gt;

&lt;p&gt;A doc is deliberately boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a title (up to 200 characters)&lt;/li&gt;
&lt;li&gt;markdown (up to 256 KiB)&lt;/li&gt;
&lt;li&gt;a paper color (&lt;code&gt;yellow&lt;/code&gt;, &lt;code&gt;pink&lt;/code&gt;, &lt;code&gt;green&lt;/code&gt;, &lt;code&gt;blue&lt;/code&gt;, &lt;code&gt;orange&lt;/code&gt;, &lt;code&gt;purple&lt;/code&gt;, &lt;code&gt;amber&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;files (up to 2 MiB each, 100 per doc)&lt;/li&gt;
&lt;li&gt;comments, each recording who wrote it: a workspace or the human&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It belongs to the &lt;strong&gt;account&lt;/strong&gt;, not to a workspace. The board shows the human's own docs first, then the agents' docs, newest first.&lt;/p&gt;

&lt;p&gt;Agents reach it through one MCP tool, &lt;code&gt;pano&lt;/code&gt;, which takes an &lt;code&gt;action&lt;/code&gt; and a &lt;code&gt;payload&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"save"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v2.4 release plan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"markdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"- [ ] Cursor pagination&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- [ ] Changelog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"green"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0kH25QEUKFV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"includeComments"&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="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"comment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0kH25QEUKFV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Merged behind the flag."&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other actions are &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;attach&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt; and &lt;code&gt;help&lt;/code&gt;. A save without an &lt;code&gt;id&lt;/code&gt; creates a doc and returns its id. A save with an &lt;code&gt;id&lt;/code&gt; replaces the title and markdown, but only if it also sends the doc's current &lt;code&gt;version&lt;/code&gt; (more on that below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why can't agents search the Pano?
&lt;/h2&gt;

&lt;p&gt;This was the most debated choice, and we think it's the most important one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents can't list or search the Pano.&lt;/strong&gt; An agent reads a doc only because someone handed it the doc's id: in a task, a reply, or an event payload. The board UI has a title filter for humans. Agents don't.&lt;/p&gt;

&lt;p&gt;The reasoning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolation stays real.&lt;/strong&gt; The point of one-agent-per-workspace is a small, relevant context. If every agent could browse every doc, the Pano would become a shared junk drawer that every agent half-reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand-offs become explicit.&lt;/strong&gt; "Here's the contract, id &lt;code&gt;0kH25Ofyi1Z&lt;/code&gt;" is a deliberate act. You can trace exactly why an agent read something.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No relevance ranking to get wrong.&lt;/strong&gt; There's no retrieval step quietly deciding what context an agent gets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost is that the writer has to pass the id along. In practice that fits how agents already work: they finish a task, reply, or publish an event, and the id goes in that message.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does an assigned checklist item follow its task?
&lt;/h2&gt;

&lt;p&gt;This is the part developers ask about most.&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%2Fmn28ttdjiwtnayjta2ac.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%2Fmn28ttdjiwtnayjta2ac.png" alt="A release plan doc: a checklist with an item assigned to @api showing Ongoing and one assigned to @web showing Completed and ticked" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: assigning writes a link into the markdown
&lt;/h3&gt;

&lt;p&gt;When you pick &lt;strong&gt;Assign to → api&lt;/strong&gt; on a checklist item, the web app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;creates a task in the &lt;code&gt;api&lt;/code&gt; workspace whose title is the item text and whose body points back to the doc;&lt;/li&gt;
&lt;li&gt;rewrites that line of the markdown so it ends with a link to the task, with the status in the link's title;&lt;/li&gt;
&lt;li&gt;saves the doc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is plain markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt; ] Cursor pagination on /v2/invoices → [@api&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;/workspaces/0kH1tWzfwLx/tasks/0kH1ye6SDz7&lt;/span&gt; &lt;span class="nn"&gt;"notstarted"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's &lt;strong&gt;no separate assignments table&lt;/strong&gt;. The link is the assignment, and the link's title is the cached status. Any agent that reads the doc sees who owns the item and where the task lives, with no extra API call. The renderer turns that link into the status chip you see on the board.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: a status change triggers a server-side rewrite
&lt;/h3&gt;

&lt;p&gt;Task status can change from anywhere: the human in the UI, the agent through its MCP tool, or the supervisor. All of those emit the same internal CRUD event. A subscriber listens for task updates and completions and runs a sync:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the task's path, &lt;code&gt;/workspaces/&amp;lt;ws&amp;gt;/tasks/&amp;lt;task&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Find the account's live docs whose markdown contains that path. It's a plain &lt;code&gt;LIKE '%path%'&lt;/code&gt; query, which is enough because the path is unique.&lt;/li&gt;
&lt;li&gt;For each doc, walk the markdown line by line and rewrite matching items.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The line-matching is two regexes: one recognizes a task-list item, and one recognizes an assignee link at the end of the line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// a task-list item: a list marker, then [ ] or [x]&lt;/span&gt;
&lt;span class="s"&gt;`^[ \t]*(?:[-*+]|\d{1,9}[.)])[ \t]+\[([ xX])\](?:[ \t]|$)`&lt;/span&gt;
&lt;span class="c"&gt;// an assignee: a link to a task, titled with its status&lt;/span&gt;
&lt;span class="s"&gt;`\]\((/workspaces/[0-9A-Za-z]+/tasks/[0-9A-Za-z]+)(?: "[a-z]*")?\)[ \t]*$`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each line that has both and points at this task, the sync writes the new status into the link title, and sets the box to &lt;code&gt;[x]&lt;/code&gt; when the status is &lt;code&gt;completed&lt;/code&gt; (otherwise &lt;code&gt;[ ]&lt;/code&gt;). Lines inside fenced code blocks are skipped, because a &lt;code&gt;- [ ]&lt;/code&gt; there renders as text, not a checkbox.&lt;/p&gt;

&lt;p&gt;So when the &lt;code&gt;api&lt;/code&gt; agent marks its task &lt;code&gt;completed&lt;/code&gt;, the item becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;x] Cursor pagination on /v2/invoices → [@api&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;/workspaces/0kH1tWzfwLx/tasks/0kH1ye6SDz7&lt;/span&gt; &lt;span class="nn"&gt;"completed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the checkbox doesn't just tick on completion. It follows the status both ways: reopen the task and the box clears again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: the write is a sync, not a save
&lt;/h3&gt;

&lt;p&gt;A subtle detail: this write &lt;strong&gt;is not a normal save&lt;/strong&gt;. It updates only the markdown, excerpt, size and checklist counters, and deliberately skips &lt;code&gt;updated_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why it matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The doc keeps its place on the board.&lt;/strong&gt; A task finishing in another workspace shouldn't bump the release plan to the top as if someone edited it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The author stays the author.&lt;/strong&gt; The doc isn't suddenly "updated by api".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The doc's lifetime doesn't change.&lt;/strong&gt; Docs expire a set number of days after their last save (more on that below). A background status sync shouldn't keep a doc alive forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What the sync &lt;strong&gt;does&lt;/strong&gt; change is the doc's &lt;code&gt;version&lt;/code&gt;. The markdown is different now, so any agent still holding the old version has to read the doc again before it writes. The next section explains why that matters.&lt;/p&gt;

&lt;p&gt;The "2 of 6 done" progress is recounted from the markdown with the same task-item regex on every write, so the board, the card and the doc always agree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: open boards update live
&lt;/h3&gt;

&lt;p&gt;After the rewrite, the server publishes a &lt;code&gt;pano.changed&lt;/code&gt; event on the account's event stream. Any open board or doc page reloads that doc. The human watching the release plan sees the chip go from Ongoing to Completed and the box tick, without a refresh.&lt;/p&gt;

&lt;p&gt;One honest constraint: the event bus is in-process, so only the backend instance that handled the task change does the rewrite. That's fine, because the rewrite goes to the shared database, and any instance serves the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do two agents avoid overwriting each other?
&lt;/h2&gt;

&lt;p&gt;A shared doc has the classic lost-update problem. In the first version of the Pano, a save with an id replaced the whole doc. So this could happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;api&lt;/code&gt; agent reads the release plan.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;web&lt;/code&gt; agent reads the same release plan.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api&lt;/code&gt; ticks its item and saves the whole markdown.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;web&lt;/code&gt; adds a line and saves the whole markdown it read in step 2.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;web&lt;/code&gt;'s save silently wipes out &lt;code&gt;api&lt;/code&gt;'s tick. Nobody gets an error, and the doc is just wrong. With several agents and a server-side checklist sync all writing to the same docs, this isn't hypothetical.&lt;/p&gt;

&lt;p&gt;The fix (&lt;a href="https://github.com/agentrq/agentrq/pull/896" rel="noopener noreferrer"&gt;agentrq#896&lt;/a&gt;, merged right after v0.10.0) is &lt;strong&gt;optimistic concurrency&lt;/strong&gt; with a version number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every doc has a version
&lt;/h3&gt;

&lt;p&gt;Each doc has a &lt;code&gt;version&lt;/code&gt;, starting at 1, which goes up on &lt;strong&gt;every&lt;/strong&gt; write: a save, an edit, and the checklist sync described above. &lt;code&gt;read&lt;/code&gt; returns it, and any update (a &lt;code&gt;save&lt;/code&gt; with an id, or an &lt;code&gt;edit&lt;/code&gt;) must send the version it read. Creating a doc needs no version.&lt;/p&gt;

&lt;h3&gt;
  
  
  The check happens in the UPDATE itself
&lt;/h3&gt;

&lt;p&gt;The important detail is &lt;strong&gt;where&lt;/strong&gt; the check runs. Reading the version, comparing it, and then writing would still leave a race window between the compare and the write. Instead, the version is part of the &lt;code&gt;UPDATE&lt;/code&gt;'s &lt;code&gt;WHERE&lt;/code&gt; clause, and the same statement bumps it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;pano_docs&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;   &lt;span class="c1"&gt;-- the version the writer read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If another write got in first, the row's version has already moved, the &lt;code&gt;WHERE&lt;/code&gt; matches nothing, and &lt;strong&gt;zero rows are affected&lt;/strong&gt;. The server then checks whether the doc still exists at all. If it does, the write is a conflict. If it doesn't, it's "not found". Either the whole write lands at version N+1, or nothing changes. The compare and the write can't be split.&lt;/p&gt;

&lt;h3&gt;
  
  
  A conflict tells the agent exactly what to do
&lt;/h3&gt;

&lt;p&gt;A stale write is refused with &lt;strong&gt;HTTP 409&lt;/strong&gt; in the REST API, and the MCP tool returns a message written for an agent to act on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the Pano doc changed since you read it (you sent version 3, it is now 4):
read it again, apply your change to the new content and save again with the new version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A save with no version at all gets a similar message that names the current version. That matters because agents written before versioning existed would otherwise keep overwriting. Now they're told how to fix their call.&lt;/p&gt;

&lt;p&gt;The agent doesn't need special retry logic. It does what the message says: read the doc, redo its change on the new content, and save with the new version. The overwrite becomes an explicit merge, done by the one party that knows what its change meant.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;edit&lt;/code&gt;: change a line without resending the doc
&lt;/h3&gt;

&lt;p&gt;Versioning makes a whole-doc save safe, but resending 256 KiB of markdown to tick one box is still wasteful, and a bigger payload means more chances for an agent to garble something it didn't mean to touch. So there's a new &lt;code&gt;edit&lt;/code&gt; action that takes a list of ops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"payload"&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="s2"&gt;"0kH25QEUKFV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ops"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"oldString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"- [ ] Changelog and docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"newString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"- [x] Changelog and docs"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"append"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- [ ] Announce on the blog"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;append&lt;/code&gt; adds text to the end.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;replace&lt;/code&gt; swaps &lt;code&gt;oldString&lt;/code&gt; for &lt;code&gt;newString&lt;/code&gt;, but &lt;strong&gt;only if &lt;code&gt;oldString&lt;/code&gt; occurs exactly once&lt;/strong&gt;. Zero matches is refused ("not in the markdown"). Two or more is refused with the count and a hint to include more surrounding text. That's the same rule as a good code-edit tool: an ambiguous edit is an error, never a guess.&lt;/li&gt;
&lt;li&gt;The ops apply &lt;strong&gt;in order, all or none&lt;/strong&gt;. If op 2 fails, op 1 isn't saved either.&lt;/li&gt;
&lt;li&gt;The version is checked &lt;strong&gt;before&lt;/strong&gt; the ops run. Against a stale doc, a &lt;code&gt;replace&lt;/code&gt; would fail with a misleading "not found", so the agent is told the real problem: the doc changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result then goes through the same save path, so the size limits, the linked-file rules and the version check all still apply.&lt;/p&gt;

&lt;p&gt;From a shell, the CLI does the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @agentrq/agentrq-ws@latest pano edit &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt; 3 &lt;span class="nt"&gt;--replace&lt;/span&gt; &lt;span class="s2"&gt;"- [ ] Tag"&lt;/span&gt; &lt;span class="nt"&gt;--with&lt;/span&gt; &lt;span class="s2"&gt;"- [x] Tag"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;save&lt;/code&gt; and &lt;code&gt;edit&lt;/code&gt; now print the doc's id &lt;strong&gt;and&lt;/strong&gt; its new version, so a script can chain writes without another read.&lt;/p&gt;

&lt;h3&gt;
  
  
  What humans see on a conflict
&lt;/h3&gt;

&lt;p&gt;The web editor uses the same check. If you're editing a doc and an agent saves it first, your save is refused, &lt;strong&gt;but your draft stays in the editor&lt;/strong&gt;. A banner says the note changed while you were editing and offers to load the latest version, so you can copy your text first. Ticking a box on the board works the same way: on a 409, the doc is reloaded and you're asked to try again, so what you see is what a retry would change.&lt;/p&gt;

&lt;p&gt;The checklist sync bumping the version fits in here too. If the &lt;code&gt;api&lt;/code&gt; task completes while you have the release plan open in the editor, the server's rewrite moves the version, and your stale save is refused instead of un-ticking the item.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do @mentions turn into tasks?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fh6pv2kovizkf5ick0g.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%2F7fh6pv2kovizkf5ick0g.png" alt="An API contract doc with a comment from the web workspace asking for a bigger page size, and the api workspace's answer" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Comments are plain text with an author. Typing &lt;code&gt;@&lt;/code&gt; suggests workspaces. When the comment is posted, every workspace it names by its full name gets a task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;title: &lt;code&gt;Pano: &amp;lt;doc title&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;body: the comment quoted, then an instruction to read the doc with &lt;code&gt;includeComments&lt;/code&gt; and &lt;strong&gt;answer with a comment on the doc&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part is the design. The answer goes back on the doc, not into a private task thread, so the conversation stays where the work is and the human can read it later. Pings are sent about a second apart, because the server allows one task creation per second per account. If one ping fails, the comment is already posted and the others still go out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stops the board from filling up forever?
&lt;/h2&gt;

&lt;p&gt;Every doc has a time to live: &lt;strong&gt;30 days after its last save&lt;/strong&gt; by default. A save can ask for any whole number of days up to a server maximum (365 by default, configurable). An expired doc is hidden right away. An hourly sweep on every backend instance deletes expired docs and their files. If two instances race, the loser just skips the doc.&lt;/p&gt;

&lt;p&gt;This matches how hand-off docs actually behave. A release plan is useful for a few weeks. A postmortem can ask for a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the rules for files?
&lt;/h2&gt;

&lt;p&gt;There are two kinds of files, and the split keeps docs self-consistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linked files&lt;/strong&gt; travel with the markdown. Each must be linked from it by name, as &lt;code&gt;![chart](chart.png)&lt;/code&gt; or &lt;code&gt;[spec](spec.pdf)&lt;/code&gt;. A save that sends a file the markdown doesn't link is &lt;strong&gt;refused&lt;/strong&gt;. A linked file that the markdown stops linking is &lt;strong&gt;deleted at the next save&lt;/strong&gt;. So every linked file is always reachable from the doc's text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attachments&lt;/strong&gt; are added with &lt;code&gt;attach&lt;/code&gt;, need no link, and stay until the doc is deleted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Limits refuse rather than truncate. An agent told its doc is over 256 KiB can decide what to cut. One whose doc was silently cut in half can't.&lt;/p&gt;

&lt;p&gt;For shells and CI, the &lt;code&gt;agentrq-ws&lt;/code&gt; CLI keeps the doc out of the agent's context entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @agentrq/agentrq-ws@latest pano save &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"Release plan"&lt;/span&gt; &lt;span class="nt"&gt;--markdown&lt;/span&gt; @plan.md &lt;span class="nt"&gt;--file&lt;/span&gt; chart.png &lt;span class="nt"&gt;--color&lt;/span&gt; blue
npx &lt;span class="nt"&gt;-y&lt;/span&gt; @agentrq/agentrq-ws@latest pano &lt;span class="nb"&gt;read&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--comments&lt;/span&gt; &lt;span class="nt"&gt;--out&lt;/span&gt; ./pano
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;save&lt;/code&gt; prints only the id. &lt;code&gt;read --out&lt;/code&gt; writes the markdown and its linked files side by side, so relative links still resolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does this unlock?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agents hand work to each other directly.&lt;/strong&gt; The API agent posts the contract and puts the id in its reply. The web agent reads it. No human copy-paste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A plan tracks itself.&lt;/strong&gt; Items assigned to workspaces tick when the work is done, not when someone remembers to update the list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation and coordination together.&lt;/strong&gt; Each workspace keeps its own tasks and memory. The Pano is the one shared surface, used on purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand-offs become visible.&lt;/strong&gt; What agents tell each other is on a page a human can scan, edit or delete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many writers, no lost updates.&lt;/strong&gt; Agents, the checklist sync and humans can all write to the same doc, and a stale write is refused instead of quietly winning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is the Pano a vector store or RAG?&lt;/strong&gt;&lt;br&gt;
No. There's no embedding, no retrieval and no search for agents. An agent reads exactly the docs whose ids it was given.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if someone edits an assigned line by hand?&lt;/strong&gt;&lt;br&gt;
The sync only matches lines that are a task-list item &lt;strong&gt;and&lt;/strong&gt; end with a link to that exact task path. Change the text before the arrow freely. Remove the link, and the item is no longer assigned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a status sync count as an edit?&lt;/strong&gt;&lt;br&gt;
Not for the board. It updates the markdown and checklist counts without touching &lt;code&gt;updated_at&lt;/code&gt;, so the doc keeps its place, author and expiry. It does bump the &lt;code&gt;version&lt;/code&gt;, so a writer holding the old one has to read again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if two agents save the same doc at once?&lt;/strong&gt;&lt;br&gt;
The first write wins and moves the version. The second is refused with a 409 and a message telling the agent to read the doc again, redo its change and save with the new version. Nothing is silently overwritten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not lock the doc while an agent edits it?&lt;/strong&gt;&lt;br&gt;
Agents can stall or crash mid-task, and a lock held by a dead agent blocks everyone. Optimistic concurrency costs nothing when there's no conflict, which is almost always, and when there is one, the writer that knows what its change meant is the one asked to redo it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who can read a doc?&lt;/strong&gt;&lt;br&gt;
Every workspace in the account, plus the human. Comments and writes record the workspace that made them (a fork records its parent).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I self-host it?&lt;/strong&gt;&lt;br&gt;
Yes. AgentRQ is open source. File storage is local by default, or S3 with &lt;code&gt;AGENTRQ_PANO_STORAGE=s3&lt;/code&gt;, and &lt;code&gt;AGENTRQ_PANO_MAX_TTL_DAYS&lt;/code&gt; caps doc lifetime.&lt;/p&gt;




&lt;p&gt;How are you passing context between agents today: shared files, a vector store, or a human in the middle? We'd like to hear what's worked for you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://agentrq.com/blog/pano-shared-markdown-board-for-ai-agents-across-workspaces" rel="noopener noreferrer"&gt;AgentRQ blog&lt;/a&gt;. AgentRQ is an open-source, human-in-the-loop task manager for AI agents: &lt;a href="https://github.com/agentrq/agentrq" rel="noopener noreferrer"&gt;github.com/agentrq/agentrq&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to Enable Claude Code Channels for Team and Enterprise Plans (Step by Step)</title>
      <dc:creator>Mustafa Turan</dc:creator>
      <pubDate>Sat, 10 Oct 2026 19:18:49 +0000</pubDate>
      <link>https://dev.to/agentrq/how-to-enable-claude-code-channels-for-team-and-enterprise-plans-step-by-step-54a8</link>
      <guid>https://dev.to/agentrq/how-to-enable-claude-code-channels-for-team-and-enterprise-plans-step-by-step-54a8</guid>
      <description>&lt;p&gt;You connected Claude Code to a channel, maybe &lt;a href="https://agentrq.com" rel="noopener noreferrer"&gt;AgentRQ&lt;/a&gt;, a Telegram bot or a Discord bot, and sent it a message from your phone. Nothing happened. Claude Code started, the MCP server connected and its tools work, but the message never showed up in the session. A startup warning says to ask your admin.&lt;/p&gt;

&lt;p&gt;If you are on a &lt;strong&gt;claude.ai Team or Enterprise plan&lt;/strong&gt;, that is expected. Channels ship switched &lt;strong&gt;off&lt;/strong&gt; for organizations, and one person has to flip a switch before anyone can use them.&lt;/p&gt;

&lt;p&gt;This guide walks through that switch screen by screen, then covers the managed-settings route and how to limit which channel plugins your team can run.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code Channels are a research preview, and they are &lt;strong&gt;blocked by default on claude.ai Team and Enterprise&lt;/strong&gt; plans.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Owner&lt;/strong&gt; enables them at &lt;strong&gt;Organization settings → Claude Code → Channels&lt;/strong&gt; (&lt;a href="https://claude.ai/admin-settings/claude-code" rel="noopener noreferrer"&gt;claude.ai/admin-settings/claude-code&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;The alternative is to set &lt;code&gt;channelsEnabled: true&lt;/code&gt; in managed settings. Add &lt;code&gt;allowedChannelPlugins&lt;/code&gt; to pick which plugins can register.&lt;/li&gt;
&lt;li&gt;Once Channels are on, each developer still chooses channels per session. Step 4 shows how to set up AgentRQ, Telegram and Discord.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What are Claude Code Channels, and why are they off for my org?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;channel&lt;/strong&gt; is an MCP server that &lt;strong&gt;pushes&lt;/strong&gt; events into a Claude Code session that is already running. Ordinary MCP tools only respond when Claude calls them. A channel can deliver a new task, a reply to a question the agent asked you, a CI failure or a monitoring alert into the session while you are away from the terminal. Claude reads the event, does the work against your real files and can reply through the same channel.&lt;/p&gt;

&lt;p&gt;That makes a channel an inbound path into a developer's machine, and Anthropic leaves it to organization admins to decide whether to allow it. The defaults depend on how you sign in:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Account type&lt;/th&gt;
&lt;th&gt;Channels by default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pro / Max (no organization)&lt;/td&gt;
&lt;td&gt;Available, opt in per session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude.ai &lt;strong&gt;Team / Enterprise&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Blocked until an Owner enables them&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Console (API key)&lt;/td&gt;
&lt;td&gt;Allowed, unless your org deploys managed settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock / Google Cloud / Foundry&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You also need Claude Code &lt;strong&gt;v2.1.80 or later&lt;/strong&gt;. The official channel plugins (Telegram, Discord, iMessage and the fakechat demo) need &lt;a href="https://bun.sh" rel="noopener noreferrer"&gt;Bun&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who can enable Channels?
&lt;/h2&gt;

&lt;p&gt;Only a user with the &lt;strong&gt;Owner&lt;/strong&gt; role on the claude.ai organization. Admins without the Owner role, and regular members, will not be able to change the toggle. If that isn't you, send this post to your Owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Open Organization settings
&lt;/h2&gt;

&lt;p&gt;Sign in to &lt;a href="https://claude.ai" rel="noopener noreferrer"&gt;claude.ai&lt;/a&gt; with your Team or Enterprise account. Click your name in the &lt;strong&gt;bottom-left corner&lt;/strong&gt; to open the account menu, make sure your organization (not your personal account) is selected at the top, and choose &lt;strong&gt;Organization settings&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F413pxzc22eddqjffmbgm.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%2F413pxzc22eddqjffmbgm.png" alt="The claude.ai account menu open in the bottom-left corner, with Organization settings highlighted"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Go to Products → Claude Code and find Channels
&lt;/h2&gt;

&lt;p&gt;In the left sidebar of Organization settings, scroll to the &lt;strong&gt;Products&lt;/strong&gt; group and click &lt;strong&gt;Claude Code&lt;/strong&gt; (1).&lt;/p&gt;

&lt;p&gt;The Claude Code page is long. The quickest way to the setting is your browser's find bar: press &lt;code&gt;Cmd+F&lt;/code&gt; on macOS or &lt;code&gt;Ctrl+F&lt;/code&gt; on Windows and Linux, and type &lt;code&gt;channels&lt;/code&gt; (2). It sits in the &lt;strong&gt;Capabilities&lt;/strong&gt; section, below Dynamic workflows.&lt;/p&gt;

&lt;p&gt;Turn on the &lt;strong&gt;Channels&lt;/strong&gt; toggle (3). It carries a &lt;strong&gt;Preview&lt;/strong&gt; badge, and its description reads: "Allow your team to receive inbound messages from MCP servers in Claude Code sessions."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fepszshustshzvzxc7u7c.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%2Fepszshustshzvzxc7u7c.png" alt="Organization settings, Claude Code page: (1) Products → Claude Code in the sidebar, (2) the browser find bar searching for channels, (3) the Channels Preview toggle switched on"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can skip the menu clicks by opening the page directly: &lt;a href="https://claude.ai/admin-settings/claude-code" rel="noopener noreferrer"&gt;claude.ai/admin-settings/claude-code&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Confirm it says "Set by admin"
&lt;/h2&gt;

&lt;p&gt;Once the toggle is on, the label next to it changes from &lt;strong&gt;Default&lt;/strong&gt; to &lt;strong&gt;Set by admin&lt;/strong&gt;. That label shows the setting is now enforced for the whole organization, and members cannot override it.&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%2Fij9oou9xpe7fwje0e9cg.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%2Fij9oou9xpe7fwje0e9cg.png" alt="The Channels Preview setting switched on, labelled Set by admin"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the whole admin side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Connect a channel (AgentRQ, Telegram or Discord)
&lt;/h2&gt;

&lt;p&gt;Turning the setting on doesn't start anything. Each developer still picks which channel servers their session will trust. Below are three common setups: AgentRQ, plus Anthropic's official Telegram and Discord plugins. Pick one, or run several at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: AgentRQ
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://agentrq.com" rel="noopener noreferrer"&gt;AgentRQ&lt;/a&gt; lets you assign tasks, answer the agent's questions and approve its actions from your phone or desktop while Claude Code keeps running on your machine. You don't run a bot or a server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create a workspace.&lt;/strong&gt; Sign in at &lt;a href="https://app.agentrq.com" rel="noopener noreferrer"&gt;app.agentrq.com&lt;/a&gt;, create a workspace for your project and click &lt;strong&gt;Generate MCP Token&lt;/strong&gt;. The token is shown only once, so copy it. Note the workspace ID too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Add the server to &lt;code&gt;.mcp.json&lt;/code&gt;&lt;/strong&gt; in the project root, where you run &lt;code&gt;claude&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"agentrq-WORKSPACE_ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://WORKSPACE_ID.mcp.agentrq.com/mcp?token=YOUR_MCP_TOKEN"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server key has to include your workspace ID, because the next command refers to the server by that name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Start Claude Code with the server as a channel:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--dangerously-load-development-channels&lt;/span&gt; server:agentrq-WORKSPACE_ID &lt;span class="nt"&gt;--name&lt;/span&gt; my-api-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A server in &lt;code&gt;.mcp.json&lt;/code&gt; alone gives Claude the AgentRQ tools, but it can't push anything into the session. New tasks and your replies only arrive as they happen when the server is also named as a channel.&lt;/p&gt;

&lt;p&gt;AgentRQ is a custom channel. During the research preview, only the plugins on Anthropic's allowlist load through &lt;code&gt;--channels&lt;/code&gt;, so a custom channel like this one loads through &lt;code&gt;--dangerously-load-development-channels&lt;/code&gt;. That flag skips the plugin allowlist after a confirmation prompt. &lt;strong&gt;It does not get around an organization that has Channels turned off&lt;/strong&gt;, which is why the Owner toggle above comes first. &lt;code&gt;--name&lt;/code&gt; just labels the session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Send it work.&lt;/strong&gt; Create a task in the AgentRQ app. It arrives in the running session as a &lt;code&gt;&amp;lt;channel&amp;gt;&lt;/code&gt; event, Claude starts on it, and its progress updates and questions come back to the app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Telegram
&lt;/h3&gt;

&lt;p&gt;The official plugins run on &lt;a href="https://bun.sh" rel="noopener noreferrer"&gt;Bun&lt;/a&gt;, so install that first.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In Telegram, open &lt;a href="https://t.me/BotFather" rel="noopener noreferrer"&gt;@BotFather&lt;/a&gt;, send &lt;code&gt;/newbot&lt;/code&gt;, choose a name and a username ending in &lt;code&gt;bot&lt;/code&gt;, and copy the token it returns.&lt;/li&gt;
&lt;li&gt;Install the plugin and save the token. Run these inside Claude Code:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin install telegram@claude-plugins-official
/telegram:configure &amp;lt;token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Exit Claude Code and restart it with the channel:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--channels&lt;/span&gt; plugin:telegram@claude-plugins-official
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Send any message to your bot. It replies with a pairing code. Approve the code and lock access to your account:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/telegram:access pair &amp;lt;code&amp;gt;
/telegram:access policy allowlist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option C: Discord
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;a href="https://discord.com/developers/applications" rel="noopener noreferrer"&gt;Discord Developer Portal&lt;/a&gt;, create an application. In the &lt;strong&gt;Bot&lt;/strong&gt; section, click &lt;strong&gt;Reset Token&lt;/strong&gt; and copy it. Under &lt;strong&gt;Privileged Gateway Intents&lt;/strong&gt;, turn on &lt;strong&gt;Message Content Intent&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;OAuth2 → URL Generator&lt;/strong&gt;, select the &lt;code&gt;bot&lt;/code&gt; scope with these permissions: View Channels, Send Messages, Send Messages in Threads, Read Message History, Attach Files and Add Reactions. Open the generated URL to add the bot to your server.&lt;/li&gt;
&lt;li&gt;Install the plugin, save the token, then restart with the channel:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin install discord@claude-plugins-official
/discord:configure &amp;lt;token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--channels&lt;/span&gt; plugin:discord@claude-plugins-official
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Send the bot a DM. It replies with a pairing code. Approve it and lock access:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/discord:access pair &amp;lt;code&amp;gt;
/discord:access policy allowlist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run more than one channel in a session, list them all. &lt;code&gt;--channels&lt;/code&gt; takes several plugins separated by spaces, and you can use it together with &lt;code&gt;--dangerously-load-development-channels&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;claude &lt;span class="nt"&gt;--channels&lt;/span&gt; plugin:telegram@claude-plugins-official plugin:discord@claude-plugins-official &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dangerously-load-development-channels&lt;/span&gt; server:agentrq-WORKSPACE_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither flag shows up in &lt;code&gt;claude --help&lt;/code&gt; while Channels are in preview, but both work. If the org setting is still off, every server connects and its tools work, but no channel messages arrive, and a startup warning tells the user to ask an admin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I enable Channels with managed settings instead?
&lt;/h2&gt;

&lt;p&gt;Yes. If you already roll out Claude Code &lt;a href="https://code.claude.com/docs/en/settings" rel="noopener noreferrer"&gt;managed settings&lt;/a&gt;, set the master switch there:&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;"channelsEnabled"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the same switch as the toggle above, and users cannot override it. Console organizations that deploy managed settings need this key too, because deploying managed settings blocks Channels until it is set.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I restrict which channel plugins can run?
&lt;/h2&gt;

&lt;p&gt;By default, any plugin on Anthropic's channel allowlist can register, which means the ones in &lt;a href="https://github.com/anthropics/claude-plugins-official/tree/main/external_plugins" rel="noopener noreferrer"&gt;claude-plugins-official&lt;/a&gt;. On Team and Enterprise you can &lt;strong&gt;replace&lt;/strong&gt; that list with &lt;code&gt;allowedChannelPlugins&lt;/code&gt;. Each entry names a plugin and the marketplace it comes from:&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;"channelsEnabled"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowedChannelPlugins"&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;"marketplace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-plugins-official"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"plugin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"telegram"&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;"marketplace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-plugins-official"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"plugin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"discord"&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;"marketplace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme-corp-plugins"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"plugin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internal-alerts"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few rules worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;allowedChannelPlugins&lt;/code&gt; only applies when &lt;code&gt;channelsEnabled&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If a plugin isn't on your list, Claude Code still starts, but the channel doesn't register. The startup notice says the plugin is not on the organization's approved list.&lt;/li&gt;
&lt;li&gt;Each entry has to match the &lt;strong&gt;plugin name&lt;/strong&gt; as well as the marketplace.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;empty array&lt;/strong&gt; blocks every allowlisted plugin. Servers loaded with &lt;code&gt;--dangerously-load-development-channels&lt;/code&gt;, such as AgentRQ, can still run. To shut Channels off completely, development flag included, leave &lt;code&gt;channelsEnabled&lt;/code&gt; unset.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What should admins think about before turning it on?
&lt;/h2&gt;

&lt;p&gt;Channels are useful because they let outside events reach a session, and that is also the part to plan for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who can reach the session.&lt;/strong&gt; Telegram and Discord keep a sender allowlist that you fill by pairing, and messages from anyone else are dropped silently. Always finish with &lt;code&gt;access policy allowlist&lt;/code&gt;. With AgentRQ, the workspace MCP token is the key. Anyone with the token can connect to the workspace, so keep &lt;code&gt;.mcp.json&lt;/code&gt; out of public repos and generate a new token if one leaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who can approve actions.&lt;/strong&gt; If a channel relays permission prompts, anyone who can answer through it can approve or deny tool use in that session. Only give that access to people you would trust at the keyboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unattended sessions.&lt;/strong&gt; People who pair Channels with &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; should do it only in environments they trust.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need to restart Claude Code after the admin enables Channels?&lt;/strong&gt;&lt;br&gt;
Yes. The setting is checked when a session starts. Exit and relaunch with the channel flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The MCP server connects and its tools work, so why don't messages arrive?&lt;/strong&gt;&lt;br&gt;
That is the exact symptom of Channels being off at the org level. The server still connects, but channel messages are dropped. Check the startup warning, then ask your Owner to check &lt;strong&gt;Organization settings → Claude Code → Channels&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm an Admin but the toggle doesn't change.&lt;/strong&gt;&lt;br&gt;
Enabling Channels requires the &lt;strong&gt;Owner&lt;/strong&gt; role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does enabling Channels connect everyone to AgentRQ, Telegram or Discord automatically?&lt;/strong&gt;&lt;br&gt;
No. It only allows Channels. Each developer still sets up a channel and names it with &lt;code&gt;--channels&lt;/code&gt; or &lt;code&gt;--dangerously-load-development-channels&lt;/code&gt; when they start a session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are Channels available on Bedrock, Google Cloud or Microsoft Foundry?&lt;/strong&gt;&lt;br&gt;
No. They need Anthropic authentication, either a claude.ai login or a Console API key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is this stable?&lt;/strong&gt;&lt;br&gt;
Channels are still a &lt;strong&gt;research preview&lt;/strong&gt;. Anthropic says the flag syntax and the protocol may change, so check the &lt;a href="https://code.claude.com/docs/en/channels#enable-channels-for-your-organization" rel="noopener noreferrer"&gt;official Channels docs&lt;/a&gt; before you script it.&lt;/p&gt;




&lt;p&gt;Has your team turned Channels on yet? What's the first thing you'd hand to an agent you can reach from your phone? Tell us in the comments.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>mcp</category>
      <category>devops</category>
    </item>
    <item>
      <title>Use Claude right inside your Chrome and let it play with WebMCP tools</title>
      <dc:creator>Mustafa Turan</dc:creator>
      <pubDate>Fri, 02 Oct 2026 03:00:01 +0000</pubDate>
      <link>https://dev.to/agentrq/use-claude-right-inside-your-chrome-and-let-it-play-with-webmcp-tools-3k41</link>
      <guid>https://dev.to/agentrq/use-claude-right-inside-your-chrome-and-let-it-play-with-webmcp-tools-3k41</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; WebMCP lets a website offer tools to AI agents, but only an agent inside that same tab can call them. We built a Chrome extension that removes that limit. Share a site with a workspace, and Claude Code (or Codex, or any MCP agent) can call the site's tools from your laptop or from a server in another building. The call runs in your signed-in tab, and anything that writes waits for your approval. The same popup also shows the &lt;strong&gt;live Claude Code terminal&lt;/strong&gt; of every agent on every machine you've connected. Below are the problems we hit building both, and how we solved them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem does WebMCP leave open?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/webmachinelearning/webmcp" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt; is a young browser standard. A page registers tools with &lt;code&gt;document.modelContext&lt;/code&gt;, such as "search the docs" or "create an issue", each with a name, a description and a JSON Schema for its input. An agent can call them without scraping the page or clicking through it.&lt;/p&gt;

&lt;p&gt;There's a catch: the tools live in the page's JavaScript. Only an agent running in that browser, in that tab, can reach them. Your Claude Code session in a terminal can't. Neither can the agent on your build box, or the one a cron job wakes up at 3am.&lt;/p&gt;

&lt;p&gt;Those are the agents doing most of the work. So we asked: what would it take to give a page's tools to an agent that isn't anywhere near the browser?&lt;/p&gt;

&lt;p&gt;Here's where we ended up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Claude Code / Codex / any MCP agent  (laptop, build box, cloud VM)
              │  MCP: listSiteTools · getSiteToolDefinition · callSiteTool
              ▼
         AgentRQ server ── approval in the task, if the tool writes
              │  WebSocket, opened by the browser
              ▼
   Extension service worker ── picks the right tab
              │  chrome.runtime messages
              ▼
   Bridge (isolated world) ⇄ Observer (page's world) ── tool.execute()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4a9e7p79iskhbqb3n25u.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%2F4a9e7p79iskhbqb3n25u.png" alt="The AgentRQ popup over agentrq.com: the icon shows a green 3, and a strip offers to share the site's 3 WebMCP tools with a workspace"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The WebMCP bridge
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: How do you see a page's tools without becoming a tool provider?
&lt;/h3&gt;

&lt;p&gt;The obvious approach is to inject a &lt;code&gt;modelContext&lt;/code&gt; and see what the page puts in it. We didn't, because then the extension would decide which pages get WebMCP and how it behaves. A page that feature-detects WebMCP would detect us instead of Chrome.&lt;/p&gt;

&lt;p&gt;Instead, a small observer script runs in the page's own JavaScript world at &lt;code&gt;document_start&lt;/code&gt;, in the top frame only. It wraps the browser's native &lt;code&gt;registerTool&lt;/code&gt; so that it still calls through to Chrome, and it records what was registered. If the browser has no &lt;code&gt;modelContext&lt;/code&gt;, the observer does nothing. It never adds one.&lt;/p&gt;

&lt;p&gt;Three edge cases turned out to matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools can be withdrawn.&lt;/strong&gt; &lt;code&gt;registerTool&lt;/code&gt; takes an &lt;code&gt;AbortSignal&lt;/code&gt;. When a page aborts a tool, any calls still running on it fail with a clear error instead of hanging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-registration wins.&lt;/strong&gt; If a page registers the same name twice, the newer one replaces the older one, and an abort on the stale one doesn't remove the new one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptors must survive JSON.&lt;/strong&gt; A tool whose description or schema can't be serialized can never be described to a remote agent, so we drop it at the door.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Challenge 2: How do you talk to page code without the page listening in?
&lt;/h3&gt;

&lt;p&gt;Content scripts come in two worlds. The &lt;strong&gt;main world&lt;/strong&gt; shares JavaScript with the page, so it can wrap &lt;code&gt;registerTool&lt;/code&gt;, but it can't use extension APIs. The &lt;strong&gt;isolated world&lt;/strong&gt; can use &lt;code&gt;chrome.runtime&lt;/code&gt;, but it can't see the page's objects. So the design needs two scripts, the observer and a bridge, and they have to talk over the DOM, which the page can also use.&lt;/p&gt;

&lt;p&gt;A fixed event name like &lt;code&gt;agentrq-call&lt;/code&gt; would let any page listen to every call or forge results. Here's what we did instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The bridge (isolated world) generates a random UUID and drops it on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; as a data attribute.&lt;/li&gt;
&lt;li&gt;The observer (main world) runs next. It &lt;strong&gt;reads the nonce and deletes the attribute&lt;/strong&gt; before any page script runs.&lt;/li&gt;
&lt;li&gt;Every message travels as a &lt;code&gt;CustomEvent&lt;/code&gt; whose &lt;strong&gt;event type is the nonce&lt;/strong&gt;. The page doesn't know the event name, so it can't subscribe and can't forge.&lt;/li&gt;
&lt;li&gt;The observer saves &lt;code&gt;dispatchEvent&lt;/code&gt;, &lt;code&gt;CustomEvent&lt;/code&gt; and &lt;code&gt;JSON&lt;/code&gt; at startup, so a page script that patches them later can't intercept anything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; this depends on the bridge running before the observer. Chrome runs &lt;code&gt;document_start&lt;/code&gt; scripts in order of their &lt;strong&gt;registration IDs&lt;/strong&gt;, not the order you register them in. The bridge's ID has to sort first alphabetically. We learned that the hard way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 3: How does a server reach a browser behind NAT?
&lt;/h3&gt;

&lt;p&gt;It can't, so the browser dials out. The extension's Manifest V3 service worker opens a WebSocket to the AgentRQ server. That brought three MV3 problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A WebSocket from a service worker carries no cookies.&lt;/strong&gt; The worker first asks for a one-minute &lt;strong&gt;ticket&lt;/strong&gt; using your normal sign-in cookie (refreshing the short-lived access token once if needed), then connects with that ticket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome kills idle service workers.&lt;/strong&gt; An open socket keeps the worker alive only while traffic flows, and &lt;strong&gt;pings from the server don't count&lt;/strong&gt;. So the worker sends its own ping every 20 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No socket unless it's needed.&lt;/strong&gt; The socket is open only while at least one site is shared. When it drops, it reconnects with backoff from 1s up to 30s.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Challenge 4: Which tab runs the call?
&lt;/h3&gt;

&lt;p&gt;You might have three tabs of the same site open, or none. The worker tracks every tab that has announced tools and sends each call to the &lt;strong&gt;most recently used&lt;/strong&gt; tab of that origin that offers that tool. If you've closed them all, it &lt;strong&gt;reopens the site in a background tab&lt;/strong&gt; and waits for the page to register the tool again.&lt;/p&gt;

&lt;p&gt;Navigation was the messiest part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A page unloading mid-call&lt;/strong&gt; fails only the calls sent to &lt;em&gt;that document&lt;/em&gt;. After a cross-site navigation, the new page may have announced its tools before the old page's unload event arrives, so we track calls by document, not by tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The back/forward cache&lt;/strong&gt; restores a page without re-running its scripts, so the observer never announces again. The bridge keeps the page's last tool list and sends it again on a persisted &lt;code&gt;pageshow&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A navigate tool&lt;/strong&gt; (one that moves the page somewhere) sends its result before the page unloads. Results and "page gone" go through the same channel in order, so the result arrives first instead of being reported as a failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Challenge 5: How do you keep remote tools from eating the agent's context window?
&lt;/h3&gt;

&lt;p&gt;If you send every schema of every tool on every shared site with each listing, the agent pays for it in tokens on every call. So the MCP surface has three steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;listSiteTools&lt;/code&gt; returns &lt;strong&gt;only names and descriptions&lt;/strong&gt;, plus whether each site is online.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getSiteToolDefinition&lt;/code&gt; returns one tool's full input schema when the agent actually wants to use it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;callSiteTool&lt;/code&gt; runs it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server &lt;strong&gt;validates arguments against the page's own JSON Schema&lt;/strong&gt; before anything reaches your browser, so a malformed call fails fast with a message the agent can fix. Calls have a 60-second deadline and results are capped at 256 KiB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 6: How do you let a remote agent act as you without handing it your credentials?
&lt;/h3&gt;

&lt;p&gt;This is the part we think matters most. The call runs &lt;strong&gt;in your signed-in Chrome, with your session on that site&lt;/strong&gt;. The agent never sees a password, a cookie or an API token. To the site, it's just you, in your tab, using a tool the site chose to offer.&lt;/p&gt;

&lt;p&gt;That makes approval essential:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tools the site annotates as &lt;strong&gt;read-only&lt;/strong&gt; (a search, say) just run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything else asks first.&lt;/strong&gt; The request appears in the task the agent is working on, showing the tool and the exact arguments as pretty-printed JSON. You choose &lt;strong&gt;Allow once&lt;/strong&gt;, &lt;strong&gt;Always allow &amp;lt;tool&amp;gt; on this site&lt;/strong&gt;, or &lt;strong&gt;Deny&lt;/strong&gt;. "Always allow" is remembered per site and per tool.&lt;/li&gt;
&lt;li&gt;The agent has to pass the &lt;code&gt;taskId&lt;/code&gt; it's working on, so every write is tied to a piece of work you can see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only a human can share a site&lt;/strong&gt;, from the popup, one site at a time. An agent can't add one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detection is off by default.&lt;/strong&gt; Until you turn it on, the extension doesn't even look. Once it's on, nothing leaves the browser until you click Share.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also put effort into the failure messages, because the agent is the one reading them. If Chrome is closed, the agent doesn't get a vague "connection refused". It's told the human's Chrome isn't connected and to ask them to open it. If a site doesn't have the tool it asked for, the error lists the tools it does have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Claude Code's terminal, inside the extension
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3auhaqxsaslljtpqknet.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%2F3auhaqxsaslljtpqknet.png" alt="The live Claude Code terminal of an agent on a connected machine, shown in AgentRQ"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the toolbar icon (or press Alt+Shift+A), open an agent, and you get its &lt;strong&gt;real, live terminal&lt;/strong&gt;, not a log or a summary. You can watch it, scroll back, or type into it and take over. It works the same for the agent on your laptop, the one on the box under your desk and the one on a cloud VM, without SSH or a &lt;code&gt;tmux attach&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 7: Why not just bundle the web app into the extension?
&lt;/h3&gt;

&lt;p&gt;Code bundled into an extension runs on the extension's own &lt;code&gt;chrome-extension://&lt;/code&gt; origin, and your sign-in cookie never goes there. You'd be signed out no matter what. So the popup &lt;strong&gt;frames the server's own web app&lt;/strong&gt; instead. Everything the web app can do works in the popup, with the same session as your tabs.&lt;/p&gt;

&lt;p&gt;Two browser limits shaped the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A framed page gets its cookie only if the extension has host permission&lt;/strong&gt; for that site. That's why the extension asks to read app.agentrq.com (or your self-hosted server, which is asked for when you save it and given back when you switch away).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google and GitHub refuse to show sign-in inside a frame.&lt;/strong&gt; A signed-out popup opens sign-in in a tab, and once you're signed in there, the popup is too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chrome caps popups at 800×600 and closes them when you click away, so the popup is phone-sized (440×600). &lt;strong&gt;Full size&lt;/strong&gt; (Alt+Shift+F) opens the same app in a tab.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 8: How do you replay a terminal that redraws itself 100 times a second?
&lt;/h3&gt;

&lt;p&gt;The terminal comes from a small daemon on each connected machine. It runs the agent in a pseudo-terminal (ConPTY on Windows, openpty everywhere else) and &lt;strong&gt;dials out&lt;/strong&gt; to the server. There are no inbound ports to open.&lt;/p&gt;

&lt;p&gt;The first instinct is to keep a ring buffer of output and replay it when someone opens the terminal. That fails with Claude Code, because a spinner or progress bar isn't much output. It's &lt;strong&gt;one line rewritten constantly&lt;/strong&gt;. 256 KiB of ring buffer becomes 256 KiB of the same line, and the history you cared about is gone. Replaying it in a fresh terminal shows a thousand frames of how the screen got there instead of what it shows now.&lt;/p&gt;

&lt;p&gt;So the daemon keeps &lt;strong&gt;its own terminal emulator&lt;/strong&gt; for each session and feeds it every byte, whether or not anyone is watching. When you open the terminal, it sends a &lt;strong&gt;redraw of the current screen&lt;/strong&gt;, the same thing tmux does when a second client attaches. A progress bar shows up looking like a progress bar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 9: How do you handle backpressure without corrupting the stream?
&lt;/h3&gt;

&lt;p&gt;"Drop the oldest bytes" is destructive for a terminal. The stream is stateful: drop an &lt;code&gt;ESC[0m&lt;/code&gt; and the colors stay wrong for the rest of the session, and drop half an escape sequence and the emulator parses garbage. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Output is &lt;strong&gt;coalesced in 25 ms windows&lt;/strong&gt;, which is about one frame. A spinner redrawing 100 times a second becomes roughly 40 sends with no visible difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No byte is ever dropped.&lt;/strong&gt; If the connection can't keep up, the pending batch is thrown away as a whole and replaced with a redraw of the screen. The viewer misses some in-between frames but always sees a correct screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Challenge 10: Why is input never batched?
&lt;/h3&gt;

&lt;p&gt;It's tempting to batch keystrokes the way output is batched. But that would add latency to every keypress, and it would &lt;strong&gt;merge a deliberate Esc with the next key into an escape sequence nobody typed&lt;/strong&gt;. Esc is how you interrupt Claude Code, so that can't happen.&lt;/p&gt;

&lt;p&gt;Every keystroke is sent the moment it's typed, as the exact bytes xterm.js produced: &lt;code&gt;0x1b&lt;/code&gt; for Esc, &lt;code&gt;\r&lt;/code&gt; for Enter, &lt;code&gt;ESC [ A&lt;/code&gt; for the up arrow. There is no list of "special keys". Any such list would be wrong for the next key combination someone needs, while a transparent byte pipe handles all of them.&lt;/p&gt;

&lt;p&gt;One rule here is about security. &lt;strong&gt;The browser doesn't choose which session it types into.&lt;/strong&gt; The server decides which session an attached viewer may drive and overwrites whatever session ID arrives. Otherwise, changing a number would let you type into someone else's agent.&lt;/p&gt;

&lt;p&gt;That rule came from a bug. The first version took the session ID from the caller and converted it with &lt;code&gt;BigInt&lt;/code&gt;, which throws on a base62 string. &lt;strong&gt;Every keystroke threw while output kept arriving&lt;/strong&gt;, so the terminal looked like it was ignoring the keyboard. The unit tests passed numeric IDs and never caught it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 11: Why did terminals sometimes go blank?
&lt;/h3&gt;

&lt;p&gt;Claude Code's interface is mostly box drawing. The DOM renderer asks the font for those glyphs, and the font we ship doesn't include them, so we use xterm's &lt;strong&gt;WebGL renderer&lt;/strong&gt;, which draws box characters itself. WebGL has two failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It fails late.&lt;/strong&gt; On VMs, GPU-less Chromium and machines with blocked drivers, it fails inside &lt;code&gt;activate()&lt;/code&gt;, not in the constructor. So each renderer is tried by actually loading it, and if one throws we move to the next. The last fallback is the plain DOM renderer, because a terminal that renders imperfectly beats one that renders nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can lose its GPU context at any time&lt;/strong&gt;, through a driver reset, a laptop switching GPUs, or too many contexts in other tabs. Afterward it never draws again, and a blank terminal looks exactly like an agent that stopped. So we rebuild the renderer once. If the context is lost a second time, we drop to the next renderer and stay there, rather than burning GPU contexts in a loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What did we trade away?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your Chrome has to be open.&lt;/strong&gt; Calls run in your browser, so a closed laptop means the site is offline. We chose that over storing your credentials on a server. The agent gets a clear "offline" error rather than a hang.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It only works with sites that ship WebMCP.&lt;/strong&gt; It's an early standard and the list is short. agentrq.com offers three tools and the AgentRQ app exposes its own interface, so there's something to try today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approvals add a human in the loop.&lt;/strong&gt; That's by design for writes, and "Always allow" per tool keeps it from getting in the way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The popup is small.&lt;/strong&gt; Chrome sets that limit. Full size is one shortcut away.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which agents can call shared sites?&lt;/strong&gt; Any agent connected to the workspace over MCP, including Claude Code, Codex and Antigravity. There's nothing to install on the agent's side and no token to paste.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the extension read every page I visit?&lt;/strong&gt; Only with detection on, and even then it only checks whether the top frame registers WebMCP tools. Nothing is sent until you share a site. After that, only that site's tool list, its last address and the results of your agents' calls go to your AgentRQ server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I self-host?&lt;/strong&gt; Yes. Set your server's address in Options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it open source?&lt;/strong&gt; Yes. The extension, the daemon and the server are all in the &lt;a href="https://github.com/agentrq/agentrq" rel="noopener noreferrer"&gt;agentrq repository&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Install &lt;a href="https://chromewebstore.google.com/detail/agentrq/iadkenmjgjoilnioldgchgjpdnbaghbj" rel="noopener noreferrer"&gt;AgentRQ for Chrome&lt;/a&gt;, turn on detection, open agentrq.com, share it with a workspace, and ask Claude Code to search the docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over to you:&lt;/strong&gt; if your agents could call any site you're signed into, through tools the site chose to offer and with you approving every write, which site would you hand them first? And if you've built on WebMCP or MV3 service workers, which of these problems did you hit differently? Tell us in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://agentrq.com/blog/agentrq-chrome-extension-webmcp-bridge-for-local-and-remote-agents" rel="noopener noreferrer"&gt;AgentRQ blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>antigravity</category>
      <category>devtools</category>
      <category>webmcp</category>
    </item>
    <item>
      <title>Stop Overwriting Your AI Agent's Task Status: Why We Keep an Immutable State History</title>
      <dc:creator>Mustafa Turan</dc:creator>
      <pubDate>Wed, 30 Sep 2026 01:33:27 +0000</pubDate>
      <link>https://dev.to/agentrq/stop-overwriting-your-ai-agents-task-status-why-we-keep-an-immutable-state-history-159i</link>
      <guid>https://dev.to/agentrq/stop-overwriting-your-ai-agents-task-status-why-we-keep-an-immutable-state-history-159i</guid>
      <description>&lt;p&gt;An agent picks up a task, works on it, marks it &lt;code&gt;completed&lt;/code&gt;. You look at it the next morning.&lt;/p&gt;

&lt;p&gt;How long did it take? How much of that was the agent working, and how much was it waiting for you to click &lt;strong&gt;Allow&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;For a long time &lt;a href="https://agentrq.com" rel="noopener noreferrer"&gt;AgentRQ&lt;/a&gt; (an open-source task board and MCP server for Claude Code and other agents) couldn't tell you. A task had one status field, and every change overwrote it. &lt;code&gt;ongoing → blocked → ongoing → completed&lt;/code&gt;, and each write erased the one before. The end state was right. Everything in between was gone, and you can't get a duration out of a snapshot.&lt;/p&gt;

&lt;p&gt;We fixed that in &lt;a href="https://github.com/agentrq/agentrq/pull/756" rel="noopener noreferrer"&gt;agentrq/agentrq#756&lt;/a&gt;. This post isn't a code walkthrough. It covers the design decisions behind it, because I think they apply to anyone building agent tooling: why the history is &lt;strong&gt;immutable&lt;/strong&gt;, why the states are a &lt;strong&gt;fixed enum&lt;/strong&gt;, why there is exactly &lt;strong&gt;one writer&lt;/strong&gt;, and why the most useful state in the timeline is one the task can never actually be in, and how it all rolls up into a p50 dashboard.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Keep the current status for everything that asks "what is this task now?", and add an &lt;strong&gt;immutable, append-only history&lt;/strong&gt; beside it for "how did it get here?"&lt;/li&gt;
&lt;li&gt;Immutability is what makes the numbers trustworthy: nobody, not the agent, not a person, not a bug, can rewrite how long something took.&lt;/li&gt;
&lt;li&gt;States are a &lt;strong&gt;closed enum that only ever grows at the end&lt;/strong&gt;, so a row written today means the same thing in five years.&lt;/li&gt;
&lt;li&gt;"Waiting on the human" is a &lt;strong&gt;derived, history-only state&lt;/strong&gt;. It never becomes the task's status, so nothing else in the system behaves differently.&lt;/li&gt;
&lt;li&gt;The same history rolls up into a &lt;strong&gt;stats dashboard&lt;/strong&gt;: p50, min and max of start to close, worked, blocked and needs input, per workspace and across the account.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why is overwriting a status the wrong model for agent work?
&lt;/h2&gt;

&lt;p&gt;Because the interesting question about agent work is almost never "is it done?" It's "where did the time go?"&lt;/p&gt;

&lt;p&gt;With a human ticket, you can ask the assignee. With an agent, the only record is what the system kept. If the system kept one mutable field, you have a photograph, not a film.&lt;/p&gt;

&lt;p&gt;It's the event-sourcing lesson in miniature: &lt;strong&gt;the current state is a projection; the transitions are the data.&lt;/strong&gt; Keep the transitions and you can always work out the state. Keep only the state and the transitions are lost for good.&lt;/p&gt;

&lt;p&gt;We didn't go full event sourcing, though. The task's status stays the source of truth for "what is it now", because a lot of the system reads it: the work queue agents pull from, filters, counts, and the MCP tools agents call. We didn't want to touch any of that. The history sits beside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does "immutable history" mean here?
&lt;/h2&gt;

&lt;p&gt;Every status change appends one record: the state the task came from, the state it went to, when, and which agent (and model) made the change. That's it. A record is never edited after it's written, and nothing is ever inserted into the past.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;not started   09:00  ─┐ 6m
ongoing       09:06  ─┤ 24m
needs input   09:30  ─┤ 8m
ongoing       09:38  ─┤ 31m
blocked       10:09  ─┤ 38m
ongoing       10:47  ─┤ 19m
completed     11:06  ─┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A state lasts from its record to the next one. The state the task is in right now lasts until "now", so it keeps counting.&lt;/p&gt;

&lt;p&gt;Two honest caveats about "immutable": if you move a task to another workspace, its history moves with it, and if you delete a task, its history goes too. What never changes is the content of a record: which states, and when.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does immutability matter so much for agents?
&lt;/h2&gt;

&lt;p&gt;Three reasons, in order of how much they bit us.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The numbers have to be trustworthy, or they're useless.&lt;/strong&gt; The whole point of the timeline is to answer questions like "is this class of work worth handing to an agent?" and "am I the bottleneck?". If records could be edited, every number derived from them would be an opinion. Append-only means the only way to change the story is to add to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Agents write status too, and they're not always right.&lt;/strong&gt; An agent might mark a task &lt;code&gt;completed&lt;/code&gt;, then get reopened by a person, then complete it again. With an overwritten field, the final &lt;code&gt;completed&lt;/code&gt; is all you see. With an immutable history, the false finish is on the record, with its time. That's exactly the kind of thing you want to spot when you're tuning a prompt or a workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It makes concurrent writers boring.&lt;/strong&gt; An agent and a person can move the same task at the same moment: the agent marks it &lt;code&gt;completed&lt;/code&gt; while you click &lt;strong&gt;Reject&lt;/strong&gt;. With an append-only log, there's nothing to merge. Both changes are recorded in the order they happened, and each record says where it came from. (We do lock the task while recording, so the second change sees the first one's result as its starting point.)&lt;/p&gt;

&lt;p&gt;There's a fourth, quieter benefit: the conversation got cleaner. Every status change used to drop a "Status updated to …" line into the task thread, which buried the messages that mattered. With the history in its own place, those lines are gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are the states an enum instead of free-form strings?
&lt;/h2&gt;

&lt;p&gt;This looks like a small decision. In an immutable log it's a big one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A record written today has to mean the same thing forever.&lt;/strong&gt; Free-form strings drift. Someone writes &lt;code&gt;complete&lt;/code&gt; instead of &lt;code&gt;completed&lt;/code&gt;, a state gets renamed, a new client sends &lt;code&gt;in_progress&lt;/code&gt;. In a mutable column you can fix that with a migration. In an append-only history, "fixing" old records is exactly what you promised never to do. So the set of states is &lt;strong&gt;closed&lt;/strong&gt;: not started, ongoing, blocked, needs input, completed, rejected, and scheduled (cron). Nothing outside that set can be recorded as itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The enum only ever grows at the end.&lt;/strong&gt; Each state is stored as a small number, and new states are appended after the existing ones, never inserted in between or reordered. Reordering would silently change the meaning of every record already written, so it's the one rule that can't be broken. "Needs input" was added exactly this way, as the last value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's an explicit "none" state.&lt;/strong&gt; When a task is created, the first record goes from "none" to its first status. That gives the first segment a start time, and it gives any unrecognised status somewhere safe to land instead of failing the write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Numbers inside, names outside.&lt;/strong&gt; Storage uses the compact number, because this history grows with every status change on every task and the workspace stats pages aggregate over it. Everything outside the backend, including the API, the UI and the agents, sees the readable names. The mapping lives in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is there only one writer?
&lt;/h2&gt;

&lt;p&gt;Because a history that some code paths forget to write is worse than no history: it looks complete and isn't.&lt;/p&gt;

&lt;p&gt;Status changes come from everywhere: the MCP tools agents call, the web app, the CLI, scheduled tasks, events that create tasks in other workspaces. Instead of asking each of those to remember to record a transition, the record is written in &lt;strong&gt;one place&lt;/strong&gt;, the storage layer's task save, and &lt;strong&gt;in the same transaction&lt;/strong&gt; as the status change itself.&lt;/p&gt;

&lt;p&gt;That gives two guarantees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No status change without a record.&lt;/strong&gt; If recording fails, the status change rolls back. You never see a task that says &lt;code&gt;completed&lt;/code&gt; with no trace of how it got there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No record without a status change.&lt;/strong&gt; Saving a task for any other reason (a new title, a new description) adds nothing to the history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything above the storage layer sets the status and saves, as it always did. It doesn't know the history exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is "needs input" a state the task can never be in?
&lt;/h2&gt;

&lt;p&gt;This is the part I'd steal if I were building something similar.&lt;/p&gt;

&lt;p&gt;When an agent asks the person something through AgentRQ (a permission request before running a command, or a form question), the task is waiting &lt;strong&gt;on the human&lt;/strong&gt;. That's a different kind of wait from &lt;code&gt;blocked&lt;/code&gt;, where the agent hit a wall and said so. They call for opposite fixes, so they deserve separate numbers.&lt;/p&gt;

&lt;p&gt;The obvious implementation is a new status. We didn't do that. Agents read and write the status, the work queue hands out tasks based on it, and filters and counters group by it. A new value would change behaviour in all of those places: an agent looking up its ongoing tasks could suddenly miss the one it is in the middle of.&lt;/p&gt;

&lt;p&gt;So "needs input" is &lt;strong&gt;derived, and it only exists in the history&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;While any question in the task's thread is still waiting for an answer, the history records the task as &lt;strong&gt;needs input&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;When the last one is answered, the history moves it back to whatever its real status is.&lt;/li&gt;
&lt;li&gt;The task's own status stays &lt;code&gt;ongoing&lt;/code&gt; the whole time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few rules made it correct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It counts open questions, it doesn't toggle.&lt;/strong&gt; An agent can have two permission requests open at once. Answering the first must not end the wait.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only real questions count.&lt;/strong&gt; An agent's plan has to-do items that are "pending" too, but a to-do isn't a question to anyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closed means closed.&lt;/strong&gt; A question arriving on a completed task doesn't reopen its history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the status never changes, nothing that reads it behaves any differently. Only the timeline and the totals know.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it look like?
&lt;/h2&gt;

&lt;p&gt;Under each task's description there's now a &lt;a href="https://agentrq.com/features/task-timeline" rel="noopener noreferrer"&gt;task timeline&lt;/a&gt;: one dot per state, with the time spent in each on the line between two dots, and the totals above.&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%2F822wr0t1zz1ibylc9ufe.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%2F822wr0t1zz1ibylc9ufe.png" alt="A completed AgentRQ task with its timeline under the description: not started 6m, ongoing 24m, needs input 8m, ongoing 31m, blocked 38m, ongoing 19m, completed. The totals above read worked 1h 14m, blocked 38m, needs input 8m, start to close 2h" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Value&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;strong&gt;Worked&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1h 14m&lt;/td&gt;
&lt;td&gt;All the time the agent was &lt;code&gt;ongoing&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Blocked&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;38m&lt;/td&gt;
&lt;td&gt;All the time the agent said it couldn't go on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Needs input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8m&lt;/td&gt;
&lt;td&gt;All the time a question to the person was open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Start to close&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2h&lt;/td&gt;
&lt;td&gt;From the &lt;strong&gt;first&lt;/strong&gt; &lt;code&gt;ongoing&lt;/code&gt; to completed or rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start to close begins at the first &lt;code&gt;ongoing&lt;/code&gt;, not at creation, on purpose. A task that sat in the queue overnight shouldn't look like an agent that took twelve hours.&lt;/p&gt;

&lt;p&gt;An open task keeps ticking. Here the agent is waiting on a permission prompt, so the last dot is yellow and counting, and there's no start to close yet:&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%2Fd37ay0wf0gy49h57f3xm.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%2Fd37ay0wf0gy49h57f3xm.png" alt="An ongoing task in dark mode, waiting on a permission request. The timeline ends on a pulsing yellow " width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A small consistency detail: the page works out the totals from the same segments it draws, instead of showing the server's numbers next to a bar it animates itself. Otherwise the live counter and the bar would drift apart every second. The server's totals are still in the API for scripts, agents and the stats pages.&lt;/p&gt;

&lt;p&gt;On a phone it collapses to one line, the dots and the total:&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%2Fjotzo5we9l1k1u8438ps.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%2Fjotzo5we9l1k1u8438ps.png" alt="Two phone screenshots side by side. Left, light mode: a single-line timeline ending on a yellow needs-input dot at 4m, total 45m, above a permission request. Right, dark mode: a completed task's single-line timeline, total 2h" width="800" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How does the dashboard tell the story across every task?
&lt;/h2&gt;

&lt;p&gt;One task's timeline tells you what happened to that task. It doesn't tell you whether that was normal. Is two hours usual for this workspace? Did this week go faster than last? Do agents sit blocked longer in one workspace than another?&lt;/p&gt;

&lt;p&gt;So the history feeds a second view: a &lt;strong&gt;Task Latency&lt;/strong&gt; panel on each workspace's stats page and on the account stats page. It charts the same four totals, in minutes, over every task closed in the range you pick, with a toggle between &lt;strong&gt;P50&lt;/strong&gt; (the typical task), &lt;strong&gt;Min&lt;/strong&gt; (the quickest) and &lt;strong&gt;Max&lt;/strong&gt; (the slowest).&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%2Fzhdpxzqfm1sn7oijvhyy.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%2Fzhdpxzqfm1sn7oijvhyy.png" alt="The Task Latency panel on a workspace stats page in light mode. Four summary cards on the left read start to close 108 min, worked 66 min, blocked 19 min and needs input 7 min, over 42 tasks. The chart on the right has one line per value from Sep 21 to Sep 27, and the tooltip for Sep 24 reads 9 closed: start to close 150 min, worked 94 min, blocked 46 min, needs input 17 min" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read the cards like a story. This workspace closed 42 tasks in the week. At p50, a task took &lt;strong&gt;108 minutes&lt;/strong&gt; start to close, of which &lt;strong&gt;66&lt;/strong&gt; were work, &lt;strong&gt;19&lt;/strong&gt; were blocked and &lt;strong&gt;7&lt;/strong&gt; were waiting on a person. (Each line is its own median, so they don't add up exactly, and they aren't meant to.) Hover over Sep 24 and the typical task took 150 minutes, well above the week's figure. That's the day to go and look at.&lt;/p&gt;

&lt;p&gt;The account page shows the same panel across every workspace:&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%2Fr6l8cf7eb2qsuixlex7c.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%2Fr6l8cf7eb2qsuixlex7c.png" alt="The same Task Latency panel on the account stats page in dark mode, covering 126 closed tasks: start to close 146 min, worked 89 min, blocked 26 min, needs input 9.5 min. The tooltip for Sep 24 reads 27 closed: start to close 203 min, worked 127 min, blocked 62 min, needs input 23 min" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Across the account the typical task took 146 minutes and was blocked for 26 of them. The workspace above beats both, so something else is pulling the account figure up, and you know where to look next.&lt;/p&gt;

&lt;p&gt;The design decisions behind the numbers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why p50 and not the average?&lt;/strong&gt; Agent task durations have a long tail. One task that sat blocked over a weekend turns a 30-minute average into a 3-hour one, and then the average describes no task you actually ran. The median is the task you'd typically get. Min and max sit next to it on purpose: max finds the one that dragged, and min shows how fast the work goes when nothing gets in the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why an estimated p50?&lt;/strong&gt; Stats are rolled up per hour, per day and per month, so a three-month chart doesn't re-read every task. But medians don't add up. You can't combine two hours' medians into a day's median. So each rollup keeps a small &lt;strong&gt;histogram&lt;/strong&gt; of durations with fixed, log-spaced buckets: under a minute, 1–2 minutes, 2–5 minutes, and so on through hours and days, up to over 30 days. Histograms merge by adding counts. The p50 is read back out of the merged histogram, interpolated inside the bucket where the median falls, and clamped between the exact min and max, which merge perfectly. It's an estimate, and it's exact when a bucket holds a single task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The histogram buckets are immutable too.&lt;/strong&gt; Same lesson as the state enum: counts are stored by position, so the bucket edges can never be changed or reordered. A finer histogram would be a new field, not an edit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One arithmetic, two views.&lt;/strong&gt; The task page and the dashboard compute durations with the same function over the same history, so a task's timeline and its contribution to the chart can never disagree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every close counts once.&lt;/strong&gt; When a task closes, its totals are recorded in the same transaction that closes it. If it's reopened and closed again, its totals are recalculated over its whole history, but a period that was already rolled up isn't rewritten. Each close counts once, in the period it happened in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero is data; nothing is a gap.&lt;/strong&gt; A task that was never blocked counts as 0 minutes blocked, so the blocked line shows how much blocking a typical task really sees, not just the unlucky ones. But a day with &lt;strong&gt;no&lt;/strong&gt; closed tasks is left as a gap in the line, not drawn as zero, so a quiet day never looks like a fast one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The resolution follows the range.&lt;/strong&gt; A day or two plots by the hour, up to about three months by the day, and anything longer by the month. All in UTC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents can read it too.&lt;/strong&gt; The same figures are available to an agent, so you can ask one to watch for a workspace whose blocked time is creeping up, or to report how long its own work took this week.&lt;/p&gt;

&lt;p&gt;It shipped as a follow-up, in &lt;a href="https://github.com/agentrq/agentrq/pull/758" rel="noopener noreferrer"&gt;agentrq/agentrq#758&lt;/a&gt;. The full tour is in &lt;a href="https://agentrq.com/blog/task-latency-statistics-for-ai-agent-workspaces" rel="noopener noreferrer"&gt;Task Latency: How Long Your Agents' Tasks Take&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it unlock?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"How long did that take?" finally has an answer.&lt;/strong&gt; Start to close per task is the number you want when you decide whether a class of work is worth handing to an agent at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent time vs. your time.&lt;/strong&gt; A two-hour task might be two hours of work, or twenty minutes of work plus an hour and forty minutes waiting on a permission prompt. The first means the task is big. The second means the human is the bottleneck, and the fix is a broader permission rule, a better prompt, or handing it off at a different time of day. Worked and needs input are separate numbers, so you know which one you have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocked stops being invisible.&lt;/strong&gt; Before, you saw a blocked task only if you looked while it was blocked. Now every blocked stretch stays on the record with its length, and the dashboard shows which workspace keeps getting stuck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Improvements become measurable.&lt;/strong&gt; Change a prompt, a skill or a workflow, and watch whether the typical start to close drops the next week. One fast task proves nothing. A p50 over a week of tasks does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the trade-offs?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No backfill of history.&lt;/strong&gt; History that was never recorded can't be rebuilt. Existing tasks start their timeline at their next status change. (The dashboard does backfill from whatever history exists, once, on the first boot after the upgrade.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p50 is an estimate.&lt;/strong&gt; It comes from a mergeable histogram, not from sorting every task. That's the price of rolling up months of data cheaply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One more write per status change.&lt;/strong&gt; It's small, and it happens inside a transaction we were already opening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A derived state depends on every path that can answer a question.&lt;/strong&gt; "Needs input" is right only because every way a question gets asked or answered re-checks it. That's why the logic lives in the storage layer and not in the individual features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The enum is a one-way door.&lt;/strong&gt; You can add states. You can never remove, rename or reorder the stored ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why not full event sourcing?&lt;/strong&gt;&lt;br&gt;
Too much for this. The status is read in many places, including by agents over MCP. An append-only history &lt;strong&gt;beside&lt;/strong&gt; the current status gets the durations without migrating every reader.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not work out durations from the message thread?&lt;/strong&gt;&lt;br&gt;
Messages and status changes are different streams. An agent can go blocked without writing anything, and a quiet agent can be working for 30 minutes. A dedicated history records exactly what changed and when.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does "needs input" change a task's status?&lt;/strong&gt;&lt;br&gt;
No. It exists only in the history. Queues, counts, filters and agents see the same status as before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does the dashboard show p50 instead of the average?&lt;/strong&gt;&lt;br&gt;
Agent task durations have a long tail. One task blocked over a weekend drags the average far from anything typical. The median doesn't move, and max is right next to it for finding the outlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which states can appear?&lt;/strong&gt;&lt;br&gt;
Not started, ongoing, blocked, needs input, completed, rejected, and scheduled for &lt;a href="https://agentrq.com/features/task-scheduling" rel="noopener noreferrer"&gt;recurring or one-time scheduled tasks&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;The change is in &lt;a href="https://github.com/agentrq/agentrq/pull/756" rel="noopener noreferrer"&gt;agentrq/agentrq#756&lt;/a&gt;, and the &lt;a href="https://agentrq.com/features/task-timeline" rel="noopener noreferrer"&gt;Task Timeline feature page&lt;/a&gt; has the short tour. Open any task that has changed status since the update and look under the description.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question for you:&lt;/strong&gt; if you run agents against a task queue, how do you tell "the agent was slow" from "I was slow"? Do you track human wait time separately, or does it all go into one latency number?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://agentrq.com/blog/task-status-history-timeline-for-ai-agent-tasks" rel="noopener noreferrer"&gt;AgentRQ blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>observability</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Single Responsibility for AI Agents: One Workspace, One Job</title>
      <dc:creator>Mustafa Turan</dc:creator>
      <pubDate>Mon, 28 Sep 2026 04:29:21 +0000</pubDate>
      <link>https://dev.to/agentrq/single-responsibility-for-ai-agents-one-workspace-one-job-3532</link>
      <guid>https://dev.to/agentrq/single-responsibility-for-ai-agents-one-workspace-one-job-3532</guid>
      <description>&lt;p&gt;You would never ship a &lt;code&gt;God&lt;/code&gt; class that handles billing, email templates, database migrations and the marketing site. So why do so many of us run exactly that as an AI agent?&lt;/p&gt;

&lt;p&gt;I did, for months. One agent, one workspace, every repository, every note I had ever written, and a mission description trying to cover a whole company. It was never bad. It was never great at anything either. It wrote landing page copy in the tone of a commit message. It suggested a database migration in the middle of a blog post. It applied a rule from one project to another where that rule was flat-out wrong.&lt;/p&gt;

&lt;p&gt;So I refactored it the way I would refactor code: &lt;strong&gt;one workspace, one job.&lt;/strong&gt; This post covers what that means in practice, the two reasons it works, how isolated agents still collaborate (pub/sub, not DMs), and the places where I think you should disagree with me.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;An AI agent's memory is only useful if every note in it is true for every task it runs. Mixing jobs breaks that.&lt;/li&gt;
&lt;li&gt;Everything an agent loads at the start of a task, it pays for on every turn. A narrow job means a small starting context.&lt;/li&gt;
&lt;li&gt;Isolated workspaces coordinate through &lt;strong&gt;events&lt;/strong&gt; (publish/subscribe) and one &lt;strong&gt;supervisor&lt;/strong&gt; agent, never agent-to-agent messages.&lt;/li&gt;
&lt;li&gt;Split where the knowledge diverges, not where the task names differ.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is an isolated, single-purpose AI agent workspace?
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://agentrq.com" rel="noopener noreferrer"&gt;AgentRQ&lt;/a&gt;, so that is the vocabulary I will use, but the idea is tool-agnostic. A &lt;strong&gt;workspace&lt;/strong&gt; is the unit an agent connects to. It holds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;mission&lt;/strong&gt; the agent reads when it starts a task,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;task board&lt;/strong&gt; with the history of every job,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;memory&lt;/strong&gt; the agent reads and writes (&lt;code&gt;loadMemory&lt;/code&gt; / &lt;code&gt;saveMemory&lt;/code&gt; over MCP),&lt;/li&gt;
&lt;li&gt;a set of &lt;strong&gt;skills&lt;/strong&gt; it can search and load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Single-purpose" is just a discipline on top: one workspace, one definition of done. Not "engineering". Something closer to "the static marketing site and its SEO content." If you can't describe success in one sentence, the scope is too wide.&lt;/p&gt;

&lt;p&gt;Today I run a handful: static site, core app, QA, support, social, outreach. They don't know about each other. That is the point.&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%2F21pu7vnaki0ofrkrv5a1.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%2F21pu7vnaki0ofrkrv5a1.png" alt="Four isolated AgentRQ workspaces (static site, core app, outreach and support), each with its own memory and skills" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a shared workspace make AI agent memory go bad?
&lt;/h2&gt;

&lt;p&gt;This is the part I underestimated most.&lt;/p&gt;

&lt;p&gt;Agent memory rots in a specific way when jobs share it: a lesson that is true in one context gets applied in another where it is false.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app repo:     "Always run the migration before deploying."   ✅ gospel
# static site:  "Always run the migration before deploying."   ❌ nonsense

# static site:  "The Markdown converter has no italics; use bold."  ✅ vital
# app repo:     "The Markdown converter has no italics; use bold."  🤷 noise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put both in one pile and the agent has to guess which rules apply, on every task. Sometimes it guesses wrong, confidently. It's the agent equivalent of a global mutable config shared between services.&lt;/p&gt;

&lt;p&gt;In a single-purpose workspace, every note is about the same system. The workspace that builds our site has a memory index of about forty entries, and every one of them is about that site: the converter has no italics or blockquotes, headings render as plain text, the CSS hash changes on every build and that diff churn is expected, slugs should be long and descriptive. None of those would survive contact with another project. Here, all of them are simply true.&lt;/p&gt;

&lt;p&gt;Two side effects I didn't expect:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The agent writes memory freely.&lt;/strong&gt; I don't have to curate defensively, wondering whether a note will leak into the wrong context. The agent writes down whatever would have saved it a detour, and the next run skips the detour. Learning compounds instead of colliding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I can actually review it.&lt;/strong&gt; Opening one workspace's memory is reading the operating manual for one job. I can spot a wrong note in a minute. A mixed memory is a junk drawer, and nobody audits a junk drawer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How much context does an AI agent really need to start a task?
&lt;/h2&gt;

&lt;p&gt;Only what the job needs, and the difference adds up.&lt;/p&gt;

&lt;p&gt;Before an agent does any work, the mission, the memory index, the skill descriptions and the instructions all go into its context window. In a stateless tool-calling loop, that prefix is re-sent on every turn.&lt;/p&gt;

&lt;p&gt;A do-everything workspace has a do-everything prefix. The mission explains five businesses. The memory index lists notes for all of them. The skill list offers a release playbook to an agent writing a tweet. Most of it is noise for the task at hand.&lt;/p&gt;

&lt;p&gt;A single-purpose workspace starts smaller because there is less to say. I haven't benchmarked the exact saving (it depends entirely on how big your notes get and how long your tasks run), so I won't invent a number. The shape is simple arithmetic, though:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost ≈ starting_context × turns + work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shrink the first term and you save on every turn of every task.&lt;/p&gt;

&lt;p&gt;Tokens are the smaller half. The bigger half is &lt;strong&gt;attention&lt;/strong&gt;: an agent whose window is full of relevant context makes better decisions than one that has to ignore half of what it was handed. I pair this with &lt;a href="https://agentrq.com/features/clear-context" rel="noopener noreferrer"&gt;Clear Context&lt;/a&gt;, which starts every task on a clean window, for the same reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do isolated AI agents collaborate without sharing context?
&lt;/h2&gt;

&lt;p&gt;The obvious objection: a feature has to be built, tested, released, written up and announced. No single workspace owns all of that.&lt;/p&gt;

&lt;p&gt;The workspaces are isolated from each other's &lt;strong&gt;context&lt;/strong&gt;, not from each other's &lt;strong&gt;work&lt;/strong&gt;. They coordinate in two ways, and neither involves one agent reading another's memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A supervisor agent that sees everything
&lt;/h3&gt;

&lt;p&gt;One &lt;a href="https://agentrq.com/blog/supervisor-worker-agentic-system" rel="noopener noreferrer"&gt;supervisor agent&lt;/a&gt; connects to an account-wide &lt;a href="https://agentrq.com/docs/supervisor-mcp" rel="noopener noreferrer"&gt;Supervisor MCP&lt;/a&gt; instead of a single workspace. It can see every workspace, task and status; create tasks anywhere; and move a task that landed in the wrong place.&lt;/p&gt;

&lt;p&gt;When I want a feature shipped end to end, I brief the supervisor once. It hands the core app its half, the static site its half, and outreach its half, each written for that workspace. The workers stay narrow; the supervisor is the one place allowed to be broad. And it has a single job too: dispatching, not doing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Events, not direct messages
&lt;/h3&gt;

&lt;p&gt;For routine hand-offs I don't want a supervisor in the middle. The workspaces talk through &lt;a href="https://agentrq.com/features/events" rel="noopener noreferrer"&gt;events&lt;/a&gt;, which is plain publish/subscribe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A workspace finishes something and publishes a named event with a short payload.&lt;/li&gt;
&lt;li&gt;AgentRQ finds every trigger subscribed to that name.&lt;/li&gt;
&lt;li&gt;Each subscribed workspace gets a new task, with the payload in the task body.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The publisher doesn't know who is listening. The listeners don't know who published. &lt;strong&gt;The event name is the only contract.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These are the events my workspaces actually run on:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Published when&lt;/th&gt;
&lt;th&gt;Who picks it up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;code_changed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core app merges a change&lt;/td&gt;
&lt;td&gt;QA runs its checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qa_failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;QA finds a regression&lt;/td&gt;
&lt;td&gt;Core app gets a fix task, failure attached&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bug_fixed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core app lands the fix&lt;/td&gt;
&lt;td&gt;QA re-checks, Support tells the reporter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feature_released&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A release goes out&lt;/td&gt;
&lt;td&gt;Static site drafts a post, updates feature pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blog_published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A post goes live&lt;/td&gt;
&lt;td&gt;Social turns it into an X thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x_thread_created&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The thread is posted&lt;/td&gt;
&lt;td&gt;Outreach adds it to follow-ups&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every name is &lt;strong&gt;past tense&lt;/strong&gt;, on purpose. An event reports a fact any workspace can react to in its own way. &lt;code&gt;fix_the_bug&lt;/code&gt; would be a command aimed at one agent, which is just a direct message wearing an event's name.&lt;/p&gt;

&lt;p&gt;A trigger can also name the event to fire when its task completes, so the chain wires itself. Here is the release part of it as a &lt;a href="https://agentrq.com/features/workflows" rel="noopener noreferrer"&gt;workflow&lt;/a&gt; called &lt;code&gt;release_cycle&lt;/code&gt;, spanning five workspaces. Note the fan-out on &lt;code&gt;bug_fixed&lt;/code&gt;: Support notifies reporters while Core App cuts the release, in parallel.&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%2Fm2uhqpb2lqzmuq4ct01r.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%2Fm2uhqpb2lqzmuq4ct01r.png" alt="The release_cycle workflow in AgentRQ: code_changed starts QA, which emits qa_failed; Core App fixes it and emits bug_fixed, which fans out to Support and to Core App cutting the release; feature_released starts Static Site, which emits blog_published; Social turns it into an X thread and emits x_thread_created" width="800" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I ran it once on a local dev build to check it behaves like the table says. I created exactly one task, a Core App merge that published &lt;code&gt;code_changed&lt;/code&gt;. Every task after that was created by an event. Each workspace did its part over its own connection, knowing only its own mission:&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%2F37u6xgdqxyplmvp4zk58.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%2F37u6xgdqxyplmvp4zk58.png" alt="Tasks from one run of the release_cycle workflow, all completed: the Core App merge, QA's regression check, the Core App fix, the Support reply, the release, the Static Site post and the Social X thread" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Outreach isn't even in that workflow. It subscribes to &lt;code&gt;x_thread_created&lt;/code&gt; on its own. The chain grows at the edges without anyone rewiring the middle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just let agents message each other?
&lt;/h3&gt;

&lt;p&gt;Same reason you put a queue between microservices instead of having every service call every other one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coupling.&lt;/strong&gt; An agent that can DM any other agent has to know who they all are and what each does. That is exactly the cross-domain context I kept out of its window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combinatorics.&lt;/strong&gt; Point-to-point links grow as &lt;code&gt;n(n-1)/2&lt;/code&gt;. Five workspaces is already 10 conversations to keep straight, each a place for a request to get lost or answered twice. With events, adding a sixth workspace is one new trigger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability.&lt;/strong&gt; Every hand-off is a task with a title, a body describing what happened upstream, and a status on the board. When a chain breaks, I'm reading tasks, not reconstructing a group chat between bots.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What does isolating AI agents actually unlock?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agents get good at one job and stay good.&lt;/strong&gt; The workspace that runs our site has closed more than ninety tasks, and lessons from the first ones are still in its memory, still true, still read before every new task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegation without re-explaining.&lt;/strong&gt; I send a one-paragraph brief from my phone. The workspace already knows the tone, the build steps, the formatting quirks and what I rejected last time. That's the difference between delegating a task and babysitting one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small blast radius.&lt;/strong&gt; A bad note or a confused agent can only damage one job. When outreach learns something wrong, the release process doesn't notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap agent swaps.&lt;/strong&gt; The knowledge lives in the workspace, not in one vendor's config file. Point a different coding agent at the workspace and it inherits the mission, memory and skills on day one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pitfalls I actually hit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Going too narrow.&lt;/strong&gt; After the first split I split everything: blog posts, glossary terms, feature pages, each in its own workspace. Same repo, same build, same quirks, and I was teaching three agents the same lessons. The rule now: &lt;strong&gt;split where the knowledge diverges, not where the task names do.&lt;/strong&gt; If two jobs would write the same notes, they belong together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Genuinely shared knowledge.&lt;/strong&gt; Some rules are universal: how PRs are described, which branch never gets a direct push. Copying them into every workspace is the same rot, spread thinner. Keep memory for what's local and put what's universal in something shared. In AgentRQ that's &lt;a href="https://agentrq.com/blog/distributed-agent-skills-over-mcp-across-workspaces" rel="noopener noreferrer"&gt;skills shared across workspaces&lt;/a&gt;: fix a playbook once, fixed everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Work that crosses the line.&lt;/strong&gt; A launch needs code in the app and a post on the site. I don't create a third workspace; I split it into two tasks, via the supervisor or a &lt;code&gt;feature_released&lt;/code&gt; event. A task that lands in the wrong workspace gets moved, with its history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory still goes stale.&lt;/strong&gt; Isolation keeps memory &lt;em&gt;relevant&lt;/em&gt;, not &lt;em&gt;current&lt;/em&gt;. Tools get upgraded, a limitation gets fixed, and the note warning about it becomes a lie. Tell the agent to correct or delete a note when it finds one that no longer holds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup cost.&lt;/strong&gt; A new workspace needs a mission, a connected agent and a few tasks before its memory is worth anything. Isolation pays back on work that repeats, not on one-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where you should push back
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Context windows are huge now, just give it everything."&lt;/strong&gt; Windows have grown, and strong models ignore a lot of noise. But every token is still billed on every turn, and ignoring noise is a skill models have, not one they are perfect at. If your contexts are small and cost isn't a concern, this argument carries real weight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"A generalist sees connections a specialist misses."&lt;/strong&gt; The strongest objection, and true. My narrow workspaces will never notice on their own that a new feature deserves a post. The supervisor and events cover the connections I already know about, but a dispatcher over narrow workers isn't one mind holding everything. If cross-domain insight is your main value, a generalist may serve you better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Just use retrieval over one big memory."&lt;/strong&gt; Works when retrieval is good; fails &lt;em&gt;quietly&lt;/em&gt; when it isn't. The wrong note ranks high and the agent acts on it. Isolation is blunter, but it fails &lt;em&gt;loudly&lt;/em&gt;: you can always see which workspace you are in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Overkill for small projects."&lt;/strong&gt; Often, yes. One repo and one kind of work? One workspace &lt;em&gt;is&lt;/em&gt; the single-purpose workspace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Teams need shared context, not silos."&lt;/strong&gt; For teams, boundaries should follow ownership and on-call lines, not one person's mental model. What I describe is shaped for one person running many jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is a single-purpose AI agent workspace?&lt;/strong&gt;&lt;br&gt;
A workspace scoped to one job with one definition of done, holding its own mission, memory, skills and task history, so an agent working in it only ever loads context that is relevant to that job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does isolating AI agents reduce token usage?&lt;/strong&gt;&lt;br&gt;
Usually. The mission, memory index and skill list are loaded at the start of every task and re-sent on every turn, so a narrower workspace means a smaller fixed cost per turn. The exact saving depends on your notes and task length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do isolated AI agents hand off work?&lt;/strong&gt;&lt;br&gt;
Through publish/subscribe events (a workspace publishes a past-tense event like &lt;code&gt;bug_fixed&lt;/code&gt;; subscribed workspaces receive a new task) or through a supervisor agent with account-wide visibility that creates tasks in each workspace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should I keep one workspace instead of splitting?&lt;/strong&gt;&lt;br&gt;
When every note in your agent's memory is true for every task it runs. If you catch yourself writing "except in the other project" into a note, that workspace probably wants to become two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on one job
&lt;/h2&gt;

&lt;p&gt;None of this is a law. It's the setup that fits one person running many unrelated jobs and wanting to delegate and walk away. I know people who run one giant workspace very productively, people who spin up a workspace per feature and throw it away on ship, and teams who isolate by customer instead of function.&lt;/p&gt;

&lt;p&gt;So take the question, not my answer: &lt;strong&gt;is every note in your agent's memory true for every task it will run?&lt;/strong&gt; If not, split one job out, give it a week of tasks, and read its memory at the end.&lt;/p&gt;

&lt;p&gt;How do you scope your agents: one generalist, many specialists, or something else? I'd like to hear what's working for you in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://agentrq.com/blog/why-i-use-isolated-single-purpose-workspaces-for-ai-agents" rel="noopener noreferrer"&gt;AgentRQ blog&lt;/a&gt;. AgentRQ is a human-in-the-loop task manager for AI agents: &lt;a href="https://agentrq.com" rel="noopener noreferrer"&gt;agentrq.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Zero Dependency Open Source</title>
      <dc:creator>Mustafa Turan</dc:creator>
      <pubDate>Thu, 02 May 2019 07:29:01 +0000</pubDate>
      <link>https://dev.to/mustafaturan/zero-dependency-open-source-o7g</link>
      <guid>https://dev.to/mustafaturan/zero-dependency-open-source-o7g</guid>
      <description>&lt;p&gt;Writing a zero-dependency open-source library could be the way to bring an immortal contribution to the developer communities. Adding dependencies to your open-source will bring many update/upgrade work while your busy life goes on. And you/community may give-up one day to update those dependencies because of several reasons. Even the zero-dependency library might need language version updates. So, the real intention is keeping as minimum dependency as possible. &lt;/p&gt;




&lt;p&gt;Instead of putting dependencies, put interfaces to the library and let the library users to use their own choice of libraries by implementing your interface. And share sample implementations with a dependent library as an example instead of directly adding implementation. (What is being defined is also a part of hexagonal-architecture pattern for software design which makes the library optioned without implementation choices.) &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4 steps to provide zero-dependency library:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Implement your library functionality&lt;/li&gt;
&lt;li&gt;When you need an external dependency(external library from another author), create an interface instead of adding the dependency&lt;/li&gt;
&lt;li&gt;Document a sample implementation with/without dependency (but do not implement)&lt;/li&gt;
&lt;li&gt;Let the library user decide what to use &amp;amp; leave the interface implementation to the library user. &lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;This could be the way to bring an immortal contribution to the developer communities. Since there will be no dependency, the library can be used for new projects/libraries with different implementation details. Even after years, new implementations would be possible without updating the library...&lt;/p&gt;

&lt;p&gt;Before taking action, it is highly recommended also reading &lt;a href="https://dhh.dk/2012/rails-is-omakase.html" rel="noopener noreferrer"&gt;DHH's Rails is Omakase&lt;/a&gt; post. More ideas are highly welcome. Let's discuss.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Decoupled Package Communication in Go</title>
      <dc:creator>Mustafa Turan</dc:creator>
      <pubDate>Sun, 28 Apr 2019 23:14:24 +0000</pubDate>
      <link>https://dev.to/mustafaturan/decoupled-package-communication-in-go-g39</link>
      <guid>https://dev.to/mustafaturan/decoupled-package-communication-in-go-g39</guid>
      <description>&lt;p&gt;Go is very powerful language for software development with its simplicity, concurrency, first-class functions and tools. &lt;/p&gt;

&lt;p&gt;In Go, usually, a package is responsible only for one thing. Applications grows with several packages with their own responsibilities. When the number of packages in the project and responsibilities of the project increase, it is possible to mess up on communication with other packages. (In here, the communication means calling functions on different packages/modules on the flow with/without waiting result. Like incrementing counter metric when an order received/full-filled)&lt;/p&gt;




&lt;p&gt;One of the known ways of decoupling on communication is the event driven software designs. Usually, using an external event/message-bus server is the way distributing the responsibility to internal/external services. But this also brings another external component to your stack. And each stack comes with its own problems.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;For small services, packages/libraries, and embedded systems adding an external bus could be unnecessary addition. To overcome this problem, adding an internal message-bus package to your package may help. And when things getting bigger, it is possible to communicate with this internal message-bus to external message-bus services.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fawa9905696zi1go0fj80.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fawa9905696zi1go0fj80.png" alt="event bus"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Assume that, a project needs to do several things on user registration like; incrementing counters, sending email, logging, and/or sending webhook to external service; instead of directly calling each of the functionalities directly, a message-bus can help to execute multiple functionalities asynchronously without knowing the caller.&lt;/p&gt;




&lt;p&gt;Decoupled packages help us to write cleaner code, focus on only one thing at a time. With &lt;a href="https://github.com/mustafaturan/bus" rel="noopener noreferrer"&gt;bus&lt;/a&gt; Go package, you can get benefit of real message bus system and write decoupled packages easily. The &lt;code&gt;bus&lt;/code&gt; package allows any handler to listen any events, without knowing who generates the event. Thus, packages can communicate without depending on each other. Moreover, it is very easy to substitute a consumer function. As long as the new function understands the &lt;code&gt;Event&lt;/code&gt; struct that are being sent and received, the other functions will never know.&lt;/p&gt;




&lt;h3&gt;
  
  
  Get Started With &lt;code&gt;Bus&lt;/code&gt; Go Package in 4 Steps
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Configure
&lt;/h4&gt;

&lt;p&gt;The &lt;a href="https://github.com/mustafaturan/bus" rel="noopener noreferrer"&gt;&lt;code&gt;bus&lt;/code&gt;&lt;/a&gt; package requires a unique id generator to assign ids to events. You can write your own function to generate unique ids or use a package that provides unique id generation functionality. Here is a sample configuration using monoton id generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/mustafaturan/bus"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/mustafaturan/monoton"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/mustafaturan/monoton/sequencer"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// configure id generator (it doesn't have to be monoton)&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt;        &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;initialTime&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;monoton&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sequencer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMillisecond&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initialTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// configure bus&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;monoton&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"whoops"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Register Event Topics
&lt;/h4&gt;

&lt;p&gt;To emit events to the topics, topic names need to be registered first:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="c"&gt;// register topics&lt;/span&gt;
    &lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterTopics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order.received"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order.fulfilled"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Register Event Handlers
&lt;/h4&gt;

&lt;p&gt;To receive topic events you need to register handlers; A handler basically requires two values which are a &lt;code&gt;Handle&lt;/code&gt; function and topic &lt;code&gt;Matcher&lt;/code&gt; &lt;em&gt;regex pattern&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt; 
        &lt;span class="c"&gt;// fmt.Printf("Event: %+v %+v\n", e, e.Topic)&lt;/span&gt;
        &lt;span class="c"&gt;// do something&lt;/span&gt;
        &lt;span class="c"&gt;// NOTE: Highly recommended to process the event in an async way&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;Matcher&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;".*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// regex pattern that matches all topics&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a unique key for the handler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Emitting Events
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;bus&lt;/code&gt; package provides a generic event struct to standardize what an event is for all of its handlers. Before delivering an event, it is good to know what an event is for the &lt;a href="https://github.com/mustafaturan/bus" rel="noopener noreferrer"&gt;&lt;code&gt;bus&lt;/code&gt;&lt;/a&gt; package. If this is your fist moments with the library, it is highly recommended to check the data structure of the &lt;a href="https://godoc.org/github.com/mustafaturan/bus#Event" rel="noopener noreferrer"&gt;&lt;code&gt;Event&lt;/code&gt;&lt;/a&gt; struct.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;txID&lt;/span&gt;  &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"some-transaction-id-if-exists"&lt;/span&gt; &lt;span class="c"&gt;// if it is blank, bus will generate one&lt;/span&gt;
&lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"order.received"&lt;/span&gt;                &lt;span class="c"&gt;// event topic name (must be registered before)&lt;/span&gt;
&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c"&gt;// interface{} data for event&lt;/span&gt;

&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"orderID"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"123456"&lt;/span&gt;
&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"orderAmount"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"112.20"&lt;/span&gt;
&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"USD"&lt;/span&gt;

&lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;txID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// emit the event for the topic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Congratulations!!! You emitted your first event with the &lt;code&gt;bus&lt;/code&gt; package. Now, the all matched handlers will receive the same event. Please try yourself and share your experience as comment. &lt;/p&gt;

&lt;p&gt;Also a &lt;strong&gt;sample project&lt;/strong&gt; using &lt;a href="https://github.com/mustafaturan/bus" rel="noopener noreferrer"&gt;&lt;code&gt;bus&lt;/code&gt;&lt;/a&gt; package can be found on &lt;a href="https://github.com/mustafaturan/bus-sample-project" rel="noopener noreferrer"&gt;bus-sample-project Github&lt;/a&gt;.&lt;/p&gt;




&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/mustafaturan" rel="noopener noreferrer"&gt;
        mustafaturan
      &lt;/a&gt; / &lt;a href="https://github.com/mustafaturan/bus" rel="noopener noreferrer"&gt;
        bus
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      🔊Minimalist message bus implementation for internal communication with zero-allocation magic on Emit
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🔊 Bus&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://godoc.org/github.com/mustafaturan/bus" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/16d1db6f25b18596e00fc985058fba80651eda5888ae9c727e858a0a1b6f5fd0/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f6d757374616661747572616e2f6275733f7374617475732e737667" alt="GoDoc"&gt;&lt;/a&gt;
&lt;a href="https://travis-ci.org/mustafaturan/bus" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/29c54f33a486b768cb41b75ad5962aabe5d0b6d3728f4de9d7962b57af68d0fd/68747470733a2f2f7472617669732d63692e6f72672f6d757374616661747572616e2f6275732e7376673f6272616e63683d6d61696e" alt="Build Status"&gt;&lt;/a&gt;
&lt;a href="https://coveralls.io/github/mustafaturan/bus?branch=main" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/537774c3c43f44bf9d436bf48da4a1cb4b663b258a90282ef5162637364e5485/68747470733a2f2f636f766572616c6c732e696f2f7265706f732f6769746875622f6d757374616661747572616e2f6275732f62616467652e7376673f6272616e63683d6d61696e" alt="Coverage Status"&gt;&lt;/a&gt;
&lt;a href="https://goreportcard.com/report/github.com/mustafaturan/bus" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/87ca88c8f7288613f3cfee04fb1450b1adf009f4706a6247a8b55a84b95fde5d/68747470733a2f2f676f7265706f7274636172642e636f6d2f62616467652f6769746875622e636f6d2f6d757374616661747572616e2f627573" alt="Go Report Card"&gt;&lt;/a&gt;
&lt;a href="https://github.com/mustafaturan/bus/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/cb5e8d0e620413e6b76e6fe7fda054afda903498d96b2ec31fc2a42c6ede4d95/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6d757374616661747572616e2f6275732e737667" alt="GitHub license"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Bus is a minimalist event/message bus implementation for internal communication
It is heavily inspired from my &lt;a href="https://github.com/otobus/event_bus" rel="noopener noreferrer"&gt;event_bus&lt;/a&gt;
package for Elixir language.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;API&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The method names and arities/args are stable now. No change should be expected
on the package for the version &lt;code&gt;3.x.x&lt;/code&gt; except any bug fixes.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Installation&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Via go packages:
&lt;code&gt;go get github.com/mustafaturan/bus/v3&lt;/code&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Configure&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;The package requires a unique id generator to assign ids to events. You can
write your own function to generate unique ids or use a package that provides
unique id generation functionality.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;bus&lt;/code&gt; package respect to software design choice of the packages/projects. It
supports both singleton and dependency injection to init a &lt;code&gt;bus&lt;/code&gt; instance.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Hint:&lt;/em&gt;
Check the &lt;a href="https://github.com/mustafaturan/bus-sample-project" rel="noopener noreferrer"&gt;demo project&lt;/a&gt; for
the singleton configuration.&lt;/p&gt;
&lt;p&gt;Here is a sample initilization using &lt;code&gt;monoton&lt;/code&gt; id generator:&lt;/p&gt;
&lt;div class="highlight highlight-source-go notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; (
    &lt;span class="pl-s"&gt;"github.com/mustafaturan/bus/v3"&lt;/span&gt;
    &lt;span class="pl-s"&gt;"github.com/mustafaturan/monoton/v2"&lt;/span&gt;
    &lt;span class="pl-s"&gt;"github.com/mustafaturan/monoton/v2/sequencer"&lt;/span&gt;
)
&lt;span class="pl-k"&gt;func&lt;/span&gt; &lt;span class="pl-en"&gt;NewBus&lt;/span&gt;() &lt;span class="pl-c1"&gt;*&lt;/span&gt;bus.&lt;span class="pl-smi"&gt;Bus&lt;/span&gt; {
    &lt;span class="pl-c"&gt;// configure id generator (it doesn't have&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/mustafaturan/bus" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>go</category>
      <category>pubsub</category>
      <category>eventbus</category>
      <category>messagebus</category>
    </item>
    <item>
      <title>Persisting event data to Postgres using GenStage and EventBus</title>
      <dc:creator>Mustafa Turan</dc:creator>
      <pubDate>Tue, 31 Jul 2018 08:10:52 +0000</pubDate>
      <link>https://dev.to/mustafaturan/persisting-event-data-to-postgres-using-genstage-and-eventbus-oi9</link>
      <guid>https://dev.to/mustafaturan/persisting-event-data-to-postgres-using-genstage-and-eventbus-oi9</guid>
      <description>&lt;p&gt;One of the ways to consume EventBus events is implementing GenStage consumers. GenStage handles backpressure easily with configurable workers. &lt;code&gt;event_bus_postgres&lt;/code&gt; library uses GenStage to persist &lt;code&gt;event_bus&lt;/code&gt; events to postgres DB with batch insert.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;+-----+
|     |                                         GEN STAGE
|     |        EVENTBUS      +------------------------------------------+
|     |        CONSUMER      |                   +---+                  |
|     |        +-----+       |                   |   |                  |
|     |        |     |       |                   |   |          +---+   |
|     |        |  E  |       |                   |   |          |   |   |
|     |        |  v  |       |                   |   |          |   |   |
|     |        |  e  |       |                   |   |          |   |   |
|  E  |        |  n  |       |                   | E |          |   |   |
|  l  |        |  t  |       |  +-------+        | v |          |   |   |
|  i  | topic  |  B  |  topic   |       |        | e |          |   |
|  x  |   +    |  u  |    +     |   Q   |        | n |          | B |       +--+
|  i  |event_id|  s  | event_id |   u   |   ask  | t |    ask   | u |       |  |
|  r  |-------&amp;gt;|  .  |---------&amp;gt;|   e   |&amp;lt;-------|   | &amp;lt;--------| c | BATCH |  |
|     |        |  P  |          |   u   |-------&amp;gt;| M | --------&amp;gt;| k |------&amp;gt;|DB|
|  E  |        |  o  |          |   e   |   pull | a |    pull  | e | INSERT|  |
|  v  |        |  s  |          |       |        | p |          | t |       |  |
|  e  |        |  t  |       |  +-------+        | p |          |   |   |   +--+
|  n  |        |  g  |       |  GENSTAGE         | e |          |   |   |
|  t  |        |  r  |       |  PRODUCER         | r |          |   |   |
|  B  |        |  e  |       |                   |   |          |   |   |
|  u  |        |  s  |       |                   |   |          |   |   |
|  s  |        +-----+       |                   |   |          |   |   |
|     |&amp;lt;-----------------------------------------|   |          +---+   |
|     |                      |    fetch_event/1  |   |         CONSUMER |
|     |                      |                   |   |                  |
+-----+                      |                   +---+                  |
                             |                  CONSUMER                |
                             |                  PRODUCER                |
                             +------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Components
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;EventBus&lt;/strong&gt;&lt;br&gt;
Message bus for Elixir; it publishes &lt;code&gt;event_id&lt;/code&gt; and &lt;code&gt;topic&lt;/code&gt; data to &lt;code&gt;topic&lt;/code&gt; subscribers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EventBus.Postgres&lt;/strong&gt;&lt;br&gt;
Message bus event consumer; it pushes &lt;code&gt;event_id&lt;/code&gt; and &lt;code&gt;topic&lt;/code&gt; to the &lt;code&gt;EventBus.Postgres.Queue&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;GenStage&lt;/code&gt; producer; it is a simple queue implementaion&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EventMapper&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;GenStage&lt;/code&gt; producer-consumer; it pulls/dequeues from &lt;code&gt;EventBus.Postgres.Queue&lt;/code&gt;, and fetch original event from &lt;code&gt;EventBus&lt;/code&gt; and then convert data into &lt;code&gt;Ecto&lt;/code&gt; model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bucket&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;GenStage&lt;/code&gt; consumer; it pulls/dequeues from &lt;code&gt;EventBus.Postgres.EventMapper&lt;/code&gt; and batch insert data to Postgres DB.&lt;/p&gt;

&lt;p&gt;Source code: &lt;a href="https://github.com/otobus/event_bus_postgres" rel="noopener noreferrer"&gt;https://github.com/otobus/event_bus_postgres&lt;/a&gt;&lt;/p&gt;

</description>
      <category>genstage</category>
      <category>eventbus</category>
      <category>elixir</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Decoupled Modules with Elixir EventBus</title>
      <dc:creator>Mustafa Turan</dc:creator>
      <pubDate>Mon, 23 Jul 2018 07:48:41 +0000</pubDate>
      <link>https://dev.to/mustafaturan/decoupled-modules-with-elixireventbus-4fcd</link>
      <guid>https://dev.to/mustafaturan/decoupled-modules-with-elixireventbus-4fcd</guid>
      <description>&lt;p&gt;Elixir EventBus is a library that allows different modules to communicate with each other without knowing about each other. A module/function can create an Event struct, and deliver to the EventBus without knowing which modules will consume.&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%2Fzl0hffq02hpk7vh0h5im.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%2Fzl0hffq02hpk7vh0h5im.png" width="800" height="199"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Modules can also listen to events on the EventBus, without knowing who sent the events. Thus, modules can communicate without depending on each other. Moreover, it is very easy to substitute a consumer module. As long as the new module understands the &lt;code&gt;Event&lt;/code&gt; struct that is being sent and received, the other modules will never know.&lt;/p&gt;




&lt;h3&gt;
  
  
  Get started with decoupled modules in 4&amp;nbsp;steps
&lt;/h3&gt;

&lt;p&gt;Decoupled modules help us to write clear code, focus on only one thing at a time. With EventBus library, you can get benefit of real event bus system and write decoupled modules easily.&lt;/p&gt;

&lt;h4&gt;
  
  
  Registering event&amp;nbsp;topics
&lt;/h4&gt;

&lt;p&gt;On your app init, or anytime before delivering the events we need to register our topics. For example, when we receive a payment, we will create &lt;code&gt;checkout_completed&lt;/code&gt; event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register_topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:checkout_completed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register_topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:checkout_failed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Delivering events
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;EventBus.EventSource&lt;/code&gt; module provides notify helper to deliver events without modifying your current code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Order&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;EventSource&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;event_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;topic:&lt;/span&gt; &lt;span class="ss"&gt;:checkout_completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;error_topic:&lt;/span&gt; &lt;span class="ss"&gt;:checkout_failed&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="no"&gt;EventSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# process the payment as usual in here and if errors then return {:error, _} tuple&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Subscribing consumers to&amp;nbsp;topics
&lt;/h4&gt;

&lt;p&gt;Assume that we have several operations waiting on success and failure conditions. Let's subscribe the consumers to the event topics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="no"&gt;StockUpdateService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^checkout_completed$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"^new_stock_arrived$"&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="no"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="no"&gt;CargoService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^checkout_completed$, "&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;cargo_dispatched&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;new_stock_arrived&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;", ...]})
EventBus.subscribe({EmailService, ["&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;user_registered&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;checkout_completed&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;", ...]})
EventBus.subscribe({EventBus.Logger, ["&lt;/span&gt;&lt;span class="o"&gt;.*&lt;/span&gt;&lt;span class="s2"&gt;"]})
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Consuming events
&lt;/h4&gt;

&lt;p&gt;EventBus consumers are just modules so they can be implemented by any kind of consumer strategy including GenStage(&lt;a href="https://github.com/otobus/event_bus_postgres" rel="noopener noreferrer"&gt;event_bus_postgres&lt;/a&gt; as sample), pooling, DynamicSupervisors, global consumers, spawn et al. So, depending on your use case, implement your consumers with the best suitable strategy. (But any kind of blocker strategies are not recommended!)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;CargoService&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;GenServer&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;topic&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="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;GenServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_shadow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_cast&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;topic&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="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;payment_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetch_event_data&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;topic&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="c1"&gt;# do sth with payment_data&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="c1"&gt;# mark event as completed for this consumer&lt;/span&gt;
    &lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark_as_completed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="no"&gt;CargoService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&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="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is another consumer that needs all event structure and consumes with spawning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;StockUpdateService&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;topic&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="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;     
      &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetch_event&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;topic&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="c1"&gt;# do sth with event.data and/or any other event attributes&lt;/span&gt;
      &lt;span class="o"&gt;...&lt;/span&gt;
      &lt;span class="c1"&gt;# mark event as completed for this consumer&lt;/span&gt;
      &lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark_as_completed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="no"&gt;StockUpdateService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&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="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  About the EventBus&amp;nbsp;library
&lt;/h3&gt;

&lt;p&gt;EventBus library is a traceable, extendible, fast, and memory friendly pure Elixir implementation without any external dependency in the pocket.&lt;/p&gt;

&lt;h4&gt;
  
  
  Traceable
&lt;/h4&gt;

&lt;p&gt;EventBus library comes with good optional attributes to provide traceability. When a consumer receive the topic and event_id, it can fetch structure with &lt;code&gt;fetch_event/1&lt;/code&gt; function. And if you use EventBus.EventSource helper, the optional fields are set automatically for you. Here is the structure of an Event model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;# required&lt;/span&gt;
  &lt;span class="ss"&gt;transaction_id:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;# optional&lt;/span&gt;
  &lt;span class="ss"&gt;topic:&lt;/span&gt; &lt;span class="n"&gt;atom&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;# required&lt;/span&gt;
  &lt;span class="ss"&gt;data:&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# required,&lt;/span&gt;
  &lt;span class="ss"&gt;initialized_at:&lt;/span&gt; &lt;span class="n"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;# optional&lt;/span&gt;
  &lt;span class="ss"&gt;occurred_at:&lt;/span&gt; &lt;span class="n"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;# optional&lt;/span&gt;
  &lt;span class="ss"&gt;source:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;# optional&lt;/span&gt;
  &lt;span class="ss"&gt;ttl:&lt;/span&gt; &lt;span class="n"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# optional&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Extendible
&lt;/h4&gt;

&lt;p&gt;EventBus library allows subscribing events with regex patterns, which allows consumers to subscribe new topics automatically. With this feature, it allows extending the system asynchronously with generic consumers like &lt;a href="https://github.com/otobus/event_bus_logger" rel="noopener noreferrer"&gt;event_bus_logger&lt;/a&gt;, &lt;a href="https://github.com/otobus/event_bus_postgres" rel="noopener noreferrer"&gt;generic Postgresql event store&lt;/a&gt;, &lt;a href="https://github.com/otobus/event_bus/wiki/EventBus-Metrics-and-UI" rel="noopener noreferrer"&gt;event_bus_metrics UI&lt;/a&gt;, and so on.&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%2Fx0pgwfryxkszzqvrmwub.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%2Fx0pgwfryxkszzqvrmwub.png" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Fast by&amp;nbsp;design
&lt;/h4&gt;

&lt;p&gt;EventBus library uses builtin memory store ETS effectively to get benefits of concurrent reads and writes. It doesn't block majority of the read operations, and allows concurrent reads to fetch Event data.&lt;/p&gt;

&lt;p&gt;It applies &lt;a href="https://www.vividcortex.com/resources/queueing-theory" rel="noopener noreferrer"&gt;queueing theory&lt;/a&gt; to handle inputs. And almost all implementation data accesses have O(1) complexity.&lt;/p&gt;

&lt;h4&gt;
  
  
  Memory friendly
&lt;/h4&gt;

&lt;p&gt;EventBus library delivers only topic and event_id to the consumers and keeps the original event data in the topic's Event table. So, only when consumers are able to process the event, then they fetch the event data.&lt;/p&gt;




&lt;h4&gt;
  
  
  Distributed?
&lt;/h4&gt;

&lt;p&gt;Even though the main intention to create this library is to use for internal module communications, it is possible to deliver events across nodes. One of the ways to delivery is sending message to connected other nodes as usual Elixir/Erlang way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="no"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:register_topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:checkout_completed&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="no"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:notify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[%{&lt;/span&gt;&lt;span class="no"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;}}])&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Note: EventBus library is not in the same class distributed solutions like Apache Kafka, RabbitMQ, or Redis Streams.&lt;/p&gt;

&lt;p&gt;Original story on Medium: &lt;a href="https://medium.com/elixirlabs/decoupled-modules-with-elixir-eventbus-a709b1479411" rel="noopener noreferrer"&gt;https://medium.com/elixirlabs/decoupled-modules-with-elixir-eventbus-a709b1479411&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The source code of EventBus library: &lt;a href="https://github.com/otobus/event_bus" rel="noopener noreferrer"&gt;https://github.com/otobus/event_bus&lt;/a&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>eventbus</category>
      <category>pubsub</category>
    </item>
    <item>
      <title>When to give a star to an open source library?</title>
      <dc:creator>Mustafa Turan</dc:creator>
      <pubDate>Tue, 17 Jul 2018 04:23:00 +0000</pubDate>
      <link>https://dev.to/mustafaturan/when-to-give-a-star-to-an-open-sourcelibrary-11a4</link>
      <guid>https://dev.to/mustafaturan/when-to-give-a-star-to-an-open-sourcelibrary-11a4</guid>
      <description>&lt;p&gt;Giving a star to an open source library means to a lot to the developers and consumers of the library. Because this will give the developers a positive impact to make the library better. It is definitely a way to tell gently to the developers, there are people out there who use that library.&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%2Fj0h3zdvnkp2qvw46pghp.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%2Fj0h3zdvnkp2qvw46pghp.png" alt="rails/rails repos current star count at 2018-07-15" width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  As a consumer of the&amp;nbsp;library
&lt;/h3&gt;

&lt;p&gt;If you are using an open source library, go ahead and find its Github/Gitlab (or any version control source) and give it a star. If you need to use a high-quality generic library, you should help to bring new people to the library. Starring will bring quality and aliveness to the library. One of the people that you bring, might be the next contributor to the library.&lt;/p&gt;

&lt;h3&gt;
  
  
  To support the work or&amp;nbsp;idea
&lt;/h3&gt;

&lt;p&gt;You find an open source library in the following interests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Like the idea&lt;/li&gt;
&lt;li&gt;Plan to use it in your current or next project&lt;/li&gt;
&lt;li&gt;Like the software design of the library&lt;/li&gt;
&lt;li&gt;A topic of interests like distributed systems, data structures, useful links&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't forget that any positive starring activity will bring more people to like the library with the social impact. This positive impact will also bring more contributors to the library. And will prevent the library to get out-dated.&lt;/p&gt;

&lt;p&gt;These are only my ideas while starring a library, please do not hesitate to share yours. I will be happy to include in the story including your name.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>developers</category>
      <category>community</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
